commit 0397a67ae8b87869eba0eebe83284915de649149 Author: xtcnet Date: Sat Mar 7 20:49:44 2026 +0700 Initial commit for D3V-NPMWG diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..86d8a8e --- /dev/null +++ b/.cursorrules @@ -0,0 +1,12 @@ +# AI Assistant Instructions + +You are working on **NPM-WG** (Nginx Proxy Manager + WireGuard). +Whenever you start a task in this workspace or are asked to fix a bug, please **FIRST READ the file `AI_CONTEXT.md`** at the root of the project. + +It contains: +- The backend API map for WireGuard integration. +- Details about Knex DB schema limitations (ES Modules). +- Fixes for line-endings (`CRLF` -> `LF`) requirements for Alpine Linux Docker build. +- Important commands required for the `.tsx` Vite environment before Docker container builds. + +Do not start writing or refactoring WireGuard integration code without reading `AI_CONTEXT.md` first. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6ec6a7c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,55 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + + +**Checklist** +- Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image? + - Yes / No +- Are you sure you're not using someone else's docker image? + - Yes / No +- Have you searched for similar issues (both open and closed)? + - Yes / No + +**Describe the bug** + + + +**Nginx Proxy Manager Version** + + + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + + +**Expected behavior** + + + +**Screenshots** + + + +**Operating System** + + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/dns_challenge_request.md b/.github/ISSUE_TEMPLATE/dns_challenge_request.md new file mode 100644 index 0000000..0a00f00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/dns_challenge_request.md @@ -0,0 +1,18 @@ +--- +name: DNS challenge provider request +about: Suggest a new provider to be available for a certificate DNS challenge +title: '' +labels: dns provider request +assignees: '' + +--- + +**What provider would you like to see added to NPM?** + + + +**Have you checked if a certbot plugin exists?** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..cf5b0f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + + + +**Is your feature request related to a problem? Please describe.** + + + +**Describe the solution you'd like** + + + +**Describe alternatives you've considered** + + + +**Additional context** + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f1b4ffb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,104 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/backend" + schedule: + interval: "weekly" + groups: + dev-patch-updates: + dependency-type: "development" + update-types: + - "patch" + dev-minor-updates: + dependency-type: "development" + update-types: + - "minor" + prod-patch-updates: + dependency-type: "production" + update-types: + - "patch" + prod-minor-updates: + dependency-type: "production" + update-types: + - "minor" + + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "weekly" + groups: + dev-patch-updates: + dependency-type: "development" + update-types: + - "patch" + dev-minor-updates: + dependency-type: "development" + update-types: + - "minor" + prod-patch-updates: + dependency-type: "production" + update-types: + - "patch" + prod-minor-updates: + dependency-type: "production" + update-types: + - "minor" + + - package-ecosystem: "npm" + directory: "/docs" + schedule: + interval: "weekly" + groups: + dev-patch-updates: + dependency-type: "development" + update-types: + - "patch" + dev-minor-updates: + dependency-type: "development" + update-types: + - "minor" + prod-patch-updates: + dependency-type: "production" + update-types: + - "patch" + prod-minor-updates: + dependency-type: "production" + update-types: + - "minor" + + - package-ecosystem: "npm" + directory: "/test" + schedule: + interval: "weekly" + groups: + dev-patch-updates: + dependency-type: "development" + update-types: + - "patch" + dev-minor-updates: + dependency-type: "development" + update-types: + - "minor" + prod-patch-updates: + dependency-type: "production" + update-types: + - "patch" + prod-minor-updates: + dependency-type: "production" + update-types: + - "minor" + + - package-ecosystem: "docker" + directory: "/docker" + schedule: + interval: "weekly" + groups: + updates: + update-types: + - "patch" + - "minor" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..3a2ae9e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + stale-issue-label: 'stale' + stale-pr-label: 'stale' + stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:' + stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:' + close-issue-message: 'Issue was closed due to inactivity.' + close-pr-message: 'PR was closed due to inactivity.' + days-before-stale: 182 + days-before-close: 365 + operations-per-run: 50 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bf37c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.idea +.qodo +._* +.vscode +certbot-help.txt +test/node_modules +*/node_modules +docker/dev/dnsrouter-config.json.tmp +docker/dev/resolv.conf diff --git a/.version b/.version new file mode 100644 index 0000000..edcfe40 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +2.14.0 diff --git a/AI_CONTEXT.md b/AI_CONTEXT.md new file mode 100644 index 0000000..83120ff --- /dev/null +++ b/AI_CONTEXT.md @@ -0,0 +1,64 @@ +# AI Context for NPM-WG Project + +## 1. Project Overview +**NPM-WG** is a custom fork of [Nginx Proxy Manager](https://github.com/NginxProxyManager/nginx-proxy-manager) integrated with **WireGuard VPN** management capabilities, inspired by `wg-easy`. + +The project structure remains mostly identical to Nginx Proxy Manager, but specific backend and frontend modules have been added to manage WireGuard securely inside the Docker container without needing external dependencies. + +--- + +## 2. Technology Stack +- **Backend**: Node.js, Express.js, Knex (Query Builder), SQLite/MySQL/PostgreSQL. Uses ES Modules (`"type": "module"`). +- **Frontend**: React 18, TypeScript, Vite, React Router, React Bootstrap (`ez-modal-react`), Formik, React Query (`@tanstack/react-query`). +- **Container**: Alpine Linux with `s6-overlay` for service process management. + +--- + +## 3. WireGuard Integration Architecture + +### Core Idea +WireGuard functionality is disabled by default and enabled via the `WG_ENABLED` environment variable. The system uses a Node.js cron wrapper to manipulate the WireGuard `wg` and `wg-quick` CLI tools directly. It leverages Docker volume mapping (`/etc/wireguard`) to maintain state. + +### Backend Map (Node.js) +If you need to edit WireGuard logic, check these files: +- **`backend/lib/wg-helpers.js`**: Shell wrappers for `wg` CLI (create keys, parse CIDR, parse `wg show` dumps, gen configurations). +- **`backend/internal/wireguard.js`**: Core business logic. Manages interface start/stop, adding/removing clients, IP allocation, and token expiration checking via cron. +- **`backend/routes/wireguard.js`**: REST APIs exposing CRUD operations to the frontend. Note: Handlers use ES module export functions syntax. +- **`backend/routes/main.js`**: Mounts the `/api/wireguard` routes. +- **`backend/index.js`**: Contains the startup hook `internalWireguard.startup(knex)` and graceful SIGTERM shutdown hooks. +- **`backend/migrations/20260307000000_wireguard.js`**: Knex schema initialization for tables `wg_interface` and `wg_client`. *Note: Must use ES Module `export function up()` instead of `exports.up`!* + +### Frontend Map (React) +If you need to edit the UI/UX, check these files: +- **`frontend/src/api/backend/wireguard.ts`**: API fetch helper definitions. +- **`frontend/src/hooks/useWireGuard.ts`**: `@tanstack/react-query` data fetchers and mutators. +- **`frontend/src/pages/WireGuard/index.tsx`**: Main UI Page rendering the interface stats and clients table. +- **`frontend/src/modals/WireGuardClientModal.tsx`**: Form to create a new client. *Note: Modal built explicitly over `react-bootstrap/Modal` to prevent backdrop freezing issues.* +- **`frontend/src/modals/WireGuardQRModal.tsx`**: Generates and parses QR codes. +- **`frontend/src/Router.tsx` & `SiteMenu.tsx`**: Routing and UI Navigation injection points for WireGuard UI. + +--- + +## 4. Build & Deployment Gotchas + +### Line Endings (CRLF vs LF) +- **CRITICAL**: All files in `docker/rootfs` and `docker/scripts` are used by `s6-overlay` inside Alpine Linux. **They MUST be formatted using UNIX Line Endings (LF)**. If you download this repository on Windows, ensure the git config does not automatically convert text files to `CRLF`, otherwise container booting will crash with `s6-rc-compile: fatal: invalid type: must be oneshot, longrun, or bundle`. + +### Compilation Steps +- The React Frontend **MUST** be pre-built before Docker can build. +- You must run `yarn install`, `yarn locale-compile`, and `yarn build` inside the `frontend/` directory before `docker build`. +- Use the script `./scripts/build-project.sh` to execute the full pipeline if you have a bash environment. + +### Docker Config Requirements +- **Required capabilities**: `--cap-add=NET_ADMIN` and `--cap-add=SYS_MODULE` are required for WireGuard to manipulate interfaces. +- **Sysctls**: `--sysctl net.ipv4.ip_forward=1` must be applied to the container. +- **Volumes**: Volume `/etc/letsencrypt` is severely required by original NPM core. + +--- + +## 5. Agent Instructions +If you are an AI reading this file: +1. Treat existing NPM-specific code as sacred. Do not modify global `.ts` hooks or Knex config unless instructed. +2. If fixing a bug in the Frontend, use `useWgClients()` / `useInterfaceStatus()` standard hooks. Use React-Bootstrap `Modal` instead of raw div class names. +3. If changing the DB, create a new `backend/migrations/*.js` file in ES Module format. +4. When testing out scripts, remember that the docker container requires port mapping to 51820/udp. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8864d4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..288dfb1 --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# D3V-NPMWG — Nginx Proxy Manager + WireGuard VPN + +A powerful, all-in-one Docker container that combines **Nginx Proxy Manager** (reverse proxy with SSL) and **WireGuard VPN** management in a single, beautiful web interface. + +## ✨ Features + +### Nginx Proxy Manager +- 🌐 Reverse proxy management with a beautiful UI +- 🔒 Free SSL certificates via Let's Encrypt +- 🔀 Proxy hosts, redirection hosts, streams, and 404 hosts +- 🛡️ Access control lists +- 📊 Audit logging + +### WireGuard VPN Manager +- 🔑 Create, enable, disable, and delete VPN clients +- 📱 QR code generation for mobile clients +- 📥 Download `.conf` configuration files +- 📡 Real-time client status (connected, idle, data transfer) +- ⏰ Client expiration support +- 🔄 Auto-sync WireGuard configs + +## 🚀 Quick Start (Auto Install) + +The easiest way to install, update, and manage your D3V-NPMWG instance on Linux is by using our interactive manager script. + +```bash +# Download and run the install script +curl -sSL https://raw.githubusercontent.com/npm-wg/npm-wg/main/install.sh -o install.sh +chmod +x install.sh +sudo ./install.sh +``` + +**Features included in the script:** +- `Install D3V-NPMWG`: Automatically setup docker-compose and directories in `/opt/d3v-npmwg`. +- `Uninstall D3V-NPMWG`: Remove containers and wipe data. +- `Reset Password`: Resets the admin login to `admin@example.com` / `changeme`. +- `Update`: Pulls the latest image and updates the docker-compose stack. + +You can also run specific commands directly: `sudo ./install.sh {install|uninstall|reset|update}` + +--- + +## 🐋 Manual Docker Run```bash +docker run -d \ + --name npm-wg \ + --cap-add=NET_ADMIN \ + --cap-add=SYS_MODULE \ + --sysctl net.ipv4.ip_forward=1 \ + --sysctl net.ipv4.conf.all.src_valid_mark=1 \ + -p 80:80 \ + -p 81:81 \ + -p 443:443 \ + -p 51820:51820/udp \ + -v npm-wg-data:/data \ + -v npm-wg-letsencrypt:/etc/letsencrypt \ + -v npm-wg-wireguard:/etc/wireguard \ + -e WG_HOST=your.server.ip \ + npm-wg:latest +``` + +## 📋 Docker Compose + +```yaml +version: "3.8" +services: + npm-wg: + image: npm-wg:latest + container_name: npm-wg + restart: unless-stopped + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + ports: + - "80:80" # HTTP + - "81:81" # Admin UI + - "443:443" # HTTPS + - "51820:51820/udp" # WireGuard + volumes: + - data:/data + - letsencrypt:/etc/letsencrypt + - wireguard:/etc/wireguard + environment: + WG_HOST: "your.server.ip" # REQUIRED: Your server's public IP or domain + # WG_PORT: 51820 # WireGuard listen port + # WG_DEFAULT_ADDRESS: 10.8.0.0/24 # VPN subnet + # WG_DNS: 1.1.1.1,8.8.8.8 # DNS for VPN clients + # WG_MTU: 1420 # MTU for VPN + # WG_ALLOWED_IPS: 0.0.0.0/0,::/0 # Allowed IPs for clients + # WG_PERSISTENT_KEEPALIVE: 25 + # WG_ENABLED: true # Set to false to disable WireGuard + +volumes: + data: + letsencrypt: + wireguard: +``` + +## 🔧 Environment Variables + +### WireGuard Settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `WG_ENABLED` | `true` | Enable/disable WireGuard VPN | +| `WG_HOST` | *(required)* | Public IP or domain of your server | +| `WG_PORT` | `51820` | WireGuard UDP listen port | +| `WG_DEFAULT_ADDRESS` | `10.8.0.0/24` | VPN subnet CIDR | +| `WG_DNS` | `1.1.1.1, 8.8.8.8` | DNS servers for VPN clients | +| `WG_MTU` | `1420` | MTU value | +| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | Default allowed IPs for clients | +| `WG_PERSISTENT_KEEPALIVE` | `25` | Keepalive interval in seconds | + +## 🌍 Ports + +| Port | Protocol | Description | +|------|----------|-------------| +| `80` | TCP | HTTP | +| `81` | TCP | Admin Web UI | +| `443` | TCP | HTTPS | +| `51820` | UDP | WireGuard VPN | + +## 📖 Usage + +1. **Access the Admin UI** at `http://your-server:81` +2. **Set up NPM** with your admin email and password +3. **Navigate to WireGuard** from the sidebar menu +4. **Create VPN clients** by clicking "New Client" +5. **Scan QR code** or **download .conf** file to configure WireGuard on your devices + +## 🏗️ Building from Source + +To build D3V-NPMWG from source, you must build the React frontend before building the Docker image: + +```bash +# Clone the repository +git clone https://github.com/npm-wg/npm-wg.git +cd npm-wg + +# 1. Build the Frontend +cd frontend +yarn install +yarn build +cd .. + +# 2. Build the Docker Image +# IMPORTANT: Do not forget the trailing dot '.' at the end of the command! +docker build -t npm-wg -f docker/Dockerfile . +``` + +Alternatively, you can run the helper script: +```bash +./scripts/build-project.sh +``` + +## ⚠️ Requirements + +- **Docker** with Linux containers +- **Host kernel** must support WireGuard (Linux 5.6+ or WireGuard kernel module) +- Container requires `NET_ADMIN` and `SYS_MODULE` capabilities +- IP forwarding must be enabled (`net.ipv4.ip_forward=1`) + +## 📜 Credits + +- [Nginx Proxy Manager](https://github.com/NginxProxyManager/nginx-proxy-manager) — Original proxy manager +- [wg-easy](https://github.com/wg-easy/wg-easy) — WireGuard management inspiration + +## 📄 License + +MIT License diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..149080b --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,8 @@ +config/development.json +data/* +yarn-error.log +tmp +certbot.log +node_modules +core.* + diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 0000000..3039bbb --- /dev/null +++ b/backend/app.js @@ -0,0 +1,92 @@ +import bodyParser from "body-parser"; +import compression from "compression"; +import express from "express"; +import fileUpload from "express-fileupload"; +import { isDebugMode } from "./lib/config.js"; +import cors from "./lib/express/cors.js"; +import jwt from "./lib/express/jwt.js"; +import { debug, express as logger } from "./logger.js"; +import mainRoutes from "./routes/main.js"; + +/** + * App + */ +const app = express(); +app.use(fileUpload()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// Gzip +app.use(compression()); + +/** + * General Logging, BEFORE routes + */ + +app.disable("x-powered-by"); +app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]); +app.enable("strict routing"); + +// pretty print JSON when not live +if (isDebugMode()) { + app.set("json spaces", 2); +} + +// CORS for everything +app.use(cors); + +// General security/cache related headers + server header +app.use((_, res, next) => { + let x_frame_options = "DENY"; + + if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) { + x_frame_options = process.env.X_FRAME_OPTIONS; + } + + res.set({ + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": x_frame_options, + "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", + Pragma: "no-cache", + Expires: 0, + }); + next(); +}); + +app.use(jwt()); +app.use("/", mainRoutes); + +// production error handler +// no stacktraces leaked to user +app.use((err, req, res, _) => { + const payload = { + error: { + code: err.status, + message: err.public ? err.message : "Internal Error", + }, + }; + + if (typeof err.message_i18n !== "undefined") { + payload.error.message_i18n = err.message_i18n; + } + + if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) { + payload.debug = { + stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null, + previous: err.previous, + }; + } + + // Not every error is worth logging - but this is good for now until it gets annoying. + if (typeof err.stack !== "undefined" && err.stack) { + debug(logger, err.stack); + if (typeof err.public === "undefined" || !err.public) { + logger.warn(err.message); + } + } + + res.status(err.status || 500).send(payload); +}); + +export default app; diff --git a/backend/biome.json b/backend/biome.json new file mode 100644 index 0000000..ab68104 --- /dev/null +++ b/backend/biome.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "!**/dist/**/*" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 4, + "lineWidth": 120, + "formatWithErrors": true + }, + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "groups": [ + ":BUN:", + ":NODE:", + [ + "npm:*", + "npm:*/**" + ], + ":PACKAGE_WITH_PROTOCOL:", + ":URL:", + ":PACKAGE:", + [ + "/src/*", + "/src/**" + ], + [ + "/**" + ], + [ + "#*", + "#*/**" + ], + ":PATH:" + ] + } + } + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "useUniqueElementIds": "off" + }, + "suspicious": { + "noExplicitAny": "off" + }, + "performance": { + "noDelete": "off" + }, + "nursery": "off", + "a11y": { + "useSemanticElements": "off", + "useValidAnchor": "off" + }, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + } +} diff --git a/backend/certbot/README.md b/backend/certbot/README.md new file mode 100644 index 0000000..3c45646 --- /dev/null +++ b/backend/certbot/README.md @@ -0,0 +1,21 @@ +# Certbot dns-plugins + +This file contains info about available Certbot DNS plugins. +This only works for plugins which use the standard argument structure, so: +--authenticator ---credentials ---propagation-seconds + +File Structure: + +```json +{ + "cloudflare": { + "display_name": "Name displayed to the user", + "package_name": "Package name in PyPi repo", + "version_requirement": "Optional package version requirements (e.g. ==1.3 or >=1.2,<2.0, see https://www.python.org/dev/peps/pep-0440/#version-specifiers)", + "dependencies": "Additional dependencies, space separated (as you would pass it to pip install)", + "credentials": "Template of the credentials file", + "full_plugin_name": "The full plugin name as used in the commandline with certbot, e.g. 'dns-njalla'" + }, + ... +} +``` diff --git a/backend/certbot/dns-plugins.json b/backend/certbot/dns-plugins.json new file mode 100644 index 0000000..8f592f6 --- /dev/null +++ b/backend/certbot/dns-plugins.json @@ -0,0 +1,658 @@ +{ + "acmedns": { + "name": "ACME-DNS", + "package_name": "certbot-dns-acmedns", + "version": "~=0.1.0", + "dependencies": "", + "credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json", + "full_plugin_name": "dns-acmedns" + }, + "active24": { + "name": "Active24", + "package_name": "certbot-dns-active24", + "version": "~=2.0.0", + "dependencies": "", + "credentials": "dns_active24_api_key = \ndns_active24_secret = ", + "full_plugin_name": "dns-active24" + }, + "aliyun": { + "name": "Aliyun", + "package_name": "certbot-dns-aliyun", + "version": "~=2.0.0", + "dependencies": "", + "credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef", + "full_plugin_name": "dns-aliyun" + }, + "arvan": { + "name": "ArvanCloud", + "package_name": "certbot-dns-arvan", + "version": ">=0.1.0", + "dependencies": "", + "credentials": "dns_arvan_key = Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "full_plugin_name": "dns-arvan" + }, + "azure": { + "name": "Azure", + "package_name": "certbot-dns-azure", + "version": "~=2.6.1", + "dependencies": "azure-mgmt-dns==8.2.0", + "credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2", + "full_plugin_name": "dns-azure" + }, + "baidu": { + "name": "baidu", + "package_name": "certbot-dns-baidu", + "version": "~=0.1.1", + "dependencies": "", + "credentials": "dns_baidu_access_key = 12345678\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef", + "full_plugin_name": "dns-baidu" + }, + "beget": { + "name":"Beget", + "package_name": "certbot-beget-plugin", + "version": "~=1.0.0.dev9", + "dependencies": "", + "credentials": "# Beget API credentials used by Certbot\nbeget_plugin_username = username\nbeget_plugin_password = password", + "full_plugin_name": "beget-plugin" + }, + "bunny": { + "name": "bunny.net", + "package_name": "certbot-dns-bunny", + "version": "~=0.0.9", + "dependencies": "", + "credentials": "# Bunny API token used by Certbot (see https://dash.bunny.net/account/settings)\ndns_bunny_api_key = xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx", + "full_plugin_name": "dns-bunny" + }, + "cdmon": { + "name": "cdmon", + "package_name": "certbot-dns-cdmon", + "version": "~=0.4.1", + "dependencies": "", + "credentials": "dns_cdmon_api_key=your-cdmon-api-token\ndns_cdmon_domain=your_domain_is_optional", + "full_plugin_name": "dns-cdmon" + }, + "cloudflare": { + "name": "Cloudflare", + "package_name": "certbot-dns-cloudflare", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567", + "full_plugin_name": "dns-cloudflare" + }, + "cloudns": { + "name": "ClouDNS", + "package_name": "certbot-dns-cloudns", + "version": "~=0.7.0", + "dependencies": "", + "credentials": "# Target user ID (see https://www.cloudns.net/api-settings/)\n\tdns_cloudns_auth_id=1234\n\t# Alternatively, one of the following two options can be set:\n\t# dns_cloudns_sub_auth_id=1234\n\t# dns_cloudns_sub_auth_user=foobar\n\n\t# API password\n\tdns_cloudns_auth_password=password1", + "full_plugin_name": "dns-cloudns" + }, + "cloudxns": { + "name": "CloudXNS", + "package_name": "certbot-dns-cloudxns", + "version": "~=1.32.0", + "dependencies": "", + "credentials": "dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef\ndns_cloudxns_secret_key = 1122334455667788", + "full_plugin_name": "dns-cloudxns" + }, + "constellix": { + "name": "Constellix", + "package_name": "certbot-dns-constellix", + "version": "~=0.2.1", + "dependencies": "", + "credentials": "dns_constellix_apikey = 5fb4e76f-ac91-43e5-f982458bc595\ndns_constellix_secretkey = 47d99fd0-32e7-4e07-85b46d08e70b\ndns_constellix_endpoint = https://api.dns.constellix.com/v1", + "full_plugin_name": "dns-constellix" + }, + "corenetworks": { + "name": "Core Networks", + "package_name": "certbot-dns-corenetworks", + "version": "~=0.1.4", + "dependencies": "", + "credentials": "dns_corenetworks_username = asaHB12r\ndns_corenetworks_password = secure_password", + "full_plugin_name": "dns-corenetworks" + }, + "cpanel": { + "name": "cPanel", + "package_name": "certbot-dns-cpanel", + "version": "~=0.4.0", + "dependencies": "", + "credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = your_username\ncpanel_password = your_password\ncpanel_token = your_api_token", + "full_plugin_name": "cpanel" + }, + "ddnss": { + "name": "DDNSS", + "package_name": "certbot-dns-ddnss", + "version": "~=1.1.0", + "dependencies": "", + "credentials": "dns_ddnss_token = YOUR_DDNSS_API_TOKEN", + "full_plugin_name": "dns-ddnss" + }, + "desec": { + "name": "deSEC", + "package_name": "certbot-dns-desec", + "version": "~=1.2.1", + "dependencies": "", + "credentials": "dns_desec_token = YOUR_DESEC_API_TOKEN\ndns_desec_endpoint = https://desec.io/api/v1/", + "full_plugin_name": "dns-desec" + }, + "duckdns": { + "name": "DuckDNS", + "package_name": "certbot-dns-duckdns", + "version": "~=1.0", + "dependencies": "", + "credentials": "dns_duckdns_token=your-duckdns-token", + "full_plugin_name": "dns-duckdns" + }, + "digitalocean": { + "name": "DigitalOcean", + "package_name": "certbot-dns-digitalocean", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff", + "full_plugin_name": "dns-digitalocean" + }, + "directadmin": { + "name": "DirectAdmin", + "package_name": "certbot-dns-directadmin", + "version": "~=0.0.23", + "dependencies": "", + "credentials": "directadmin_url = https://my.directadminserver.com:2222\ndirectadmin_username = username\ndirectadmin_password = aSuperStrongPassword", + "full_plugin_name": "directadmin" + }, + "dnsimple": { + "name": "DNSimple", + "package_name": "certbot-dns-dnsimple", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "full_plugin_name": "dns-dnsimple" + }, + "dnsmadeeasy": { + "name": "DNS Made Easy", + "package_name": "certbot-dns-dnsmadeeasy", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a\ndns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55", + "full_plugin_name": "dns-dnsmadeeasy" + }, + "dnsmulti": { + "name": "DnsMulti", + "package_name": "certbot-dns-multi", + "version": "~=4.9", + "dependencies": "", + "credentials": "# See https://go-acme.github.io/lego/dns/#dns-providers for list of providers and their settings\n# Example provider configuration for DreamHost\n# dns_multi_provider = dreamhost\n# DREAMHOST_API_KEY = ABCDEFG1234", + "full_plugin_name": "dns-multi" + }, + "dnspod": { + "name": "DNSPod", + "package_name": "certbot-dns-dnspod", + "version": "~=0.1.0", + "dependencies": "", + "credentials": "dns_dnspod_email = \"email@example.com\"\ndns_dnspod_api_token = \"id,key\"", + "full_plugin_name": "dns-dnspod" + }, + "domainoffensive": { + "name": "DomainOffensive (do.de)", + "package_name": "certbot-dns-domainoffensive", + "version": "~=2.0.0", + "dependencies": "", + "credentials": "dns_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN", + "full_plugin_name": "dns-domainoffensive" + }, + "domeneshop": { + "name": "Domeneshop", + "package_name": "certbot-dns-domeneshop", + "version": "~=0.2.8", + "dependencies": "", + "credentials": "dns_domeneshop_client_token=YOUR_DOMENESHOP_CLIENT_TOKEN\ndns_domeneshop_client_secret=YOUR_DOMENESHOP_CLIENT_SECRET", + "full_plugin_name": "dns-domeneshop" + }, + "dynu": { + "name": "Dynu", + "package_name": "certbot-dns-dynu", + "version": "~=0.0.1", + "dependencies": "", + "credentials": "dns_dynu_auth_token = YOUR_DYNU_AUTH_TOKEN", + "full_plugin_name": "dns-dynu" + }, + "easydns": { + "name": "easyDNS", + "package_name": "certbot-dns-easydns", + "version": "~=0.1.2", + "dependencies": "", + "credentials": "dns_easydns_usertoken = YOUR_EASYDNS_USERTOKEN\ndns_easydns_userkey = YOUR_EASYDNS_USERKEY\ndns_easydns_endpoint = https://rest.easydns.net", + "full_plugin_name": "dns-easydns" + }, + "eurodns": { + "name": "EuroDNS", + "package_name": "certbot-dns-eurodns", + "version": "~=0.0.4", + "dependencies": "", + "credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy", + "full_plugin_name": "dns-eurodns" + }, + "firstdomains": { + "name": "First Domains", + "package_name": "certbot-dns-firstdomains", + "version": ">=1.0", + "dependencies": "", + "credentials": "dns_firstdomains_username = myremoteuser\ndns_firstdomains_password = verysecureremoteuserpassword", + "full_plugin_name": "dns-firstdomains" + }, + "freedns": { + "name": "FreeDNS", + "package_name": "certbot-dns-freedns", + "version": "~=0.1.0", + "dependencies": "", + "credentials": "dns_freedns_username = myremoteuser\ndns_freedns_password = verysecureremoteuserpassword", + "full_plugin_name": "dns-freedns" + }, + "gandi": { + "name": "Gandi Live DNS", + "package_name": "certbot-dns-gandi", + "version": "~=1.6.1", + "dependencies": "", + "credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN", + "full_plugin_name": "dns-gandi" + }, + "gcore": { + "name": "Gcore DNS", + "package_name": "certbot-dns-gcore", + "version": "~=0.1.8", + "dependencies": "", + "credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567", + "full_plugin_name": "dns-gcore" + }, + "glesys": { + "name": "Glesys", + "package_name": "certbot-dns-glesys", + "version": "~=2.1.0", + "dependencies": "", + "credentials": "dns_glesys_user = CL00000\ndns_glesys_password = apikeyvalue", + "full_plugin_name": "dns-glesys" + }, + "godaddy": { + "name": "GoDaddy", + "package_name": "certbot-dns-godaddy", + "version": "==2.8.0", + "dependencies": "", + "credentials": "dns_godaddy_secret = 0123456789abcdef0123456789abcdef01234567\ndns_godaddy_key = abcdef0123456789abcdef01234567abcdef0123", + "full_plugin_name": "dns-godaddy" + }, + "google": { + "name": "Google", + "package_name": "certbot-dns-google", + "version": "=={{certbot-version}}", + "dependencies": "", + "credentials": "{\n\"type\": \"service_account\",\n...\n}", + "full_plugin_name": "dns-google" + }, + "googledomains": { + "name": "GoogleDomainsDNS", + "package_name": "certbot-dns-google-domains", + "version": "~=0.1.5", + "dependencies": "", + "credentials": "dns_google_domains_access_token = 0123456789abcdef0123456789abcdef01234567\ndns_google_domains_zone = \"example.com\"", + "full_plugin_name": "dns-google-domains" + }, + "he": { + "name": "Hurricane Electric", + "package_name": "certbot-dns-he", + "version": "~=1.0.0", + "dependencies": "", + "credentials": "dns_he_user = Me\ndns_he_pass = my HE password", + "full_plugin_name": "dns-he" + }, + "he-ddns": { + "name": "Hurricane Electric - DDNS", + "package_name": "certbot-dns-he-ddns", + "version": "~=0.1.0", + "dependencies": "", + "credentials": "dns_he_ddns_password = verysecurepassword", + "full_plugin_name": "dns-he-ddns" + }, + "hetzner": { + "name": "Hetzner", + "package_name": "certbot-dns-hetzner", + "version": "~=1.0.4", + "dependencies": "", + "credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef", + "full_plugin_name": "dns-hetzner" + }, + "hetzner-cloud": { + "name": "Hetzner Cloud", + "package_name": "certbot-dns-hetzner-cloud", + "version": "~=1.0.4", + "dependencies": "", + "credentials": "dns_hetzner_cloud_api_token = your_api_token_here", + "full_plugin_name": "dns-hetzner-cloud" + }, + "hostingnl": { + "name": "Hosting.nl", + "package_name": "certbot-dns-hostingnl", + "version": "~=0.1.5", + "dependencies": "", + "credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef", + "full_plugin_name": "dns-hostingnl" + }, + "hover": { + "name": "Hover", + "package_name": "certbot-dns-hover", + "version": "~=1.2.1", + "dependencies": "", + "credentials": "dns_hover_hoverurl = https://www.hover.com\ndns_hover_username = hover-admin-username\ndns_hover_password = hover-admin-password\ndns_hover_totpsecret = 2fa-totp-secret", + "full_plugin_name": "dns-hover" + }, + "infomaniak": { + "name": "Infomaniak", + "package_name": "certbot-dns-infomaniak", + "version": "~=0.2.2", + "dependencies": "", + "credentials": "dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "full_plugin_name": "dns-infomaniak" + }, + "inwx": { + "name": "INWX", + "package_name": "certbot-dns-inwx", + "version": "~=2.1.2", + "dependencies": "", + "credentials": "dns_inwx_url = https://api.domrobot.com/xmlrpc/\ndns_inwx_username = your_username\ndns_inwx_password = your_password\ndns_inwx_shared_secret = your_shared_secret optional", + "full_plugin_name": "dns-inwx" + }, + "ionos": { + "name": "IONOS", + "package_name": "certbot-dns-ionos", + "version": "==2022.11.24", + "dependencies": "", + "credentials": "dns_ionos_prefix = myapikeyprefix\ndns_ionos_secret = verysecureapikeysecret\ndns_ionos_endpoint = https://api.hosting.ionos.com", + "full_plugin_name": "dns-ionos" + }, + "ispconfig": { + "name": "ISPConfig", + "package_name": "certbot-dns-ispconfig", + "version": "~=0.2.0", + "dependencies": "", + "credentials": "dns_ispconfig_username = myremoteuser\ndns_ispconfig_password = verysecureremoteuserpassword\ndns_ispconfig_endpoint = https://localhost:8080", + "full_plugin_name": "dns-ispconfig" + }, + "isset": { + "name": "Isset", + "package_name": "certbot-dns-isset", + "version": "~=0.0.3", + "dependencies": "", + "credentials": "dns_isset_endpoint=\"https://customer.isset.net/api\"\ndns_isset_token=\"\"", + "full_plugin_name": "dns-isset" + }, + "joker": { + "name": "Joker", + "package_name": "certbot-dns-joker", + "version": "~=1.1.0", + "dependencies": "", + "credentials": "dns_joker_username = \ndns_joker_password = \ndns_joker_domain = ", + "full_plugin_name": "dns-joker" + }, + "kas": { + "name": "All-Inkl", + "package_name": "certbot-dns-kas", + "version": "~=0.1.1", + "dependencies": "kasserver", + "credentials": "dns_kas_user = your_kas_user\ndns_kas_password = your_kas_password", + "full_plugin_name": "dns-kas" + }, + "leaseweb": { + "name": "LeaseWeb", + "package_name": "certbot-dns-leaseweb", + "version": "~=1.0.3", + "dependencies": "", + "credentials": "dns_leaseweb_api_token = 01234556789", + "full_plugin_name": "dns-leaseweb" + }, + "linode": { + "name": "Linode", + "package_name": "certbot-dns-linode", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64\ndns_linode_version = [|3|4]", + "full_plugin_name": "dns-linode" + }, + "loopia": { + "name": "Loopia", + "package_name": "certbot-dns-loopia", + "version": "~=1.0.0", + "dependencies": "", + "credentials": "dns_loopia_user = user@loopiaapi\ndns_loopia_password = abcdef0123456789abcdef01234567abcdef0123", + "full_plugin_name": "dns-loopia" + }, + "luadns": { + "name": "LuaDNS", + "package_name": "certbot-dns-luadns", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef", + "full_plugin_name": "dns-luadns" + }, + "mchost24": { + "name": "MC-HOST24", + "package_name": "certbot-dns-mchost24", + "version": "", + "dependencies": "", + "credentials": "# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\ndns_mchost24_api_token=", + "full_plugin_name": "dns-mchost24" + }, + "mijnhost": { + "name": "mijn.host", + "package_name": "certbot-dns-mijn-host", + "version": "~=0.0.4", + "dependencies": "", + "credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef", + "full_plugin_name": "dns-mijn-host" + }, + "namecheap": { + "name": "Namecheap", + "package_name": "certbot-dns-namecheap", + "version": "~=1.0.0", + "dependencies": "", + "credentials": "dns_namecheap_username = 123456\ndns_namecheap_api_key = 0123456789abcdef0123456789abcdef01234567", + "full_plugin_name": "dns-namecheap" + }, + "netcup": { + "name": "netcup", + "package_name": "certbot-dns-netcup", + "version": "~=1.0.0", + "dependencies": "", + "credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123", + "full_plugin_name": "dns-netcup" + }, + "nicru": { + "name": "nic.ru", + "package_name": "certbot-dns-nicru", + "version": "~=1.0.3", + "dependencies": "", + "credentials": "dns_nicru_client_id = application-id\ndns_nicru_client_secret = application-token\ndns_nicru_username = 0001110/NIC-D\ndns_nicru_password = password\ndns_nicru_scope = .+:.+/zones/example.com(/.+)?\ndns_nicru_service = DNS_SERVICE_NAME\ndns_nicru_zone = example.com", + "full_plugin_name": "dns-nicru" + }, + "njalla": { + "name": "Njalla", + "package_name": "certbot-dns-njalla", + "version": "~=1.0.0", + "dependencies": "", + "credentials": "dns_njalla_token = 0123456789abcdef0123456789abcdef01234567", + "full_plugin_name": "dns-njalla" + }, + "nsone": { + "name": "NS1", + "package_name": "certbot-dns-nsone", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw", + "full_plugin_name": "dns-nsone" + }, + "oci": { + "name": "Oracle Cloud Infrastructure DNS", + "package_name": "certbot-dns-oci", + "version": "~=0.3.6", + "dependencies": "oci", + "credentials": "[DEFAULT]\nuser = ocid1.user.oc1...\nfingerprint = xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx\ntenancy = ocid1.tenancy.oc1...\nregion = us-ashburn-1\nkey_file = ~/.oci/oci_api_key.pem", + "full_plugin_name": "dns-oci" + }, + "ovh": { + "name": "OVH", + "package_name": "certbot-dns-ovh", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "dns_ovh_endpoint = ovh-eu\ndns_ovh_application_key = MDAwMDAwMDAwMDAw\ndns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\ndns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw", + "full_plugin_name": "dns-ovh" + }, + "plesk": { + "name": "Plesk", + "package_name": "certbot-dns-plesk", + "version": "~=0.3.0", + "dependencies": "", + "credentials": "dns_plesk_username = your-username\ndns_plesk_password = secret\ndns_plesk_api_url = https://plesk-api-host:8443", + "full_plugin_name": "dns-plesk" + }, + "porkbun": { + "name": "Porkbun", + "package_name": "certbot-dns-porkbun", + "version": "~=0.11.0", + "dependencies": "", + "credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret", + "full_plugin_name": "dns-porkbun" + }, + "powerdns": { + "name": "PowerDNS", + "package_name": "certbot-dns-powerdns", + "version": "~=0.2.1", + "dependencies": "PyYAML==5.3.1", + "credentials": "dns_powerdns_api_url = https://api.mypowerdns.example.org\ndns_powerdns_api_key = AbCbASsd!@34", + "full_plugin_name": "dns-powerdns" + }, + "regru": { + "name": "reg.ru", + "package_name": "certbot-regru", + "version": "~=1.0.2", + "dependencies": "", + "credentials": "dns_username=username\ndns_password=password", + "full_plugin_name": "dns" + }, + "rfc2136": { + "name": "RFC 2136", + "package_name": "certbot-dns-rfc2136", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "# Target DNS server\ndns_rfc2136_server = 192.0.2.1\n# Target DNS port\ndns_rfc2136_port = 53\n# TSIG key name\ndns_rfc2136_name = keyname.\n# TSIG key secret\ndns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg==\n# TSIG key algorithm\ndns_rfc2136_algorithm = HMAC-SHA512", + "full_plugin_name": "dns-rfc2136" + }, + "rockenstein": { + "name": "rockenstein AG", + "package_name": "certbot-dns-rockenstein", + "version": "~=1.0.0", + "dependencies": "", + "credentials": "dns_rockenstein_token=", + "full_plugin_name": "dns-rockenstein" + }, + "route53": { + "name": "Route 53 (Amazon)", + "package_name": "certbot-dns-route53", + "version": "=={{certbot-version}}", + "dependencies": "acme=={{certbot-version}}", + "credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "full_plugin_name": "dns-route53" + }, + "simply": { + "name": "Simply", + "package_name": "certbot-dns-simply", + "version": "~=0.1.2", + "dependencies": "", + "credentials": "dns_simply_account_name = UExxxxxx\ndns_simply_api_key = DsHJdsjh2812872sahj", + "full_plugin_name": "dns-simply" + }, + "spaceship": { + "name": "Spaceship", + "package_name": "certbot-dns-spaceship", + "version": "~=1.0.4", + "dependencies": "", + "credentials": "[spaceship]\napi_key=your_api_key\napi_secret=your_api_secret", + "full_plugin_name": "dns-spaceship" + }, + "strato": { + "name": "Strato", + "package_name": "certbot-dns-strato", + "version": "~=0.2.2", + "dependencies": "", + "credentials": "dns_strato_username = user\ndns_strato_password = pass\n# uncomment if youre using two factor authentication:\n# dns_strato_totp_devicename = 2fa_device\n# dns_strato_totp_secret = 2fa_secret\n#\n# uncomment if domain name contains special characters\n# insert domain display name as seen on your account page here\n# dns_strato_domain_display_name = my-punicode-url.de\n#\n# if youre not using strato.de or another special endpoint you can customise it below\n# you will probably only need to adjust the host, but you can also change the complete endpoint url\n# dns_strato_custom_api_scheme = https\n# dns_strato_custom_api_host = www.strato.de\n# dns_strato_custom_api_port = 443\n# dns_strato_custom_api_path = \"/apps/CustomerService\"", + "full_plugin_name": "dns-strato" + }, + "selectelv2": { + "name": "Selectel api v2", + "package_name": "certbot-dns-selectel-api-v2", + "version": "~=0.3.0", + "dependencies": "", + "credentials": "dns_selectel_api_v2_account_id = your_account_id\ndns_selectel_api_v2_project_name = your_project\ndns_selectel_api_v2_username = your_username\ndns_selectel_api_v2_password = your_password", + "full_plugin_name": "dns-selectel-api-v2" + }, + "timeweb": { + "name": "Timeweb Cloud", + "package_name": "certbot-dns-timeweb", + "version": "~=1.0.1", + "dependencies": "", + "credentials": "dns_timeweb_api_key = XXXXXXXXXXXXXXXXXXX", + "full_plugin_name": "dns-timeweb" + }, + "transip": { + "name": "TransIP", + "package_name": "certbot-dns-transip", + "version": "~=0.5.2", + "dependencies": "", + "credentials": "dns_transip_username = my_username\ndns_transip_key_file = /etc/letsencrypt/transip-rsa.key", + "full_plugin_name": "dns-transip" + }, + "tencentcloud": { + "name": "Tencent Cloud", + "package_name": "certbot-dns-tencentcloud", + "version": "~=2.0.2", + "dependencies": "", + "credentials": "dns_tencentcloud_secret_id = TENCENT_CLOUD_SECRET_ID\ndns_tencentcloud_secret_key = TENCENT_CLOUD_SECRET_KEY", + "full_plugin_name": "dns-tencentcloud" + }, + "vultr": { + "name": "Vultr", + "package_name": "certbot-dns-vultr", + "version": "~=1.1.0", + "dependencies": "", + "credentials": "dns_vultr_key = YOUR_VULTR_API_KEY", + "full_plugin_name": "dns-vultr" + }, + "websupport": { + "name": "Websupport.sk", + "package_name": "certbot-dns-websupport", + "version": "~=2.0.1", + "dependencies": "", + "credentials": "dns_websupport_identifier = \ndns_websupport_secret_key = ", + "full_plugin_name": "dns-websupport" + }, + "wedos": { + "name": "Wedos", + "package_name": "certbot-dns-wedos", + "version": "~=2.2", + "dependencies": "", + "credentials": "dns_wedos_user = \ndns_wedos_auth = ", + "full_plugin_name": "dns-wedos" + }, + "edgedns": { + "name": "Akamai Edge DNS", + "package_name": "certbot-plugin-edgedns", + "version": "~=0.1.0", + "dependencies": "", + "credentials": "edgedns_client_secret = as3d1asd5d1a32sdfsdfs2d1asd5=\nedgedns_host = sdflskjdf-dfsdfsdf-sdfsdfsdf.luna.akamaiapis.net\nedgedns_access_token = kjdsi3-34rfsdfsdf-234234fsdfsdf\nedgedns_client_token = dkfjdf-342fsdfsd-23fsdfsdfsdf", + "full_plugin_name": "edgedns" + }, + "zoneedit": { + "name": "ZoneEdit", + "package_name": "certbot-dns-zoneedit", + "version": "~=0.3.2", + "dependencies": "--no-deps dnspython", + "credentials": "dns_zoneedit_user = \ndns_zoneedit_token = ", + "full_plugin_name": "dns-zoneedit" + } +} diff --git a/backend/config/README.md b/backend/config/README.md new file mode 100644 index 0000000..26268a1 --- /dev/null +++ b/backend/config/README.md @@ -0,0 +1,2 @@ +These files are use in development and are not deployed as part of the final product. + \ No newline at end of file diff --git a/backend/config/default.json b/backend/config/default.json new file mode 100644 index 0000000..154e66e --- /dev/null +++ b/backend/config/default.json @@ -0,0 +1,10 @@ +{ + "database": { + "engine": "mysql2", + "host": "db", + "name": "npm", + "user": "npm", + "password": "npm", + "port": 3306 + } +} diff --git a/backend/config/sqlite-test-db.json b/backend/config/sqlite-test-db.json new file mode 100644 index 0000000..b0e8707 --- /dev/null +++ b/backend/config/sqlite-test-db.json @@ -0,0 +1,26 @@ +{ + "database": { + "engine": "knex-native", + "knex": { + "client": "better-sqlite3", + "connection": { + "filename": "/app/config/mydb.sqlite" + }, + "pool": { + "min": 0, + "max": 1, + "createTimeoutMillis": 3000, + "acquireTimeoutMillis": 30000, + "idleTimeoutMillis": 30000, + "reapIntervalMillis": 1000, + "createRetryIntervalMillis": 100, + "propagateCreateError": false + }, + "migrations": { + "tableName": "migrations", + "stub": "src/backend/lib/migrate_template.js", + "directory": "src/backend/migrations" + } + } + } +} diff --git a/backend/db.js b/backend/db.js new file mode 100644 index 0000000..bf540f8 --- /dev/null +++ b/backend/db.js @@ -0,0 +1,42 @@ +import knex from "knex"; +import {configGet, configHas} from "./lib/config.js"; + +let instance = null; + +const generateDbConfig = () => { + if (!configHas("database")) { + throw new Error( + "Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/", + ); + } + + const cfg = configGet("database"); + + if (cfg.engine === "knex-native") { + return cfg.knex; + } + + return { + client: cfg.engine, + connection: { + host: cfg.host, + user: cfg.user, + password: cfg.password, + database: cfg.name, + port: cfg.port, + ...(cfg.ssl ? { ssl: cfg.ssl } : {}) + }, + migrations: { + tableName: "migrations", + }, + }; +}; + +const getInstance = () => { + if (!instance) { + instance = knex(generateDbConfig()); + } + return instance; +} + +export default getInstance; diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..b8b1de5 --- /dev/null +++ b/backend/index.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +import app from "./app.js"; +import db from "./db.js"; +import internalCertificate from "./internal/certificate.js"; +import internalIpRanges from "./internal/ip_ranges.js"; +import internalWireguard from "./internal/wireguard.js"; +import { global as logger } from "./logger.js"; +import { migrateUp } from "./migrate.js"; +import { getCompiledSchema } from "./schema/index.js"; +import setup from "./setup.js"; + +const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false"; +const WG_ENABLED = process.env.WG_ENABLED !== "false"; + +async function appStart() { + return migrateUp() + .then(setup) + .then(getCompiledSchema) + .then(() => { + if (!IP_RANGES_FETCH_ENABLED) { + logger.info("IP Ranges fetch is disabled by environment variable"); + return; + } + logger.info("IP Ranges fetch is enabled"); + return internalIpRanges.fetch().catch((err) => { + logger.error("IP Ranges fetch failed, continuing anyway:", err.message); + }); + }) + .then(async () => { + internalCertificate.initTimer(); + internalIpRanges.initTimer(); + + // Start WireGuard + if (WG_ENABLED) { + logger.info("WireGuard is enabled, starting..."); + try { + const knex = db(); + await internalWireguard.startup(knex); + logger.info("WireGuard started successfully"); + } catch (err) { + logger.error("WireGuard startup failed:", err.message); + logger.warn("NPM will continue without WireGuard functionality"); + } + } else { + logger.info("WireGuard is disabled by environment variable"); + } + + const server = app.listen(3000, () => { + logger.info(`Backend PID ${process.pid} listening on port 3000 ...`); + + process.on("SIGTERM", async () => { + logger.info(`PID ${process.pid} received SIGTERM`); + + // Shutdown WireGuard gracefully + if (WG_ENABLED) { + try { + const knex = db(); + await internalWireguard.shutdown(knex); + } catch (err) { + logger.warn("WireGuard shutdown warning:", err.message); + } + } + + server.close(() => { + logger.info("Stopping."); + process.exit(0); + }); + }); + }); + }) + .catch((err) => { + logger.error(`Startup Error: ${err.message}`, err); + setTimeout(appStart, 1000); + }); +} + +try { + appStart(); +} catch (err) { + logger.fatal(err); + process.exit(1); +} diff --git a/backend/internal/2fa.js b/backend/internal/2fa.js new file mode 100644 index 0000000..43307e0 --- /dev/null +++ b/backend/internal/2fa.js @@ -0,0 +1,305 @@ +import crypto from "node:crypto"; +import bcrypt from "bcrypt"; +import { createGuardrails, generateSecret, generateURI, verify } from "otplib"; +import errs from "../lib/error.js"; +import authModel from "../models/auth.js"; +import internalUser from "./user.js"; + +const APP_NAME = "Nginx Proxy Manager"; +const BACKUP_CODE_COUNT = 8; + +/** + * Generate backup codes + * @returns {Promise<{plain: string[], hashed: string[]}>} + */ +const generateBackupCodes = async () => { + const plain = []; + const hashed = []; + + for (let i = 0; i < BACKUP_CODE_COUNT; i++) { + const code = crypto.randomBytes(4).toString("hex").toUpperCase(); + plain.push(code); + const hash = await bcrypt.hash(code, 10); + hashed.push(hash); + } + + return { plain, hashed }; +}; + +const internal2fa = { + /** + * Check if user has 2FA enabled + * @param {number} userId + * @returns {Promise} + */ + isEnabled: async (userId) => { + const auth = await internal2fa.getUserPasswordAuth(userId); + return auth?.meta?.totp_enabled === true; + }, + + /** + * Get 2FA status for user + * @param {Access} access + * @param {number} userId + * @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>} + */ + getStatus: async (access, userId) => { + await access.can("users:password", userId); + await internalUser.get(access, { id: userId }); + const auth = await internal2fa.getUserPasswordAuth(userId); + const enabled = auth?.meta?.totp_enabled === true; + let backup_codes_remaining = 0; + + if (enabled) { + const backupCodes = auth.meta.backup_codes || []; + backup_codes_remaining = backupCodes.length; + } + + return { + enabled, + backup_codes_remaining, + }; + }, + + /** + * Start 2FA setup - store pending secret + * + * @param {Access} access + * @param {number} userId + * @returns {Promise<{secret: string, otpauth_url: string}>} + */ + startSetup: async (access, userId) => { + await access.can("users:password", userId); + const user = await internalUser.get(access, { id: userId }); + const secret = generateSecret(); + const otpauth_url = generateURI({ + issuer: APP_NAME, + label: user.email, + secret: secret, + }); + const auth = await internal2fa.getUserPasswordAuth(userId); + + // ensure user isn't already setup for 2fa + const enabled = auth?.meta?.totp_enabled === true; + if (enabled) { + throw new errs.ValidationError("2FA is already enabled"); + } + + const meta = auth.meta || {}; + meta.totp_pending_secret = secret; + + await authModel + .query() + .where("id", auth.id) + .andWhere("user_id", userId) + .andWhere("type", "password") + .patch({ meta }); + + return { secret, otpauth_url }; + }, + + /** + * Enable 2FA after verifying code + * + * @param {Access} access + * @param {number} userId + * @param {string} code + * @returns {Promise<{backup_codes: string[]}>} + */ + enable: async (access, userId, code) => { + await access.can("users:password", userId); + await internalUser.get(access, { id: userId }); + const auth = await internal2fa.getUserPasswordAuth(userId); + const secret = auth?.meta?.totp_pending_secret || false; + + if (!secret) { + throw new errs.ValidationError("No pending 2FA setup found"); + } + + const result = await verify({ token: code, secret }); + if (!result.valid) { + throw new errs.ValidationError("Invalid verification code"); + } + + const { plain, hashed } = await generateBackupCodes(); + + const meta = { + ...auth.meta, + totp_secret: secret, + totp_enabled: true, + totp_enabled_at: new Date().toISOString(), + backup_codes: hashed, + }; + delete meta.totp_pending_secret; + + await authModel + .query() + .where("id", auth.id) + .andWhere("user_id", userId) + .andWhere("type", "password") + .patch({ meta }); + + return { backup_codes: plain }; + }, + + /** + * Disable 2FA + * + * @param {Access} access + * @param {number} userId + * @param {string} code + * @returns {Promise} + */ + disable: async (access, userId, code) => { + await access.can("users:password", userId); + await internalUser.get(access, { id: userId }); + const auth = await internal2fa.getUserPasswordAuth(userId); + + const enabled = auth?.meta?.totp_enabled === true; + if (!enabled) { + throw new errs.ValidationError("2FA is not enabled"); + } + + const result = await verify({ + token: code, + secret: auth.meta.totp_secret, + guardrails: createGuardrails({ + MIN_SECRET_BYTES: 10, + }), + }); + + if (!result.valid) { + throw new errs.AuthError("Invalid verification code"); + } + + const meta = { ...auth.meta }; + delete meta.totp_secret; + delete meta.totp_enabled; + delete meta.totp_enabled_at; + delete meta.backup_codes; + + await authModel + .query() + .where("id", auth.id) + .andWhere("user_id", userId) + .andWhere("type", "password") + .patch({ meta }); + }, + + /** + * Verify 2FA code for login + * + * @param {number} userId + * @param {string} token + * @returns {Promise} + */ + verifyForLogin: async (userId, token) => { + const auth = await internal2fa.getUserPasswordAuth(userId); + const secret = auth?.meta?.totp_secret || false; + + if (!secret) { + return false; + } + + // Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars + // and the backup codes are 8 chars. + if (token.length === 6) { + const result = await verify({ + token, + secret, + // These guardrails lower the minimum length requirement for secrets. + // In v12 of otplib the default minimum length is 10 and in v13 it is 16. + // Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets + // so people won't be locked out when upgrading. + guardrails: createGuardrails({ + MIN_SECRET_BYTES: 10, + }), + }); + + if (result.valid) { + return true; + } + } + + // Try backup codes + const backupCodes = auth?.meta?.backup_codes || []; + for (let i = 0; i < backupCodes.length; i++) { + const match = await bcrypt.compare(token.toUpperCase(), backupCodes[i]); + if (match) { + // Remove used backup code + const updatedCodes = [...backupCodes]; + updatedCodes.splice(i, 1); + const meta = { ...auth.meta, backup_codes: updatedCodes }; + await authModel + .query() + .where("id", auth.id) + .andWhere("user_id", userId) + .andWhere("type", "password") + .patch({ meta }); + return true; + } + } + + return false; + }, + + /** + * Regenerate backup codes + * + * @param {Access} access + * @param {number} userId + * @param {string} token + * @returns {Promise<{backup_codes: string[]}>} + */ + regenerateBackupCodes: async (access, userId, token) => { + await access.can("users:password", userId); + await internalUser.get(access, { id: userId }); + const auth = await internal2fa.getUserPasswordAuth(userId); + const enabled = auth?.meta?.totp_enabled === true; + const secret = auth?.meta?.totp_secret || false; + + if (!enabled) { + throw new errs.ValidationError("2FA is not enabled"); + } + if (!secret) { + throw new errs.ValidationError("No 2FA secret found"); + } + + const result = await verify({ + token, + secret, + }); + + if (!result.valid) { + throw new errs.ValidationError("Invalid verification code"); + } + + const { plain, hashed } = await generateBackupCodes(); + + const meta = { ...auth.meta, backup_codes: hashed }; + await authModel + .query() + .where("id", auth.id) + .andWhere("user_id", userId) + .andWhere("type", "password") + .patch({ meta }); + + return { backup_codes: plain }; + }, + + getUserPasswordAuth: async (userId) => { + const auth = await authModel + .query() + .where("user_id", userId) + .andWhere("type", "password") + .first(); + + if (!auth) { + throw new errs.ItemNotFoundError("Auth not found"); + } + + return auth; + }, +}; + +export default internal2fa; diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js new file mode 100644 index 0000000..60a7105 --- /dev/null +++ b/backend/internal/access-list.js @@ -0,0 +1,488 @@ +import fs from "node:fs"; +import batchflow from "batchflow"; +import _ from "lodash"; +import errs from "../lib/error.js"; +import utils from "../lib/utils.js"; +import { access as logger } from "../logger.js"; +import accessListModel from "../models/access_list.js"; +import accessListAuthModel from "../models/access_list_auth.js"; +import accessListClientModel from "../models/access_list_client.js"; +import proxyHostModel from "../models/proxy_host.js"; +import internalAuditLog from "./audit-log.js"; +import internalNginx from "./nginx.js"; + +const omissions = () => { + return ["is_deleted"]; +}; + +const internalAccessList = { + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: async (access, data) => { + await access.can("access_lists:create", data); + const row = await accessListModel + .query() + .insertAndFetch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + owner_user_id: access.token.getUserId(1), + }) + .then(utils.omitRow(omissions())); + + data.id = row.id; + + const promises = []; + // Items + data.items.map((item) => { + promises.push( + accessListAuthModel.query().insert({ + access_list_id: row.id, + username: item.username, + password: item.password, + }), + ); + return true; + }); + + // Clients + data.clients?.map((client) => { + promises.push( + accessListClientModel.query().insert({ + access_list_id: row.id, + address: client.address, + directive: client.directive, + }), + ); + return true; + }); + + await Promise.all(promises); + + // re-fetch with expansions + const freshRow = await internalAccessList.get( + access, + { + id: data.id, + expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"], + }, + true // skip masking + ); + + // Audit log + data.meta = _.assign({}, data.meta || {}, freshRow.meta); + await internalAccessList.build(freshRow); + + if (Number.parseInt(freshRow.proxy_host_count, 10)) { + await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts); + } + + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "access-list", + object_id: freshRow.id, + meta: internalAccessList.maskItems(data), + }); + + return internalAccessList.maskItems(freshRow); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.name] + * @param {String} [data.items] + * @return {Promise} + */ + update: async (access, data) => { + await access.can("access_lists:update", data.id); + const row = await internalAccessList.get(access, { id: data.id }); + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`, + ); + } + + // patch name if specified + if (typeof data.name !== "undefined" && data.name) { + await accessListModel.query().where({ id: data.id }).patch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + }); + } + + // Check for items and add/update/remove them + if (typeof data.items !== "undefined" && data.items) { + const promises = []; + const itemsToKeep = []; + + data.items.map((item) => { + if (item.password) { + promises.push( + accessListAuthModel.query().insert({ + access_list_id: data.id, + username: item.username, + password: item.password, + }), + ); + } else { + // This was supplied with an empty password, which means keep it but don't change the password + itemsToKeep.push(item.username); + } + return true; + }); + + const query = accessListAuthModel.query().delete().where("access_list_id", data.id); + + if (itemsToKeep.length) { + query.andWhere("username", "NOT IN", itemsToKeep); + } + + await query; + // Add new items + if (promises.length) { + await Promise.all(promises); + } + } + + // Check for clients and add/update/remove them + if (typeof data.clients !== "undefined" && data.clients) { + const clientPromises = []; + data.clients.map((client) => { + if (client.address) { + clientPromises.push( + accessListClientModel.query().insert({ + access_list_id: data.id, + address: client.address, + directive: client.directive, + }), + ); + } + return true; + }); + + const query = accessListClientModel.query().delete().where("access_list_id", data.id); + await query; + // Add new clitens + if (clientPromises.length) { + await Promise.all(clientPromises); + } + } + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "access-list", + object_id: data.id, + meta: internalAccessList.maskItems(data), + }); + + // re-fetch with expansions + const freshRow = await internalAccessList.get( + access, + { + id: data.id, + expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"], + }, + true // skip masking + ); + + await internalAccessList.build(freshRow) + if (Number.parseInt(freshRow.proxy_host_count, 10)) { + await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts); + } + await internalNginx.reload(); + return internalAccessList.maskItems(freshRow); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @param {Boolean} [skipMasking] + * @return {Promise} + */ + get: async (access, data, skipMasking) => { + const thisData = data || {}; + const accessData = await access.can("access_lists:get", thisData.id) + + const query = accessListModel + .query() + .select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count")) + .leftJoin("proxy_host", function () { + this.on("proxy_host.access_list_id", "=", "access_list.id").andOn( + "proxy_host.is_deleted", + "=", + 0, + ); + }) + .where("access_list.is_deleted", 0) + .andWhere("access_list.id", thisData.id) + .groupBy("access_list.id") + .allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]") + .first(); + + if (accessData.permission_visibility !== "all") { + query.andWhere("access_list.owner_user_id", access.token.getUserId(1)); + } + + if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { + query.withGraphFetched(`[${thisData.expand.join(", ")}]`); + } + + let row = await query.then(utils.omitRow(omissions())); + + if (!row || !row.id) { + throw new errs.ItemNotFoundError(thisData.id); + } + if (!skipMasking && typeof row.items !== "undefined" && row.items) { + row = internalAccessList.maskItems(row); + } + // Custom omissions + if (typeof data.omit !== "undefined" && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: async (access, data) => { + await access.can("access_lists:delete", data.id); + const row = await internalAccessList.get(access, { + id: data.id, + expand: ["proxy_hosts", "items", "clients"], + }); + + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + + // 1. update row to be deleted + // 2. update any proxy hosts that were using it (ignoring permissions) + // 3. reconfigure those hosts + // 4. audit log + + // 1. update row to be deleted + await accessListModel + .query() + .where("id", row.id) + .patch({ + is_deleted: 1, + }); + + // 2. update any proxy hosts that were using it (ignoring permissions) + if (row.proxy_hosts) { + await proxyHostModel + .query() + .where("access_list_id", "=", row.id) + .patch({ access_list_id: 0 }); + + // 3. reconfigure those hosts, then reload nginx + // set the access_list_id to zero for these items + row.proxy_hosts.map((_val, idx) => { + row.proxy_hosts[idx].access_list_id = 0; + return true; + }); + + await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts); + } + + await internalNginx.reload(); + + // delete the htpasswd file + try { + fs.unlinkSync(internalAccessList.getFilename(row)); + } catch (_err) { + // do nothing + } + + // 4. audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "access-list", + object_id: row.id, + meta: _.omit(internalAccessList.maskItems(row), ["is_deleted", "proxy_hosts"]), + }); + return true; + }, + + /** + * All Lists + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [searchQuery] + * @returns {Promise} + */ + getAll: async (access, expand, searchQuery) => { + const accessData = await access.can("access_lists:list"); + + const query = accessListModel + .query() + .select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count")) + .leftJoin("proxy_host", function () { + this.on("proxy_host.access_list_id", "=", "access_list.id").andOn( + "proxy_host.is_deleted", + "=", + 0, + ); + }) + .where("access_list.is_deleted", 0) + .groupBy("access_list.id") + .allowGraph("[owner,items,clients]") + .orderBy("access_list.name", "ASC"); + + if (accessData.permission_visibility !== "all") { + query.andWhere("access_list.owner_user_id", access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof searchQuery === "string") { + query.where(function () { + this.where("name", "like", `%${searchQuery}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + const rows = await query.then(utils.omitRows(omissions())); + if (rows) { + rows.map((row, idx) => { + if (typeof row.items !== "undefined" && row.items) { + rows[idx] = internalAccessList.maskItems(row); + } + return true; + }); + } + return rows; + }, + + /** + * Count is used in reports + * + * @param {Integer} userId + * @param {String} visibility + * @returns {Promise} + */ + getCount: async (userId, visibility) => { + const query = accessListModel + .query() + .count("id as count") + .where("is_deleted", 0); + + if (visibility !== "all") { + query.andWhere("owner_user_id", userId); + } + + const row = await query.first(); + return Number.parseInt(row.count, 10); + }, + + /** + * @param {Object} list + * @returns {Object} + */ + maskItems: (list) => { + if (list && typeof list.items !== "undefined") { + list.items.map((val, idx) => { + let repeatFor = 8; + let firstChar = "*"; + + if (typeof val.password !== "undefined" && val.password) { + repeatFor = val.password.length - 1; + firstChar = val.password.charAt(0); + } + + list.items[idx].hint = firstChar + "*".repeat(repeatFor); + list.items[idx].password = ""; + return true; + }); + } + return list; + }, + + /** + * @param {Object} list + * @param {Integer} list.id + * @returns {String} + */ + getFilename: (list) => { + return `/data/access/${list.id}`; + }, + + /** + * @param {Object} list + * @param {Integer} list.id + * @param {String} list.name + * @param {Array} list.items + * @returns {Promise} + */ + build: async (list) => { + logger.info(`Building Access file #${list.id} for: ${list.name}`); + + const htpasswdFile = internalAccessList.getFilename(list); + + // 1. remove any existing access file + try { + fs.unlinkSync(htpasswdFile); + } catch (_err) { + // do nothing + } + + // 2. create empty access file + fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'}); + + // 3. generate password for each user + if (list.items.length) { + await new Promise((resolve, reject) => { + batchflow(list.items).sequential() + .each((_i, item, next) => { + if (item.password?.length) { + logger.info(`Adding: ${item.username}`); + + utils.execFile('openssl', ['passwd', '-apr1', item.password]) + .then((res) => { + try { + fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'}); + } catch (err) { + reject(err); + } + next(); + }) + .catch((err) => { + logger.error(err); + next(err); + }); + } + }) + .error((err) => { + logger.error(err); + reject(err); + }) + .end((results) => { + logger.success(`Built Access file #${list.id} for: ${list.name}`); + resolve(results); + }); + }); + } + } +} + +export default internalAccessList; diff --git a/backend/internal/audit-log.js b/backend/internal/audit-log.js new file mode 100644 index 0000000..02700dc --- /dev/null +++ b/backend/internal/audit-log.js @@ -0,0 +1,102 @@ +import errs from "../lib/error.js"; +import { castJsonIfNeed } from "../lib/helpers.js"; +import auditLogModel from "../models/audit-log.js"; + +const internalAuditLog = { + + /** + * All logs + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [searchQuery] + * @returns {Promise} + */ + getAll: async (access, expand, searchQuery) => { + await access.can("auditlog:list"); + + const query = auditLogModel + .query() + .orderBy("created_on", "DESC") + .orderBy("id", "DESC") + .limit(100) + .allowGraph("[user]"); + + // Query is used for searching + if (typeof searchQuery === "string" && searchQuery.length > 0) { + query.where(function () { + this.where(castJsonIfNeed("meta"), "like", `%${searchQuery}`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + return await query; + }, + + /** + * @param {Access} access + * @param {Object} [data] + * @param {Integer} [data.id] Defaults to the token user + * @param {Array} [data.expand] + * @return {Promise} + */ + get: async (access, data) => { + await access.can("auditlog:list"); + + const query = auditLogModel + .query() + .andWhere("id", data.id) + .allowGraph("[user]") + .first(); + + if (typeof data.expand !== "undefined" && data.expand !== null) { + query.withGraphFetched(`[${data.expand.join(", ")}]`); + } + + const row = await query; + + if (!row?.id) { + throw new errs.ItemNotFoundError(data.id); + } + + return row; + }, + + /** + * This method should not be publicly used, it doesn't check certain things. It will be assumed + * that permission to add to audit log is already considered, however the access token is used for + * default user id determination. + * + * @param {Access} access + * @param {Object} data + * @param {String} data.action + * @param {Number} [data.user_id] + * @param {Number} [data.object_id] + * @param {Number} [data.object_type] + * @param {Object} [data.meta] + * @returns {Promise} + */ + add: async (access, data) => { + if (typeof data.user_id === "undefined" || !data.user_id) { + data.user_id = access.token.getUserId(1); + } + + if (typeof data.action === "undefined" || !data.action) { + throw new errs.InternalValidationError("Audit log entry must contain an Action"); + } + + // Make sure at least 1 of the IDs are set and action + return await auditLogModel.query().insert({ + user_id: data.user_id, + action: data.action, + object_type: data.object_type || "", + object_id: data.object_id || 0, + meta: data.meta || {}, + }); + }, +}; + +export default internalAuditLog; diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js new file mode 100644 index 0000000..d54e941 --- /dev/null +++ b/backend/internal/certificate.js @@ -0,0 +1,1263 @@ +import fs from "node:fs"; +import https from "node:https"; +import path from "path"; +import archiver from "archiver"; +import _ from "lodash"; +import moment from "moment"; +import { ProxyAgent } from "proxy-agent"; +import tempWrite from "temp-write"; +import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" }; +import { installPlugin } from "../lib/certbot.js"; +import { useLetsencryptServer, useLetsencryptStaging } from "../lib/config.js"; +import error from "../lib/error.js"; +import utils from "../lib/utils.js"; +import { debug, ssl as logger } from "../logger.js"; +import certificateModel from "../models/certificate.js"; +import tokenModel from "../models/token.js"; +import userModel from "../models/user.js"; +import internalAuditLog from "./audit-log.js"; +import internalHost from "./host.js"; +import internalNginx from "./nginx.js"; + +const letsencryptConfig = "/etc/letsencrypt.ini"; +const certbotCommand = "certbot"; +const certbotLogsDir = "/data/logs"; +const certbotWorkDir = "/tmp/letsencrypt-lib"; + +const omissions = () => { + return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"]; +}; + +const internalCertificate = { + allowedSslFiles: ["certificate", "certificate_key", "intermediate_certificate"], + intervalTimeout: 1000 * 60 * 60, // 1 hour + interval: null, + intervalProcessing: false, + renewBeforeExpirationBy: [30, "days"], + + initTimer: () => { + logger.info("Let's Encrypt Renewal Timer initialized"); + internalCertificate.interval = setInterval( + internalCertificate.processExpiringHosts, + internalCertificate.intervalTimeout, + ); + // And do this now as well + internalCertificate.processExpiringHosts(); + }, + + /** + * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required + */ + processExpiringHosts: () => { + if (!internalCertificate.intervalProcessing) { + internalCertificate.intervalProcessing = true; + logger.info( + `Renewing SSL certs expiring within ${internalCertificate.renewBeforeExpirationBy[0]} ${internalCertificate.renewBeforeExpirationBy[1]} ...`, + ); + + const expirationThreshold = moment() + .add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]) + .format("YYYY-MM-DD HH:mm:ss"); + + // Fetch all the letsencrypt certs from the db that will expire within the configured threshold + certificateModel + .query() + .where("is_deleted", 0) + .andWhere("provider", "letsencrypt") + .andWhere("expires_on", "<", expirationThreshold) + .then((certificates) => { + if (!certificates || !certificates.length) { + return null; + } + + /** + * Renews must be run sequentially or we'll get an error 'Another + * instance of Certbot is already running.' + */ + let sequence = Promise.resolve(); + + certificates.forEach((certificate) => { + sequence = sequence.then(() => + internalCertificate + .renew( + { + can: () => + Promise.resolve({ + permission_visibility: "all", + }), + token: tokenModel(), + }, + { id: certificate.id }, + ) + .catch((err) => { + // Don't want to stop the train here, just log the error + logger.error(err.message); + }), + ); + }); + + return sequence; + }) + .then(() => { + logger.info("Completed SSL cert renew process"); + internalCertificate.intervalProcessing = false; + }) + .catch((err) => { + logger.error(err); + internalCertificate.intervalProcessing = false; + }); + } + }, + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: async (access, data) => { + await access.can("certificates:create", data); + data.owner_user_id = access.token.getUserId(1); + + if (data.provider === "letsencrypt") { + data.nice_name = data.domain_names.join(", "); + } + + // this command really should clean up and delete the cert if it can't fully succeed + const certificate = await certificateModel.query().insertAndFetch(data); + + try { + if (certificate.provider === "letsencrypt") { + // Request a new Cert from LE. Let the fun begin. + + // 1. Find out any hosts that are using any of the hostnames in this cert + // 2. Disable them in nginx temporarily + // 3. Generate the LE config + // 4. Request cert + // 5. Remove LE config + // 6. Re-instate previously disabled hosts + + // 1. Find out any hosts that are using any of the hostnames in this cert + const inUseResult = await internalHost.getHostsWithDomains(certificate.domain_names); + + // 2. Disable them in nginx temporarily + await internalCertificate.disableInUseHosts(inUseResult); + + const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first(); + if (!user || !user.email) { + throw new error.ValidationError( + "A valid email address must be set on your user account to use Let's Encrypt", + ); + } + + // With DNS challenge no config is needed, so skip 3 and 5. + if (certificate.meta?.dns_challenge) { + try { + await internalNginx.reload(); + // 4. Request cert + await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email); + await internalNginx.reload(); + // 6. Re-instate previously disabled hosts + await internalCertificate.enableInUseHosts(inUseResult); + } catch (err) { + // In the event of failure, revert things and throw err back + await internalCertificate.enableInUseHosts(inUseResult); + await internalNginx.reload(); + throw err; + } + } else { + // 3. Generate the LE config + try { + await internalNginx.generateLetsEncryptRequestConfig(certificate); + await internalNginx.reload(); + setTimeout(() => {}, 5000); + // 4. Request cert + await internalCertificate.requestLetsEncryptSsl(certificate, user.email); + // 5. Remove LE config + await internalNginx.deleteLetsEncryptRequestConfig(certificate); + await internalNginx.reload(); + // 6. Re-instate previously disabled hosts + await internalCertificate.enableInUseHosts(inUseResult); + } catch (err) { + // In the event of failure, revert things and throw err back + await internalNginx.deleteLetsEncryptRequestConfig(certificate); + await internalCertificate.enableInUseHosts(inUseResult); + await internalNginx.reload(); + throw err; + } + } + + // At this point, the letsencrypt cert should exist on disk. + // Lets get the expiry date from the file and update the row silently + try { + const certInfo = await internalCertificate.getCertificateInfoFromFile( + `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, + ); + const savedRow = await certificateModel + .query() + .patchAndFetchById(certificate.id, { + expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), + }) + .then(utils.omitRow(omissions())); + + // Add cert data for audit log + savedRow.meta = _.assign({}, savedRow.meta, { + letsencrypt_certificate: certInfo, + }); + + await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); + + return savedRow; + } catch (err) { + // Delete the certificate from the database if it was not created successfully + await certificateModel.query().deleteById(certificate.id); + throw err; + } + } + } catch (err) { + // Delete the certificate here. This is a hard delete, since it never existed properly + await certificateModel.query().deleteById(certificate.id); + throw err; + } + + data.meta = _.assign({}, data.meta || {}, certificate.meta); + + // Add to audit log + await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data)); + + return utils.omitRow(omissions())(certificate); + }, + + addCreatedAuditLog: async (access, certificate_id, meta) => { + await internalAuditLog.add(access, { + action: "created", + object_type: "certificate", + object_id: certificate_id, + meta: meta, + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.email] + * @param {String} [data.name] + * @return {Promise} + */ + update: async (access, data) => { + await access.can("certificates:update", data.id); + const row = await internalCertificate.get(access, { id: data.id }); + + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError( + `Certificate could not be updated, IDs do not match: ${row.id} !== ${data.id}`, + ); + } + + const savedRow = await certificateModel + .query() + .patchAndFetchById(row.id, data) + .then(utils.omitRow(omissions())); + + savedRow.meta = internalCertificate.cleanMeta(savedRow.meta); + data.meta = internalCertificate.cleanMeta(data.meta); + + // Add row.nice_name for custom certs + if (savedRow.provider === "other") { + data.nice_name = savedRow.nice_name; + } + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "certificate", + object_id: row.id, + meta: _.omit(data, ["expires_on"]), // this prevents json circular reference because expires_on might be raw + }); + + return savedRow; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: async (access, data) => { + const accessData = await access.can("certificates:get", data.id); + const query = certificateModel + .query() + .where("is_deleted", 0) + .andWhere("id", data.id) + .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]") + .first(); + + if (accessData.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + if (typeof data.expand !== "undefined" && data.expand !== null) { + query.withGraphFetched(`[${data.expand.join(", ")}]`); + } + + const row = await query.then(utils.omitRow(omissions())); + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + // Custom omissions + if (typeof data.omit !== "undefined" && data.omit !== null) { + return _.omit(row, [...data.omit]); + } + + return internalCertificate.cleanExpansions(row); + }, + + cleanExpansions: (row) => { + if (typeof row.proxy_hosts !== "undefined") { + row.proxy_hosts = utils.omitRows(["is_deleted"])(row.proxy_hosts); + } + if (typeof row.redirection_hosts !== "undefined") { + row.redirection_hosts = utils.omitRows(["is_deleted"])(row.redirection_hosts); + } + if (typeof row.dead_hosts !== "undefined") { + row.dead_hosts = utils.omitRows(["is_deleted"])(row.dead_hosts); + } + if (typeof row.streams !== "undefined") { + row.streams = utils.omitRows(["is_deleted"])(row.streams); + } + return row; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @returns {Promise} + */ + download: async (access, data) => { + await access.can("certificates:get", data); + const certificate = await internalCertificate.get(access, data); + if (certificate.provider === "letsencrypt") { + const zipDirectory = internalCertificate.getLiveCertPath(data.id); + if (!fs.existsSync(zipDirectory)) { + throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`); + } + + const certFiles = fs + .readdirSync(zipDirectory) + .filter((fn) => fn.endsWith(".pem")) + .map((fn) => fs.realpathSync(path.join(zipDirectory, fn))); + + const downloadName = `npm-${data.id}-${Date.now()}.zip`; + const opName = `/tmp/${downloadName}`; + + await internalCertificate.zipFiles(certFiles, opName); + debug(logger, "zip completed : ", opName); + return { + fileName: opName, + }; + } + throw new error.ValidationError("Only Let'sEncrypt certificates can be downloaded"); + }, + + /** + * @param {String} source + * @param {String} out + * @returns {Promise} + */ + zipFiles: async (source, out) => { + const archive = archiver("zip", { zlib: { level: 9 } }); + const stream = fs.createWriteStream(out); + + return new Promise((resolve, reject) => { + source.map((fl) => { + const fileName = path.basename(fl); + debug(logger, fl, "added to certificate zip"); + archive.file(fl, { name: fileName }); + return true; + }); + archive.on("error", (err) => reject(err)).pipe(stream); + stream.on("close", () => resolve()); + archive.finalize(); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: async (access, data) => { + await access.can("certificates:delete", data.id); + const row = await internalCertificate.get(access, { id: data.id }); + + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + + await certificateModel.query().where("id", row.id).patch({ + is_deleted: 1, + }); + + // Add to audit log + row.meta = internalCertificate.cleanMeta(row.meta); + + await internalAuditLog.add(access, { + action: "deleted", + object_type: "certificate", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + + if (row.provider === "letsencrypt") { + // Revoke the cert + await internalCertificate.revokeLetsEncryptSsl(row); + } + return true; + }, + + /** + * All Certs + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [searchQuery] + * @returns {Promise} + */ + getAll: async (access, expand, searchQuery) => { + const accessData = await access.can("certificates:list"); + + const query = certificateModel + .query() + .where("is_deleted", 0) + .groupBy("id") + .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]") + .orderBy("nice_name", "ASC"); + + if (accessData.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof searchQuery === "string") { + query.where(function () { + this.where("nice_name", "like", `%${searchQuery}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + const r = await query.then(utils.omitRows(omissions())); + for (let i = 0; i < r.length; i++) { + r[i] = internalCertificate.cleanExpansions(r[i]); + } + return r; + }, + + /** + * Report use + * + * @param {Number} userId + * @param {String} visibility + * @returns {Promise} + */ + getCount: async (userId, visibility) => { + const query = certificateModel.query().count("id as count").where("is_deleted", 0); + + if (visibility !== "all") { + query.andWhere("owner_user_id", userId); + } + + const row = await query.first(); + return Number.parseInt(row.count, 10); + }, + + /** + * @param {Object} certificate + * @returns {Promise} + */ + writeCustomCert: async (certificate) => { + logger.info("Writing Custom Certificate:", certificate); + + const dir = `/data/custom_ssl/npm-${certificate.id}`; + + return new Promise((resolve, reject) => { + if (certificate.provider === "letsencrypt") { + reject(new Error("Refusing to write letsencrypt certs here")); + return; + } + + let certData = certificate.meta.certificate; + if (typeof certificate.meta.intermediate_certificate !== "undefined") { + certData = `${certData}\n${certificate.meta.intermediate_certificate}`; + } + + try { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + } catch (err) { + reject(err); + return; + } + + fs.writeFile(`${dir}/fullchain.pem`, certData, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }).then(() => { + return new Promise((resolve, reject) => { + fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Array} data.domain_names + * @returns {Promise} + */ + createQuickCertificate: async (access, data) => { + return await internalCertificate.create(access, { + provider: "letsencrypt", + domain_names: data.domain_names, + meta: data.meta, + }); + }, + + /** + * Validates that the certs provided are good. + * No access required here, nothing is changed or stored. + * + * @param {Object} data + * @param {Object} data.files + * @returns {Promise} + */ + validate: (data) => { + // Put file contents into an object + const files = {}; + _.map(data.files, (file, name) => { + if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { + files[name] = file.data.toString(); + } + }); + + // For each file, create a temp file and write the contents to it + // Then test it depending on the file type + const promises = []; + _.map(files, (content, type) => { + promises.push( + new Promise((resolve) => { + if (type === "certificate_key") { + resolve(internalCertificate.checkPrivateKey(content)); + } else { + // this should handle `certificate` and intermediate certificate + resolve(internalCertificate.getCertificateInfo(content, true)); + } + }).then((res) => { + return { [type]: res }; + }), + ); + }); + + return Promise.all(promises).then((files) => { + let data = {}; + _.each(files, (file) => { + data = _.assign({}, data, file); + }); + return data; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Object} data.files + * @returns {Promise} + */ + upload: async (access, data) => { + const row = await internalCertificate.get(access, { id: data.id }); + if (row.provider !== "other") { + throw new error.ValidationError("Cannot upload certificates for this type of provider"); + } + + const validations = await internalCertificate.validate(data); + if (typeof validations.certificate === "undefined") { + throw new error.ValidationError("Certificate file was not provided"); + } + + _.map(data.files, (file, name) => { + if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { + row.meta[name] = file.data.toString(); + } + }); + + const certificate = await internalCertificate.update(access, { + id: data.id, + expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), + domain_names: [validations.certificate.cn], + meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later + }); + + certificate.meta = row.meta; + await internalCertificate.writeCustomCert(certificate); + return _.pick(row.meta, internalCertificate.allowedSslFiles); + }, + + /** + * Uses the openssl command to validate the private key. + * It will save the file to disk first, then run commands on it, then delete the file. + * + * @param {String} privateKey This is the entire key contents as a string + */ + checkPrivateKey: async (privateKey) => { + const filepath = await tempWrite(privateKey); + const failTimeout = setTimeout(() => { + throw new error.ValidationError( + "Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.", + ); + }, 10000); + + try { + const result = await utils.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `); + clearTimeout(failTimeout); + if (!result.toLowerCase().includes("key is valid")) { + throw new error.ValidationError(`Result Validation Error: ${result}`); + } + fs.unlinkSync(filepath); + return true; + } catch (err) { + clearTimeout(failTimeout); + fs.unlinkSync(filepath); + throw new error.ValidationError(`Certificate Key is not valid (${err.message})`, err); + } + }, + + /** + * Uses the openssl command to both validate and get info out of the certificate. + * It will save the file to disk first, then run commands on it, then delete the file. + * + * @param {String} certificate This is the entire cert contents as a string + * @param {Boolean} [throwExpired] Throw when the certificate is out of date + */ + getCertificateInfo: async (certificate, throwExpired) => { + const filepath = await tempWrite(certificate); + try { + const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired); + fs.unlinkSync(filepath); + return certData; + } catch (err) { + fs.unlinkSync(filepath); + throw err; + } + }, + + /** + * Uses the openssl command to both validate and get info out of the certificate. + * It will save the file to disk first, then run commands on it, then delete the file. + * + * @param {String} certificateFile The file location on disk + * @param {Boolean} [throw_expired] Throw when the certificate is out of date + */ + getCertificateInfoFromFile: async (certificateFile, throw_expired) => { + const certData = {}; + + try { + const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]); + // Examples: + // subject=CN = *.jc21.com + // subject=CN = something.example.com + const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; + const match = regex.exec(result); + if (match && typeof match[1] !== "undefined") { + certData.cn = match[1]; + } + + const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]); + // Examples: + // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 + // issuer=C = US, O = Let's Encrypt, CN = E5 + // issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA","O = NginxProxyManager, CN = NginxProxyManager Intermediate CA + const regex2 = /^(?:issuer=)?(.*)$/gim; + const match2 = regex2.exec(result2); + if (match2 && typeof match2[1] !== "undefined") { + certData.issuer = match2[1]; + } + + const result3 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-dates", "-noout"]); + // notBefore=Jul 14 04:04:29 2018 GMT + // notAfter=Oct 12 04:04:29 2018 GMT + let validFrom = null; + let validTo = null; + + const lines = result3.split("\n"); + lines.map((str) => { + const regex = /^(\S+)=(.*)$/gim; + const match = regex.exec(str.trim()); + + if (match && typeof match[2] !== "undefined") { + const date = Number.parseInt(moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), 10); + + if (match[1].toLowerCase() === "notbefore") { + validFrom = date; + } else if (match[1].toLowerCase() === "notafter") { + validTo = date; + } + } + return true; + }); + + if (!validFrom || !validTo) { + throw new error.ValidationError(`Could not determine dates from certificate: ${result}`); + } + + if (throw_expired && validTo < Number.parseInt(moment().format("X"), 10)) { + throw new error.ValidationError("Certificate has expired"); + } + + certData.dates = { + from: validFrom, + to: validTo, + }; + + return certData; + } catch (err) { + throw new error.ValidationError(`Certificate is not valid (${err.message})`, err); + } + }, + + /** + * Cleans the ssl keys from the meta object and sets them to "true" + * + * @param {Object} meta + * @param {Boolean} [remove] + * @returns {Object} + */ + cleanMeta: (meta, remove) => { + internalCertificate.allowedSslFiles.map((key) => { + if (typeof meta[key] !== "undefined" && meta[key]) { + if (remove) { + delete meta[key]; + } else { + meta[key] = true; + } + } + return true; + }); + return meta; + }, + + /** + * Request a certificate using the http challenge + * @param {Object} certificate the certificate row + * @param {String} email the email address to use for registration + * @returns {Promise} + */ + requestLetsEncryptSsl: async (certificate, email) => { + logger.info( + `Requesting LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, + ); + + const args = [ + "certonly", + "--config", + letsencryptConfig, + "--work-dir", + certbotWorkDir, + "--logs-dir", + certbotLogsDir, + "--cert-name", + `npm-${certificate.id}`, + "--agree-tos", + "--authenticator", + "webroot", + "-m", + email, + "--preferred-challenges", + "http", + "--domains", + certificate.domain_names.join(","), + ]; + + // Add key-type parameter if specified + if (certificate.meta?.key_type) { + args.push("--key-type", certificate.meta.key_type); + } + + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id); + args.push(...adds.args); + + logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); + + const result = await utils.execFile(certbotCommand, args, adds.opts); + logger.success(result); + return result; + }, + + /** + * @param {Object} certificate the certificate row + * @param {String} email the email address to use for registration + * @returns {Promise} + */ + requestLetsEncryptSslWithDnsChallenge: async (certificate, email) => { + await installPlugin(certificate.meta.dns_provider); + const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; + logger.info( + `Requesting LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, + ); + + const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; + fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true }); + fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 }); + + // Whether the plugin has a ---credentials argument + const hasConfigArg = certificate.meta.dns_provider !== "route53"; + + const args = [ + "certonly", + "--config", + letsencryptConfig, + "--work-dir", + certbotWorkDir, + "--logs-dir", + certbotLogsDir, + "--cert-name", + `npm-${certificate.id}`, + "--agree-tos", + "-m", + email, + "--preferred-challenges", + "dns", + "--domains", + certificate.domain_names.join(","), + "--authenticator", + dnsPlugin.full_plugin_name, + ]; + + if (hasConfigArg) { + args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation); + } + if (certificate.meta.propagation_seconds !== undefined) { + args.push( + `--${dnsPlugin.full_plugin_name}-propagation-seconds`, + certificate.meta.propagation_seconds.toString(), + ); + } + + // Add key-type parameter if specified + if (certificate.meta?.key_type) { + args.push("--key-type", certificate.meta.key_type); + } + + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); + args.push(...adds.args); + + logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); + + try { + const result = await utils.execFile(certbotCommand, args, adds.opts); + logger.info(result); + return result; + } catch (err) { + // Don't fail if file does not exist, so no need for action in the callback + fs.unlink(credentialsLocation, () => {}); + throw err; + } + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @returns {Promise} + */ + renew: async (access, data) => { + await access.can("certificates:update", data); + const certificate = await internalCertificate.get(access, data); + + if (certificate.provider === "letsencrypt") { + const renewMethod = certificate.meta.dns_challenge + ? internalCertificate.renewLetsEncryptSslWithDnsChallenge + : internalCertificate.renewLetsEncryptSsl; + + await renewMethod(certificate); + const certInfo = await internalCertificate.getCertificateInfoFromFile( + `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, + ); + + const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, { + expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), + }); + + // Add to audit log + await internalAuditLog.add(access, { + action: "renewed", + object_type: "certificate", + object_id: updatedCertificate.id, + meta: updatedCertificate, + }); + + return updatedCertificate; + } + + throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed"); + }, + + /** + * @param {Object} certificate the certificate row + * @returns {Promise} + */ + renewLetsEncryptSsl: async (certificate) => { + logger.info( + `Renewing LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, + ); + + const args = [ + "renew", + "--force-renewal", + "--config", + letsencryptConfig, + "--work-dir", + certbotWorkDir, + "--logs-dir", + certbotLogsDir, + "--cert-name", + `npm-${certificate.id}`, + "--preferred-challenges", + "http", + "--no-random-sleep-on-renew", + "--disable-hook-validation", + ]; + + // Add key-type parameter if specified + if (certificate.meta?.key_type) { + args.push("--key-type", certificate.meta.key_type); + } + + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); + args.push(...adds.args); + + logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); + + const result = await utils.execFile(certbotCommand, args, adds.opts); + logger.info(result); + return result; + }, + + /** + * @param {Object} certificate the certificate row + * @returns {Promise} + */ + renewLetsEncryptSslWithDnsChallenge: async (certificate) => { + const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; + if (!dnsPlugin) { + throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); + } + + logger.info( + `Renewing LetsEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, + ); + + const args = [ + "renew", + "--force-renewal", + "--config", + letsencryptConfig, + "--work-dir", + certbotWorkDir, + "--logs-dir", + certbotLogsDir, + "--cert-name", + `npm-${certificate.id}`, + "--preferred-challenges", + "dns", + "--disable-hook-validation", + "--no-random-sleep-on-renew", + ]; + + // Add key-type parameter if specified + if (certificate.meta?.key_type) { + args.push("--key-type", certificate.meta.key_type); + } + + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); + args.push(...adds.args); + + logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); + + const result = await utils.execFile(certbotCommand, args, adds.opts); + logger.info(result); + return result; + }, + + /** + * @param {Object} certificate the certificate row + * @param {Boolean} [throwErrors] + * @returns {Promise} + */ + revokeLetsEncryptSsl: async (certificate, throwErrors) => { + logger.info( + `Revoking LetsEncrypt certificates for Cert #${certificate.id}: ${certificate.domain_names.join(", ")}`, + ); + + const args = [ + "revoke", + "--config", + letsencryptConfig, + "--work-dir", + certbotWorkDir, + "--logs-dir", + certbotLogsDir, + "--cert-path", + `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, + "--delete-after-revoke", + ]; + + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id); + args.push(...adds.args); + + logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); + + try { + const result = await utils.execFile(certbotCommand, args, adds.opts); + await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`); + logger.info(result); + return result; + } catch (err) { + logger.error(err.message); + if (throwErrors) { + throw err; + } + } + }, + + /** + * @param {Object} certificate + * @returns {Boolean} + */ + hasLetsEncryptSslCerts: (certificate) => { + const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id); + return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`); + }, + + /** + * @param {Object} inUseResult + * @param {Number} inUseResult.total_count + * @param {Array} inUseResult.proxy_hosts + * @param {Array} inUseResult.redirection_hosts + * @param {Array} inUseResult.dead_hosts + * @returns {Promise} + */ + disableInUseHosts: async (inUseResult) => { + if (inUseResult?.total_count) { + if (inUseResult?.proxy_hosts.length) { + await internalNginx.bulkDeleteConfigs("proxy_host", inUseResult.proxy_hosts); + } + + if (inUseResult?.redirection_hosts.length) { + await internalNginx.bulkDeleteConfigs("redirection_host", inUseResult.redirection_hosts); + } + + if (inUseResult?.dead_hosts.length) { + await internalNginx.bulkDeleteConfigs("dead_host", inUseResult.dead_hosts); + } + } + }, + + /** + * @param {Object} inUseResult + * @param {Number} inUseResult.total_count + * @param {Array} inUseResult.proxy_hosts + * @param {Array} inUseResult.redirection_hosts + * @param {Array} inUseResult.dead_hosts + * @returns {Promise} + */ + enableInUseHosts: async (inUseResult) => { + if (inUseResult.total_count) { + if (inUseResult.proxy_hosts.length) { + await internalNginx.bulkGenerateConfigs("proxy_host", inUseResult.proxy_hosts); + } + + if (inUseResult.redirection_hosts.length) { + await internalNginx.bulkGenerateConfigs("redirection_host", inUseResult.redirection_hosts); + } + + if (inUseResult.dead_hosts.length) { + await internalNginx.bulkGenerateConfigs("dead_host", inUseResult.dead_hosts); + } + } + }, + + /** + * + * @param {Object} payload + * @param {string[]} payload.domains + * @returns + */ + testHttpsChallenge: async (access, payload) => { + await access.can("certificates:list"); + + // Create a test challenge file + const testChallengeDir = "/data/letsencrypt-acme-challenge/.well-known/acme-challenge"; + const testChallengeFile = `${testChallengeDir}/test-challenge`; + fs.mkdirSync(testChallengeDir, { recursive: true }); + fs.writeFileSync(testChallengeFile, "Success", { encoding: "utf8" }); + + const results = {}; + for (const domain of payload.domains) { + results[domain] = await internalCertificate.performTestForDomain(domain); + } + + // Remove the test challenge file + fs.unlinkSync(testChallengeFile); + + return results; + }, + + performTestForDomain: async (domain) => { + logger.info(`Testing http challenge for ${domain}`); + const agent = new ProxyAgent(); + const url = `http://${domain}/.well-known/acme-challenge/test-challenge`; + const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`; + const options = { + method: "POST", + headers: { + "User-Agent": "Mozilla/5.0", + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(formBody), + }, + agent, + }; + + const result = await new Promise((resolve) => { + const req = https.request("https://www.site24x7.com/tools/restapi-tester", options, (res) => { + let responseBody = ""; + + res.on("data", (chunk) => { + responseBody = responseBody + chunk; + }); + + res.on("end", () => { + try { + const parsedBody = JSON.parse(`${responseBody}`); + if (res.statusCode !== 200) { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`, + ); + resolve(undefined); + } else { + resolve(parsedBody); + } + } catch (err) { + if (res.statusCode !== 200) { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`, + ); + } else { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`, + ); + } + resolve(undefined); + } + }); + }); + + // Make sure to write the request body. + req.write(formBody); + req.end(); + req.on("error", (e) => { + logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e); + resolve(undefined); + }); + }); + + if (!result) { + // Some error occurred while trying to get the data + return "failed"; + } + if (result.error) { + logger.info( + `HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`, + ); + return `other:${result.error.msg}`; + } + if (`${result.responsecode}` === "200" && result.htmlresponse === "Success") { + // Server exists and has responded with the correct data + return "ok"; + } + if (`${result.responsecode}` === "200") { + // Server exists but has responded with wrong data + logger.info( + `HTTP challenge test failed for domain ${domain} because of invalid returned data:`, + result.htmlresponse, + ); + return "wrong-data"; + } + if (`${result.responsecode}` === "404") { + // Server exists but responded with a 404 + logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`); + return "404"; + } + if ( + `${result.responsecode}` === "0" || + (typeof result.reason === "string" && result.reason.toLowerCase() === "host unavailable") + ) { + // Server does not exist at domain + logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`); + return "no-host"; + } + // Other errors + logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); + return `other:${result.responsecode}`; + }, + + getAdditionalCertbotArgs: (certificate_id, dns_provider) => { + const args = []; + if (useLetsencryptServer() !== null) { + args.push("--server", useLetsencryptServer()); + } + if (useLetsencryptStaging() && useLetsencryptServer() === null) { + args.push("--staging"); + } + + // For route53, add the credentials file as an environment variable, + // inheriting the process env + const opts = {}; + if (certificate_id && dns_provider === "route53") { + opts.env = process.env; + opts.env.AWS_CONFIG_FILE = `/etc/letsencrypt/credentials/credentials-${certificate_id}`; + } + + if (dns_provider === "duckdns") { + args.push("--dns-duckdns-no-txt-restore"); + } + + return { args: args, opts: opts }; + }, + + getLiveCertPath: (certificateId) => { + return `/etc/letsencrypt/live/npm-${certificateId}`; + }, +}; + +export default internalCertificate; diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js new file mode 100644 index 0000000..34c94fd --- /dev/null +++ b/backend/internal/dead-host.js @@ -0,0 +1,394 @@ +import _ from "lodash"; +import errs from "../lib/error.js"; +import { castJsonIfNeed } from "../lib/helpers.js"; +import utils from "../lib/utils.js"; +import deadHostModel from "../models/dead_host.js"; +import internalAuditLog from "./audit-log.js"; +import internalCertificate from "./certificate.js"; +import internalHost from "./host.js"; +import internalNginx from "./nginx.js"; + +const omissions = () => { + return ["is_deleted"]; +}; + +const internalDeadHost = { + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: async (access, data) => { + const createCertificate = data.certificate_id === "new"; + + if (createCertificate) { + delete data.certificate_id; + } + + await access.can("dead_hosts:create", data); + + // Get a list of the domain names and check each of them against existing records + const domainNameCheckPromises = []; + + data.domain_names.map((domain_name) => { + domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name)); + return true; + }); + + await Promise.all(domainNameCheckPromises).then((check_results) => { + check_results.map((result) => { + if (result.is_taken) { + throw new errs.ValidationError(`${result.hostname} is already in use`); + } + return true; + }); + }); + + // At this point the domains should have been checked + data.owner_user_id = access.token.getUserId(1); + const thisData = internalHost.cleanSslHstsData(data); + + // Fix for db field not having a default value + // for this optional field. + if (typeof data.advanced_config === "undefined") { + thisData.advanced_config = ""; + } + + const row = await deadHostModel.query() + .insertAndFetch(thisData) + .then(utils.omitRow(omissions())); + + // Add to audit log + await internalAuditLog.add(access, { + action: "created", + object_type: "dead-host", + object_id: row.id, + meta: thisData, + }); + + if (createCertificate) { + const cert = await internalCertificate.createQuickCertificate(access, data); + + // update host with cert id + await internalDeadHost.update(access, { + id: row.id, + certificate_id: cert.id, + }); + } + + // re-fetch with cert + const freshRow = await internalDeadHost.get(access, { + id: row.id, + expand: ["certificate", "owner"], + }); + + // Sanity check + if (createCertificate && !freshRow.certificate_id) { + throw new errs.InternalValidationError("The host was created but the Certificate creation failed."); + } + + // Configure nginx + await internalNginx.configure(deadHostModel, "dead_host", freshRow); + + return freshRow; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @return {Promise} + */ + update: async (access, data) => { + const createCertificate = data.certificate_id === "new"; + if (createCertificate) { + delete data.certificate_id; + } + + await access.can("dead_hosts:update", data.id); + + // Get a list of the domain names and check each of them against existing records + const domainNameCheckPromises = []; + if (typeof data.domain_names !== "undefined") { + data.domain_names.map((domainName) => { + domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id)); + return true; + }); + + const checkResults = await Promise.all(domainNameCheckPromises); + checkResults.map((result) => { + if (result.is_taken) { + throw new errs.ValidationError(`${result.hostname} is already in use`); + } + return true; + }); + } + const row = await internalDeadHost.get(access, { id: data.id }); + + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `404 Host could not be updated, IDs do not match: ${row.id} !== ${data.id}`, + ); + } + + if (createCertificate) { + const cert = await internalCertificate.createQuickCertificate(access, { + domain_names: data.domain_names || row.domain_names, + meta: _.assign({}, row.meta, data.meta), + }); + + // update host with cert id + data.certificate_id = cert.id; + } + + // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. + let thisData = _.assign( + {}, + { + domain_names: row.domain_names, + }, + data, + ); + + thisData = internalHost.cleanSslHstsData(thisData, row); + + + // do the row update + await deadHostModel + .query() + .where({id: data.id}) + .patch(data); + + // Add to audit log + await internalAuditLog.add(access, { + action: "updated", + object_type: "dead-host", + object_id: row.id, + meta: thisData, + }); + + const thisRow = await internalDeadHost + .get(access, { + id: thisData.id, + expand: ["owner", "certificate"], + }); + + // Configure nginx + const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row); + row.meta = newMeta; + return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions()); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: async (access, data) => { + const accessData = await access.can("dead_hosts:get", data.id); + const query = deadHostModel + .query() + .where("is_deleted", 0) + .andWhere("id", data.id) + .allowGraph(deadHostModel.defaultAllowGraph) + .first(); + + if (accessData.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + if (typeof data.expand !== "undefined" && data.expand !== null) { + query.withGraphFetched(`[${data.expand.join(", ")}]`); + } + + const row = await query.then(utils.omitRow(omissions())); + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + // Custom omissions + if (typeof data.omit !== "undefined" && data.omit !== null) { + return _.omit(row, data.omit); + } + return row; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: async (access, data) => { + await access.can("dead_hosts:delete", data.id) + const row = await internalDeadHost.get(access, { id: data.id }); + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + + await deadHostModel + .query() + .where("id", row.id) + .patch({ + is_deleted: 1, + }); + + // Delete Nginx Config + await internalNginx.deleteConfig("dead_host", row); + await internalNginx.reload(); + + // Add to audit log + await internalAuditLog.add(access, { + action: "deleted", + object_type: "dead-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + return true; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + enable: async (access, data) => { + await access.can("dead_hosts:update", data.id) + const row = await internalDeadHost.get(access, { + id: data.id, + expand: ["certificate", "owner"], + }); + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (row.enabled) { + throw new errs.ValidationError("Host is already enabled"); + } + + row.enabled = 1; + + await deadHostModel + .query() + .where("id", row.id) + .patch({ + enabled: 1, + }); + + // Configure nginx + await internalNginx.configure(deadHostModel, "dead_host", row); + + // Add to audit log + await internalAuditLog.add(access, { + action: "enabled", + object_type: "dead-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + return true; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + disable: async (access, data) => { + await access.can("dead_hosts:update", data.id) + const row = await internalDeadHost.get(access, { id: data.id }); + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (!row.enabled) { + throw new errs.ValidationError("Host is already disabled"); + } + + row.enabled = 0; + + await deadHostModel + .query() + .where("id", row.id) + .patch({ + enabled: 0, + }); + + // Delete Nginx Config + await internalNginx.deleteConfig("dead_host", row); + await internalNginx.reload(); + + // Add to audit log + await internalAuditLog.add(access, { + action: "disabled", + object_type: "dead-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + return true; + }, + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [searchQuery] + * @returns {Promise} + */ + getAll: async (access, expand, searchQuery) => { + const accessData = await access.can("dead_hosts:list") + const query = deadHostModel + .query() + .where("is_deleted", 0) + .groupBy("id") + .allowGraph(deadHostModel.defaultAllowGraph) + .orderBy(castJsonIfNeed("domain_names"), "ASC"); + + if (accessData.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof searchQuery === "string" && searchQuery.length > 0) { + query.where(function () { + this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + const rows = await query.then(utils.omitRows(omissions())); + if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { + internalHost.cleanAllRowsCertificateMeta(rows); + } + return rows; + }, + + /** + * Report use + * + * @param {Number} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: async (user_id, visibility) => { + const query = deadHostModel.query().count("id as count").where("is_deleted", 0); + + if (visibility !== "all") { + query.andWhere("owner_user_id", user_id); + } + + const row = await query.first(); + return Number.parseInt(row.count, 10); + }, +}; + +export default internalDeadHost; diff --git a/backend/internal/host.js b/backend/internal/host.js new file mode 100644 index 0000000..7487162 --- /dev/null +++ b/backend/internal/host.js @@ -0,0 +1,234 @@ +import _ from "lodash"; +import { castJsonIfNeed } from "../lib/helpers.js"; +import deadHostModel from "../models/dead_host.js"; +import proxyHostModel from "../models/proxy_host.js"; +import redirectionHostModel from "../models/redirection_host.js"; + +const internalHost = { + /** + * Makes sure that the ssl_* and hsts_* fields play nicely together. + * ie: if there is no cert, then force_ssl is off. + * if force_ssl is off, then hsts_enabled is definitely off. + * + * @param {object} data + * @param {object} [existing_data] + * @returns {object} + */ + cleanSslHstsData: (data, existingData) => { + const combinedData = _.assign({}, existingData || {}, data); + + if (!combinedData.certificate_id) { + combinedData.ssl_forced = false; + combinedData.http2_support = false; + } + + if (!combinedData.ssl_forced) { + combinedData.hsts_enabled = false; + } + + if (!combinedData.hsts_enabled) { + combinedData.hsts_subdomains = false; + } + + return combinedData; + }, + + /** + * used by the getAll functions of hosts, this removes the certificate meta if present + * + * @param {Array} rows + * @returns {Array} + */ + cleanAllRowsCertificateMeta: (rows) => { + rows.map((_, idx) => { + if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) { + rows[idx].certificate.meta = {}; + } + return true; + }); + + return rows; + }, + + /** + * used by the get/update functions of hosts, this removes the certificate meta if present + * + * @param {Object} row + * @returns {Object} + */ + cleanRowCertificateMeta: (row) => { + if (typeof row.certificate !== "undefined" && row.certificate) { + row.certificate.meta = {}; + } + + return row; + }, + + /** + * This returns all the host types with any domain listed in the provided domainNames array. + * This is used by the certificates to temporarily disable any host that is using the domain + * + * @param {Array} domainNames + * @returns {Promise} + */ + getHostsWithDomains: async (domainNames) => { + const responseObject = { + total_count: 0, + dead_hosts: [], + proxy_hosts: [], + redirection_hosts: [], + }; + + const proxyRes = await proxyHostModel.query().where("is_deleted", 0); + responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames); + responseObject.total_count += responseObject.proxy_hosts.length; + + const redirRes = await redirectionHostModel.query().where("is_deleted", 0); + responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames); + responseObject.total_count += responseObject.redirection_hosts.length; + + const deadRes = await deadHostModel.query().where("is_deleted", 0); + responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames); + responseObject.total_count += responseObject.dead_hosts.length; + + return responseObject; + }, + + /** + * Internal use only, checks to see if the domain is already taken by any other record + * + * @param {String} hostname + * @param {String} [ignore_type] 'proxy', 'redirection', 'dead' + * @param {Integer} [ignore_id] Must be supplied if type was also supplied + * @returns {Promise} + */ + isHostnameTaken: (hostname, ignore_type, ignore_id) => { + const promises = [ + proxyHostModel + .query() + .where("is_deleted", 0) + .andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), + redirectionHostModel + .query() + .where("is_deleted", 0) + .andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), + deadHostModel + .query() + .where("is_deleted", 0) + .andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`), + ]; + + return Promise.all(promises).then((promises_results) => { + let is_taken = false; + + if (promises_results[0]) { + // Proxy Hosts + if ( + internalHost._checkHostnameRecordsTaken( + hostname, + promises_results[0], + ignore_type === "proxy" && ignore_id ? ignore_id : 0, + ) + ) { + is_taken = true; + } + } + + if (promises_results[1]) { + // Redirection Hosts + if ( + internalHost._checkHostnameRecordsTaken( + hostname, + promises_results[1], + ignore_type === "redirection" && ignore_id ? ignore_id : 0, + ) + ) { + is_taken = true; + } + } + + if (promises_results[2]) { + // Dead Hosts + if ( + internalHost._checkHostnameRecordsTaken( + hostname, + promises_results[2], + ignore_type === "dead" && ignore_id ? ignore_id : 0, + ) + ) { + is_taken = true; + } + } + + return { + hostname: hostname, + is_taken: is_taken, + }; + }); + }, + + /** + * Private call only + * + * @param {String} hostname + * @param {Array} existingRows + * @param {Integer} [ignoreId] + * @returns {Boolean} + */ + _checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => { + let isTaken = false; + + if (existingRows?.length) { + existingRows.map((existingRow) => { + existingRow.domain_names.map((existingHostname) => { + // Does this domain match? + if (existingHostname.toLowerCase() === hostname.toLowerCase()) { + if (!ignoreId || ignoreId !== existingRow.id) { + isTaken = true; + } + } + return true; + }); + return true; + }); + } + + return isTaken; + }, + + /** + * Private call only + * + * @param {Array} hosts + * @param {Array} domainNames + * @returns {Array} + */ + _getHostsWithDomains: (hosts, domainNames) => { + const response = []; + + if (hosts?.length) { + hosts.map((host) => { + let hostMatches = false; + + domainNames.map((domainName) => { + host.domain_names.map((hostDomainName) => { + if (domainName.toLowerCase() === hostDomainName.toLowerCase()) { + hostMatches = true; + } + return true; + }); + return true; + }); + + if (hostMatches) { + response.push(host); + } + return true; + }); + } + + return response; + }, +}; + +export default internalHost; diff --git a/backend/internal/ip_ranges.js b/backend/internal/ip_ranges.js new file mode 100644 index 0000000..6aa2b88 --- /dev/null +++ b/backend/internal/ip_ranges.js @@ -0,0 +1,158 @@ +import fs from "node:fs"; +import https from "node:https"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { ProxyAgent } from "proxy-agent"; +import errs from "../lib/error.js"; +import utils from "../lib/utils.js"; +import { ipRanges as logger } from "../logger.js"; +import internalNginx from "./nginx.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const CLOUDFRONT_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json"; +const CLOUDFARE_V4_URL = "https://www.cloudflare.com/ips-v4"; +const CLOUDFARE_V6_URL = "https://www.cloudflare.com/ips-v6"; + +const regIpV4 = /^(\d+\.?){4}\/\d+/; +const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/; + +const internalIpRanges = { + interval_timeout: 1000 * 60 * 60 * 6, // 6 hours + interval: null, + interval_processing: false, + iteration_count: 0, + + initTimer: () => { + logger.info("IP Ranges Renewal Timer initialized"); + internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout); + }, + + fetchUrl: (url) => { + const agent = new ProxyAgent(); + return new Promise((resolve, reject) => { + logger.info(`Fetching ${url}`); + return https + .get(url, { agent }, (res) => { + res.setEncoding("utf8"); + let raw_data = ""; + res.on("data", (chunk) => { + raw_data += chunk; + }); + + res.on("end", () => { + resolve(raw_data); + }); + }) + .on("error", (err) => { + reject(err); + }); + }); + }, + + /** + * Triggered at startup and then later by a timer, this will fetch the ip ranges from services and apply them to nginx. + */ + fetch: () => { + if (!internalIpRanges.interval_processing) { + internalIpRanges.interval_processing = true; + logger.info("Fetching IP Ranges from online services..."); + + let ip_ranges = []; + + return internalIpRanges + .fetchUrl(CLOUDFRONT_URL) + .then((cloudfront_data) => { + const data = JSON.parse(cloudfront_data); + + if (data && typeof data.prefixes !== "undefined") { + data.prefixes.map((item) => { + if (item.service === "CLOUDFRONT") { + ip_ranges.push(item.ip_prefix); + } + return true; + }); + } + + if (data && typeof data.ipv6_prefixes !== "undefined") { + data.ipv6_prefixes.map((item) => { + if (item.service === "CLOUDFRONT") { + ip_ranges.push(item.ipv6_prefix); + } + return true; + }); + } + }) + .then(() => { + return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL); + }) + .then((cloudfare_data) => { + const items = cloudfare_data.split("\n").filter((line) => regIpV4.test(line)); + ip_ranges = [...ip_ranges, ...items]; + }) + .then(() => { + return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL); + }) + .then((cloudfare_data) => { + const items = cloudfare_data.split("\n").filter((line) => regIpV6.test(line)); + ip_ranges = [...ip_ranges, ...items]; + }) + .then(() => { + const clean_ip_ranges = []; + ip_ranges.map((range) => { + if (range) { + clean_ip_ranges.push(range); + } + return true; + }); + + return internalIpRanges.generateConfig(clean_ip_ranges).then(() => { + if (internalIpRanges.iteration_count) { + // Reload nginx + return internalNginx.reload(); + } + }); + }) + .then(() => { + internalIpRanges.interval_processing = false; + internalIpRanges.iteration_count++; + }) + .catch((err) => { + logger.fatal(err.message); + internalIpRanges.interval_processing = false; + }); + } + }, + + /** + * @param {Array} ip_ranges + * @returns {Promise} + */ + generateConfig: (ip_ranges) => { + const renderEngine = utils.getRenderEngine(); + return new Promise((resolve, reject) => { + let template = null; + const filename = "/etc/nginx/conf.d/include/ip_ranges.conf"; + try { + template = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: "utf8" }); + } catch (err) { + reject(new errs.ConfigurationError(err.message)); + return; + } + + renderEngine + .parseAndRender(template, { ip_ranges: ip_ranges }) + .then((config_text) => { + fs.writeFileSync(filename, config_text, { encoding: "utf8" }); + resolve(true); + }) + .catch((err) => { + logger.warn(`Could not write ${filename}: ${err.message}`); + reject(new errs.ConfigurationError(err.message)); + }); + }); + }, +}; + +export default internalIpRanges; diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js new file mode 100644 index 0000000..fe84607 --- /dev/null +++ b/backend/internal/nginx.js @@ -0,0 +1,437 @@ +import fs from "node:fs"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import _ from "lodash"; +import errs from "../lib/error.js"; +import utils from "../lib/utils.js"; +import { debug, nginx as logger } from "../logger.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const internalNginx = { + /** + * This will: + * - test the nginx config first to make sure it's OK + * - create / recreate the config for the host + * - test again + * - IF OK: update the meta with online status + * - IF BAD: update the meta with offline status and remove the config entirely + * - then reload nginx + * + * @param {Object|String} model + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + configure: (model, host_type, host) => { + let combined_meta = {}; + + return internalNginx + .test() + .then(() => { + // Nginx is OK + // We're deleting this config regardless. + // Don't throw errors, as the file may not exist at all + // Delete the .err file too + return internalNginx.deleteConfig(host_type, host, false, true); + }) + .then(() => { + return internalNginx.generateConfig(host_type, host); + }) + .then(() => { + // Test nginx again and update meta with result + return internalNginx + .test() + .then(() => { + // nginx is ok + combined_meta = _.assign({}, host.meta, { + nginx_online: true, + nginx_err: null, + }); + + return model.query().where("id", host.id).patch({ + meta: combined_meta, + }); + }) + .catch((err) => { + // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported. + // It will always look like this: + // nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address) + + const valid_lines = []; + const err_lines = err.message.split("\n"); + err_lines.map((line) => { + if (line.indexOf("/var/log/nginx/error.log") === -1) { + valid_lines.push(line); + } + return true; + }); + + debug(logger, "Nginx test failed:", valid_lines.join("\n")); + + // config is bad, update meta and delete config + combined_meta = _.assign({}, host.meta, { + nginx_online: false, + nginx_err: valid_lines.join("\n"), + }); + + return model + .query() + .where("id", host.id) + .patch({ + meta: combined_meta, + }) + .then(() => { + internalNginx.renameConfigAsError(host_type, host); + }) + .then(() => { + return internalNginx.deleteConfig(host_type, host, true); + }); + }); + }) + .then(() => { + return internalNginx.reload(); + }) + .then(() => { + return combined_meta; + }); + }, + + /** + * @returns {Promise} + */ + test: () => { + debug(logger, "Testing Nginx configuration"); + return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]); + }, + + /** + * @returns {Promise} + */ + reload: () => { + return internalNginx.test().then(() => { + logger.info("Reloading Nginx"); + return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]); + }); + }, + + /** + * @param {String} host_type + * @param {Integer} host_id + * @returns {String} + */ + getConfigName: (host_type, host_id) => { + if (host_type === "default") { + return "/data/nginx/default_host/site.conf"; + } + return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`; + }, + + /** + * Generates custom locations + * @param {Object} host + * @returns {Promise} + */ + renderLocations: (host) => { + return new Promise((resolve, reject) => { + let template; + + try { + template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" }); + } catch (err) { + reject(new errs.ConfigurationError(err.message)); + return; + } + + const renderEngine = utils.getRenderEngine(); + let renderedLocations = ""; + + const locationRendering = async () => { + for (let i = 0; i < host.locations.length; i++) { + const locationCopy = Object.assign( + {}, + { access_list_id: host.access_list_id }, + { certificate_id: host.certificate_id }, + { ssl_forced: host.ssl_forced }, + { caching_enabled: host.caching_enabled }, + { block_exploits: host.block_exploits }, + { allow_websocket_upgrade: host.allow_websocket_upgrade }, + { http2_support: host.http2_support }, + { hsts_enabled: host.hsts_enabled }, + { hsts_subdomains: host.hsts_subdomains }, + { access_list: host.access_list }, + { certificate: host.certificate }, + host.locations[i], + ); + + if (locationCopy.forward_host.indexOf("/") > -1) { + const splitted = locationCopy.forward_host.split("/"); + + locationCopy.forward_host = splitted.shift(); + locationCopy.forward_path = `/${splitted.join("/")}`; + } + + renderedLocations += await renderEngine.parseAndRender(template, locationCopy); + } + }; + + locationRendering().then(() => resolve(renderedLocations)); + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + generateConfig: (host_type, host_row) => { + // Prevent modifying the original object: + const host = JSON.parse(JSON.stringify(host_row)); + const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); + + debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); + + const renderEngine = utils.getRenderEngine(); + + return new Promise((resolve, reject) => { + let template = null; + const filename = internalNginx.getConfigName(nice_host_type, host.id); + + try { + template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" }); + } catch (err) { + reject(new errs.ConfigurationError(err.message)); + return; + } + + let locationsPromise; + let origLocations; + + // Manipulate the data a bit before sending it to the template + if (nice_host_type !== "default") { + host.use_default_location = true; + if (typeof host.advanced_config !== "undefined" && host.advanced_config) { + host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); + } + } + + // For redirection hosts, if the scheme is not http or https, set it to $scheme + if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) { + host.forward_scheme = "$scheme"; + } + + if (host.locations) { + //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); + origLocations = [].concat(host.locations); + locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { + host.locations = renderedLocations; + }); + + // Allow someone who is using / custom location path to use it, and skip the default / location + _.map(host.locations, (location) => { + if (location.path === "/") { + host.use_default_location = false; + } + }); + } else { + locationsPromise = Promise.resolve(); + } + + // Set the IPv6 setting for the host + host.ipv6 = internalNginx.ipv6Enabled(); + + locationsPromise.then(() => { + renderEngine + .parseAndRender(template, host) + .then((config_text) => { + fs.writeFileSync(filename, config_text, { encoding: "utf8" }); + debug(logger, "Wrote config:", filename, config_text); + + // Restore locations array + host.locations = origLocations; + + resolve(true); + }) + .catch((err) => { + debug(logger, `Could not write ${filename}:`, err.message); + reject(new errs.ConfigurationError(err.message)); + }); + }); + }); + }, + + /** + * This generates a temporary nginx config listening on port 80 for the domain names listed + * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt + * when requesting a certificate without having a hostname set up already. + * + * @param {Object} certificate + * @returns {Promise} + */ + generateLetsEncryptRequestConfig: (certificate) => { + debug(logger, "Generating LetsEncrypt Request Config:", certificate); + const renderEngine = utils.getRenderEngine(); + + return new Promise((resolve, reject) => { + let template = null; + const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; + + try { + template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" }); + } catch (err) { + reject(new errs.ConfigurationError(err.message)); + return; + } + + certificate.ipv6 = internalNginx.ipv6Enabled(); + + renderEngine + .parseAndRender(template, certificate) + .then((config_text) => { + fs.writeFileSync(filename, config_text, { encoding: "utf8" }); + debug(logger, "Wrote config:", filename, config_text); + resolve(true); + }) + .catch((err) => { + debug(logger, `Could not write ${filename}:`, err.message); + reject(new errs.ConfigurationError(err.message)); + }); + }); + }, + + /** + * A simple wrapper around unlinkSync that writes to the logger + * + * @param {String} filename + */ + deleteFile: (filename) => { + if (!fs.existsSync(filename)) { + return; + } + try { + debug(logger, `Deleting file: ${filename}`); + fs.unlinkSync(filename); + } catch (err) { + debug(logger, "Could not delete file:", JSON.stringify(err, null, 2)); + } + }, + + /** + * + * @param {String} host_type + * @returns String + */ + getFileFriendlyHostType: (host_type) => { + return host_type.replace(/-/g, "_"); + }, + + /** + * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig` + * + * @param {Object} certificate + * @returns {Promise} + */ + deleteLetsEncryptRequestConfig: (certificate) => { + const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; + return new Promise((resolve /*, reject*/) => { + internalNginx.deleteFile(config_file); + resolve(); + }); + }, + + /** + * @param {String} host_type + * @param {Object} [host] + * @param {Boolean} [delete_err_file] + * @returns {Promise} + */ + deleteConfig: (host_type, host, delete_err_file) => { + const config_file = internalNginx.getConfigName( + internalNginx.getFileFriendlyHostType(host_type), + typeof host === "undefined" ? 0 : host.id, + ); + const config_file_err = `${config_file}.err`; + + return new Promise((resolve /*, reject*/) => { + internalNginx.deleteFile(config_file); + if (delete_err_file) { + internalNginx.deleteFile(config_file_err); + } + resolve(); + }); + }, + + /** + * @param {String} host_type + * @param {Object} [host] + * @returns {Promise} + */ + renameConfigAsError: (host_type, host) => { + const config_file = internalNginx.getConfigName( + internalNginx.getFileFriendlyHostType(host_type), + typeof host === "undefined" ? 0 : host.id, + ); + const config_file_err = `${config_file}.err`; + + return new Promise((resolve /*, reject*/) => { + fs.unlink(config_file, () => { + // ignore result, continue + fs.rename(config_file, config_file_err, () => { + // also ignore result, as this is a debugging informative file anyway + resolve(); + }); + }); + }); + }, + + /** + * @param {String} hostType + * @param {Array} hosts + * @returns {Promise} + */ + bulkGenerateConfigs: (hostType, hosts) => { + const promises = []; + hosts.map((host) => { + promises.push(internalNginx.generateConfig(hostType, host)); + return true; + }); + + return Promise.all(promises); + }, + + /** + * @param {String} host_type + * @param {Array} hosts + * @returns {Promise} + */ + bulkDeleteConfigs: (host_type, hosts) => { + const promises = []; + hosts.map((host) => { + promises.push(internalNginx.deleteConfig(host_type, host, true)); + return true; + }); + + return Promise.all(promises); + }, + + /** + * @param {string} config + * @returns {boolean} + */ + advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im), + + /** + * @returns {boolean} + */ + ipv6Enabled: () => { + if (typeof process.env.DISABLE_IPV6 !== "undefined") { + const disabled = process.env.DISABLE_IPV6.toLowerCase(); + return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes"); + } + + return true; + }, +}; + +export default internalNginx; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js new file mode 100644 index 0000000..34475c9 --- /dev/null +++ b/backend/internal/proxy-host.js @@ -0,0 +1,474 @@ +import _ from "lodash"; +import errs from "../lib/error.js"; +import { castJsonIfNeed } from "../lib/helpers.js"; +import utils from "../lib/utils.js"; +import proxyHostModel from "../models/proxy_host.js"; +import internalAuditLog from "./audit-log.js"; +import internalCertificate from "./certificate.js"; +import internalHost from "./host.js"; +import internalNginx from "./nginx.js"; + +const omissions = () => { + return ["is_deleted", "owner.is_deleted"]; +}; + +const internalProxyHost = { + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + let thisData = data; + const createCertificate = thisData.certificate_id === "new"; + + if (createCertificate) { + delete thisData.certificate_id; + } + + return access + .can("proxy_hosts:create", thisData) + .then(() => { + // Get a list of the domain names and check each of them against existing records + const domain_name_check_promises = []; + + thisData.domain_names.map((domain_name) => { + domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); + return true; + }); + + return Promise.all(domain_name_check_promises).then((check_results) => { + check_results.map((result) => { + if (result.is_taken) { + throw new errs.ValidationError(`${result.hostname} is already in use`); + } + return true; + }); + }); + }) + .then(() => { + // At this point the domains should have been checked + thisData.owner_user_id = access.token.getUserId(1); + thisData = internalHost.cleanSslHstsData(thisData); + + // Fix for db field not having a default value + // for this optional field. + if (typeof thisData.advanced_config === "undefined") { + thisData.advanced_config = ""; + } + + return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions())); + }) + .then((row) => { + if (createCertificate) { + return internalCertificate + .createQuickCertificate(access, thisData) + .then((cert) => { + // update host with cert id + return internalProxyHost.update(access, { + id: row.id, + certificate_id: cert.id, + }); + }) + .then(() => { + return row; + }); + } + return row; + }) + .then((row) => { + // re-fetch with cert + return internalProxyHost.get(access, { + id: row.id, + expand: ["certificate", "owner", "access_list.[clients,items]"], + }); + }) + .then((row) => { + // Configure nginx + return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => { + return row; + }); + }) + .then((row) => { + // Audit log + thisData.meta = _.assign({}, thisData.meta || {}, row.meta); + + // Add to audit log + return internalAuditLog + .add(access, { + action: "created", + object_type: "proxy-host", + object_id: row.id, + meta: thisData, + }) + .then(() => { + return row; + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @return {Promise} + */ + update: (access, data) => { + let thisData = data; + const createCertificate = thisData.certificate_id === "new"; + + if (createCertificate) { + delete thisData.certificate_id; + } + + return access + .can("proxy_hosts:update", thisData.id) + .then((/*access_data*/) => { + // Get a list of the domain names and check each of them against existing records + const domain_name_check_promises = []; + + if (typeof thisData.domain_names !== "undefined") { + thisData.domain_names.map((domain_name) => { + return domain_name_check_promises.push( + internalHost.isHostnameTaken(domain_name, "proxy", thisData.id), + ); + }); + + return Promise.all(domain_name_check_promises).then((check_results) => { + check_results.map((result) => { + if (result.is_taken) { + throw new errs.ValidationError(`${result.hostname} is already in use`); + } + return true; + }); + }); + } + }) + .then(() => { + return internalProxyHost.get(access, { id: thisData.id }); + }) + .then((row) => { + if (row.id !== thisData.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`, + ); + } + + if (createCertificate) { + return internalCertificate + .createQuickCertificate(access, { + domain_names: thisData.domain_names || row.domain_names, + meta: _.assign({}, row.meta, thisData.meta), + }) + .then((cert) => { + // update host with cert id + thisData.certificate_id = cert.id; + }) + .then(() => { + return row; + }); + } + return row; + }) + .then((row) => { + // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. + thisData = _.assign( + {}, + { + domain_names: row.domain_names, + }, + data, + ); + + thisData = internalHost.cleanSslHstsData(thisData, row); + + return proxyHostModel + .query() + .where({ id: thisData.id }) + .patch(thisData) + .then(utils.omitRow(omissions())) + .then((saved_row) => { + // Add to audit log + return internalAuditLog + .add(access, { + action: "updated", + object_type: "proxy-host", + object_id: row.id, + meta: thisData, + }) + .then(() => { + return saved_row; + }); + }); + }) + .then(() => { + return internalProxyHost + .get(access, { + id: thisData.id, + expand: ["owner", "certificate", "access_list.[clients,items]"], + }) + .then((row) => { + if (!row.enabled) { + // No need to add nginx config if host is disabled + return row; + } + // Configure nginx + return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => { + row.meta = new_meta; + return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + }); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + const thisData = data || {}; + return access + .can("proxy_hosts:get", thisData.id) + .then((access_data) => { + const query = proxyHostModel + .query() + .where("is_deleted", 0) + .andWhere("id", thisData.id) + .allowGraph(proxyHostModel.defaultAllowGraph) + .first(); + + if (access_data.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { + query.withGraphFetched(`[${thisData.expand.join(", ")}]`); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(thisData.id); + } + const thisRow = internalHost.cleanRowCertificateMeta(row); + // Custom omissions + if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { + return _.omit(row, thisData.omit); + } + return thisRow; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: (access, data) => { + return access + .can("proxy_hosts:delete", data.id) + .then(() => { + return internalProxyHost.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + + return proxyHostModel + .query() + .where("id", row.id) + .patch({ + is_deleted: 1, + }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig("proxy_host", row).then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "deleted", + object_type: "proxy-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + enable: (access, data) => { + return access + .can("proxy_hosts:update", data.id) + .then(() => { + return internalProxyHost.get(access, { + id: data.id, + expand: ["certificate", "owner", "access_list"], + }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (row.enabled) { + throw new errs.ValidationError("Host is already enabled"); + } + + row.enabled = 1; + + return proxyHostModel + .query() + .where("id", row.id) + .patch({ + enabled: 1, + }) + .then(() => { + // Configure nginx + return internalNginx.configure(proxyHostModel, "proxy_host", row); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "enabled", + object_type: "proxy-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + disable: (access, data) => { + return access + .can("proxy_hosts:update", data.id) + .then(() => { + return internalProxyHost.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (!row.enabled) { + throw new errs.ValidationError("Host is already disabled"); + } + + row.enabled = 0; + + return proxyHostModel + .query() + .where("id", row.id) + .patch({ + enabled: 0, + }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig("proxy_host", row).then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "disabled", + object_type: "proxy-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: async (access, expand, searchQuery) => { + const accessData = await access.can("proxy_hosts:list"); + + const query = proxyHostModel + .query() + .where("is_deleted", 0) + .groupBy("id") + .allowGraph(proxyHostModel.defaultAllowGraph) + .orderBy(castJsonIfNeed("domain_names"), "ASC"); + + if (accessData.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof searchQuery === "string" && searchQuery.length > 0) { + query.where(function () { + this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + const rows = await query.then(utils.omitRows(omissions())); + if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { + return internalHost.cleanAllRowsCertificateMeta(rows); + } + return rows; + }, + + /** + * Report use + * + * @param {Number} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + const query = proxyHostModel.query().count("id as count").where("is_deleted", 0); + + if (visibility !== "all") { + query.andWhere("owner_user_id", user_id); + } + + return query.first().then((row) => { + return Number.parseInt(row.count, 10); + }); + }, +}; + +export default internalProxyHost; diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js new file mode 100644 index 0000000..5237859 --- /dev/null +++ b/backend/internal/redirection-host.js @@ -0,0 +1,477 @@ +import _ from "lodash"; +import errs from "../lib/error.js"; +import { castJsonIfNeed } from "../lib/helpers.js"; +import utils from "../lib/utils.js"; +import redirectionHostModel from "../models/redirection_host.js"; +import internalAuditLog from "./audit-log.js"; +import internalCertificate from "./certificate.js"; +import internalHost from "./host.js"; +import internalNginx from "./nginx.js"; + +const omissions = () => { + return ["is_deleted"]; +}; + +const internalRedirectionHost = { + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + let thisData = data || {}; + const createCertificate = thisData.certificate_id === "new"; + + if (createCertificate) { + delete thisData.certificate_id; + } + + return access + .can("redirection_hosts:create", thisData) + .then((/*access_data*/) => { + // Get a list of the domain names and check each of them against existing records + const domain_name_check_promises = []; + + thisData.domain_names.map((domain_name) => { + domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name)); + return true; + }); + + return Promise.all(domain_name_check_promises).then((check_results) => { + check_results.map((result) => { + if (result.is_taken) { + throw new errs.ValidationError(`${result.hostname} is already in use`); + } + return true; + }); + }); + }) + .then(() => { + // At this point the domains should have been checked + thisData.owner_user_id = access.token.getUserId(1); + thisData = internalHost.cleanSslHstsData(thisData); + + // Fix for db field not having a default value + // for this optional field. + if (typeof data.advanced_config === "undefined") { + data.advanced_config = ""; + } + + return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions())); + }) + .then((row) => { + if (createCertificate) { + return internalCertificate + .createQuickCertificate(access, thisData) + .then((cert) => { + // update host with cert id + return internalRedirectionHost.update(access, { + id: row.id, + certificate_id: cert.id, + }); + }) + .then(() => { + return row; + }); + } + return row; + }) + .then((row) => { + // re-fetch with cert + return internalRedirectionHost.get(access, { + id: row.id, + expand: ["certificate", "owner"], + }); + }) + .then((row) => { + // Configure nginx + return internalNginx.configure(redirectionHostModel, "redirection_host", row).then(() => { + return row; + }); + }) + .then((row) => { + thisData.meta = _.assign({}, thisData.meta || {}, row.meta); + + // Add to audit log + return internalAuditLog + .add(access, { + action: "created", + object_type: "redirection-host", + object_id: row.id, + meta: thisData, + }) + .then(() => { + return row; + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @return {Promise} + */ + update: (access, data) => { + let thisData = data || {}; + const createCertificate = thisData.certificate_id === "new"; + + if (createCertificate) { + delete thisData.certificate_id; + } + + return access + .can("redirection_hosts:update", thisData.id) + .then((/*access_data*/) => { + // Get a list of the domain names and check each of them against existing records + const domain_name_check_promises = []; + + if (typeof thisData.domain_names !== "undefined") { + thisData.domain_names.map((domain_name) => { + domain_name_check_promises.push( + internalHost.isHostnameTaken(domain_name, "redirection", thisData.id), + ); + return true; + }); + + return Promise.all(domain_name_check_promises).then((check_results) => { + check_results.map((result) => { + if (result.is_taken) { + throw new errs.ValidationError(`${result.hostname} is already in use`); + } + return true; + }); + }); + } + }) + .then(() => { + return internalRedirectionHost.get(access, { id: thisData.id }); + }) + .then((row) => { + if (row.id !== thisData.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`, + ); + } + + if (createCertificate) { + return internalCertificate + .createQuickCertificate(access, { + domain_names: thisData.domain_names || row.domain_names, + meta: _.assign({}, row.meta, thisData.meta), + }) + .then((cert) => { + // update host with cert id + thisData.certificate_id = cert.id; + }) + .then(() => { + return row; + }); + } + return row; + }) + .then((row) => { + // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. + thisData = _.assign( + {}, + { + domain_names: row.domain_names, + }, + thisData, + ); + + thisData = internalHost.cleanSslHstsData(thisData, row); + + return redirectionHostModel + .query() + .where({ id: thisData.id }) + .patch(thisData) + .then((saved_row) => { + // Add to audit log + return internalAuditLog + .add(access, { + action: "updated", + object_type: "redirection-host", + object_id: row.id, + meta: thisData, + }) + .then(() => { + return _.omit(saved_row, omissions()); + }); + }); + }) + .then(() => { + return internalRedirectionHost + .get(access, { + id: thisData.id, + expand: ["owner", "certificate"], + }) + .then((row) => { + // Configure nginx + return internalNginx + .configure(redirectionHostModel, "redirection_host", row) + .then((new_meta) => { + row.meta = new_meta; + return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + }); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + const thisData = data || {}; + return access + .can("redirection_hosts:get", thisData.id) + .then((access_data) => { + const query = redirectionHostModel + .query() + .where("is_deleted", 0) + .andWhere("id", thisData.id) + .allowGraph(redirectionHostModel.defaultAllowGraph) + .first(); + + if (access_data.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { + query.withGraphFetched(`[${thisData.expand.join(", ")}]`); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + let thisRow = row; + if (!thisRow || !thisRow.id) { + throw new errs.ItemNotFoundError(thisData.id); + } + thisRow = internalHost.cleanRowCertificateMeta(thisRow); + // Custom omissions + if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { + return _.omit(thisRow, thisData.omit); + } + return thisRow; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: (access, data) => { + return access + .can("redirection_hosts:delete", data.id) + .then(() => { + return internalRedirectionHost.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + + return redirectionHostModel + .query() + .where("id", row.id) + .patch({ + is_deleted: 1, + }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig("redirection_host", row).then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "deleted", + object_type: "redirection-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + enable: (access, data) => { + return access + .can("redirection_hosts:update", data.id) + .then(() => { + return internalRedirectionHost.get(access, { + id: data.id, + expand: ["certificate", "owner"], + }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (row.enabled) { + throw new errs.ValidationError("Host is already enabled"); + } + + row.enabled = 1; + + return redirectionHostModel + .query() + .where("id", row.id) + .patch({ + enabled: 1, + }) + .then(() => { + // Configure nginx + return internalNginx.configure(redirectionHostModel, "redirection_host", row); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "enabled", + object_type: "redirection-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + disable: (access, data) => { + return access + .can("redirection_hosts:update", data.id) + .then(() => { + return internalRedirectionHost.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (!row.enabled) { + throw new errs.ValidationError("Host is already disabled"); + } + + row.enabled = 0; + + return redirectionHostModel + .query() + .where("id", row.id) + .patch({ + enabled: 0, + }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig("redirection_host", row).then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "disabled", + object_type: "redirection-host", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All Hosts + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access + .can("redirection_hosts:list") + .then((access_data) => { + const query = redirectionHostModel + .query() + .where("is_deleted", 0) + .groupBy("id") + .allowGraph(redirectionHostModel.defaultAllowGraph) + .orderBy(castJsonIfNeed("domain_names"), "ASC"); + + if (access_data.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof search_query === "string" && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + return query.then(utils.omitRows(omissions())); + }) + .then((rows) => { + if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { + return internalHost.cleanAllRowsCertificateMeta(rows); + } + + return rows; + }); + }, + + /** + * Report use + * + * @param {Number} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + const query = redirectionHostModel.query().count("id as count").where("is_deleted", 0); + + if (visibility !== "all") { + query.andWhere("owner_user_id", user_id); + } + + return query.first().then((row) => { + return Number.parseInt(row.count, 10); + }); + }, +}; + +export default internalRedirectionHost; diff --git a/backend/internal/remote-version.js b/backend/internal/remote-version.js new file mode 100644 index 0000000..dd9c927 --- /dev/null +++ b/backend/internal/remote-version.js @@ -0,0 +1,84 @@ +import https from "node:https"; +import { ProxyAgent } from "proxy-agent"; +import { debug, remoteVersion as logger } from "../logger.js"; +import pjson from "../package.json" with { type: "json" }; + +const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest"; + +const internalRemoteVersion = { + cache_timeout: 1000 * 60 * 15, // 15 minutes + last_result: null, + last_fetch_time: null, + + /** + * Fetch the latest version info, using a cached result if within the cache timeout period. + * @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info + */ + get: async () => { + if ( + !internalRemoteVersion.last_result || + !internalRemoteVersion.last_fetch_time || + Date.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout + ) { + const raw = await internalRemoteVersion.fetchUrl(VERSION_URL); + const data = JSON.parse(raw); + internalRemoteVersion.last_result = data; + internalRemoteVersion.last_fetch_time = Date.now(); + } else { + debug(logger, "Using cached remote version result"); + } + + const latestVersion = internalRemoteVersion.last_result.tag_name; + const version = pjson.version.split("-").shift().split("."); + const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`; + return { + current: currentVersion, + latest: latestVersion, + update_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion), + }; + }, + + fetchUrl: (url) => { + const agent = new ProxyAgent(); + const headers = { + "User-Agent": `NginxProxyManager v${pjson.version}`, + }; + + return new Promise((resolve, reject) => { + logger.info(`Fetching ${url}`); + return https + .get(url, { agent, headers }, (res) => { + res.setEncoding("utf8"); + let raw_data = ""; + res.on("data", (chunk) => { + raw_data += chunk; + }); + res.on("end", () => { + resolve(raw_data); + }); + }) + .on("error", (err) => { + reject(err); + }); + }); + }, + + compareVersions: (current, latest) => { + const cleanCurrent = current.replace(/^v/, ""); + const cleanLatest = latest.replace(/^v/, ""); + + const currentParts = cleanCurrent.split(".").map(Number); + const latestParts = cleanLatest.split(".").map(Number); + + for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { + const curr = currentParts[i] || 0; + const lat = latestParts[i] || 0; + + if (lat > curr) return true; + if (lat < curr) return false; + } + return false; + }, +}; + +export default internalRemoteVersion; diff --git a/backend/internal/report.js b/backend/internal/report.js new file mode 100644 index 0000000..59f13fe --- /dev/null +++ b/backend/internal/report.js @@ -0,0 +1,37 @@ +import internalDeadHost from "./dead-host.js"; +import internalProxyHost from "./proxy-host.js"; +import internalRedirectionHost from "./redirection-host.js"; +import internalStream from "./stream.js"; + +const internalReport = { + /** + * @param {Access} access + * @return {Promise} + */ + getHostsReport: (access) => { + return access + .can("reports:hosts", 1) + .then((access_data) => { + const userId = access.token.getUserId(1); + + const promises = [ + internalProxyHost.getCount(userId, access_data.permission_visibility), + internalRedirectionHost.getCount(userId, access_data.permission_visibility), + internalStream.getCount(userId, access_data.permission_visibility), + internalDeadHost.getCount(userId, access_data.permission_visibility), + ]; + + return Promise.all(promises); + }) + .then((counts) => { + return { + proxy: counts.shift(), + redirection: counts.shift(), + stream: counts.shift(), + dead: counts.shift(), + }; + }); + }, +}; + +export default internalReport; diff --git a/backend/internal/setting.js b/backend/internal/setting.js new file mode 100644 index 0000000..f8fc711 --- /dev/null +++ b/backend/internal/setting.js @@ -0,0 +1,125 @@ +import fs from "node:fs"; +import errs from "../lib/error.js"; +import settingModel from "../models/setting.js"; +import internalNginx from "./nginx.js"; + +const internalSetting = { + /** + * @param {Access} access + * @param {Object} data + * @param {String} data.id + * @return {Promise} + */ + update: (access, data) => { + return access + .can("settings:update", data.id) + .then((/*access_data*/) => { + return internalSetting.get(access, { id: data.id }); + }) + .then((row) => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`, + ); + } + + return settingModel.query().where({ id: data.id }).patch(data); + }) + .then(() => { + return internalSetting.get(access, { + id: data.id, + }); + }) + .then((row) => { + if (row.id === "default-site") { + // write the html if we need to + if (row.value === "html") { + fs.writeFileSync("/data/nginx/default_www/index.html", row.meta.html, { encoding: "utf8" }); + } + + // Configure nginx + return internalNginx + .deleteConfig("default") + .then(() => { + return internalNginx.generateConfig("default", row); + }) + .then(() => { + return internalNginx.test(); + }) + .then(() => { + return internalNginx.reload(); + }) + .then(() => { + return row; + }) + .catch((/*err*/) => { + internalNginx + .deleteConfig("default") + .then(() => { + return internalNginx.test(); + }) + .then(() => { + return internalNginx.reload(); + }) + .then(() => { + // I'm being slack here I know.. + throw new errs.ValidationError("Could not reconfigure Nginx. Please check logs."); + }); + }); + } + return row; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {String} data.id + * @return {Promise} + */ + get: (access, data) => { + return access + .can("settings:get", data.id) + .then(() => { + return settingModel.query().where("id", data.id).first(); + }) + .then((row) => { + if (row) { + return row; + } + throw new errs.ItemNotFoundError(data.id); + }); + }, + + /** + * This will only count the settings + * + * @param {Access} access + * @returns {*} + */ + getCount: (access) => { + return access + .can("settings:list") + .then(() => { + return settingModel.query().count("id as count").first(); + }) + .then((row) => { + return Number.parseInt(row.count, 10); + }); + }, + + /** + * All settings + * + * @param {Access} access + * @returns {Promise} + */ + getAll: (access) => { + return access.can("settings:list").then(() => { + return settingModel.query().orderBy("description", "ASC"); + }); + }, +}; + +export default internalSetting; diff --git a/backend/internal/stream.js b/backend/internal/stream.js new file mode 100644 index 0000000..909c92f --- /dev/null +++ b/backend/internal/stream.js @@ -0,0 +1,426 @@ +import _ from "lodash"; +import errs from "../lib/error.js"; +import { castJsonIfNeed } from "../lib/helpers.js"; +import utils from "../lib/utils.js"; +import streamModel from "../models/stream.js"; +import internalAuditLog from "./audit-log.js"; +import internalCertificate from "./certificate.js"; +import internalHost from "./host.js"; +import internalNginx from "./nginx.js"; + +const omissions = () => { + return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"]; +}; + +const internalStream = { + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + const create_certificate = data.certificate_id === "new"; + + if (create_certificate) { + delete data.certificate_id; + } + + return access + .can("streams:create", data) + .then((/*access_data*/) => { + // TODO: At this point the existing ports should have been checked + data.owner_user_id = access.token.getUserId(1); + + if (typeof data.meta === "undefined") { + data.meta = {}; + } + + // streams aren't routed by domain name so don't store domain names in the DB + const data_no_domains = structuredClone(data); + delete data_no_domains.domain_names; + + return streamModel.query().insertAndFetch(data_no_domains).then(utils.omitRow(omissions())); + }) + .then((row) => { + if (create_certificate) { + return internalCertificate + .createQuickCertificate(access, data) + .then((cert) => { + // update host with cert id + return internalStream.update(access, { + id: row.id, + certificate_id: cert.id, + }); + }) + .then(() => { + return row; + }); + } + return row; + }) + .then((row) => { + // re-fetch with cert + return internalStream.get(access, { + id: row.id, + expand: ["certificate", "owner"], + }); + }) + .then((row) => { + // Configure nginx + return internalNginx.configure(streamModel, "stream", row).then(() => { + return row; + }); + }) + .then((row) => { + // Add to audit log + return internalAuditLog + .add(access, { + action: "created", + object_type: "stream", + object_id: row.id, + meta: data, + }) + .then(() => { + return row; + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @return {Promise} + */ + update: (access, data) => { + let thisData = data; + const create_certificate = thisData.certificate_id === "new"; + + if (create_certificate) { + delete thisData.certificate_id; + } + + return access + .can("streams:update", thisData.id) + .then((/*access_data*/) => { + // TODO: at this point the existing streams should have been checked + return internalStream.get(access, { id: thisData.id }); + }) + .then((row) => { + if (row.id !== thisData.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`, + ); + } + + if (create_certificate) { + return internalCertificate + .createQuickCertificate(access, { + domain_names: thisData.domain_names || row.domain_names, + meta: _.assign({}, row.meta, thisData.meta), + }) + .then((cert) => { + // update host with cert id + thisData.certificate_id = cert.id; + }) + .then(() => { + return row; + }); + } + return row; + }) + .then((row) => { + // Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here. + thisData = _.assign( + {}, + { + domain_names: row.domain_names, + }, + thisData, + ); + + return streamModel + .query() + .patchAndFetchById(row.id, thisData) + .then(utils.omitRow(omissions())) + .then((saved_row) => { + // Add to audit log + return internalAuditLog + .add(access, { + action: "updated", + object_type: "stream", + object_id: row.id, + meta: thisData, + }) + .then(() => { + return saved_row; + }); + }); + }) + .then(() => { + return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate"] }).then((row) => { + return internalNginx.configure(streamModel, "stream", row).then((new_meta) => { + row.meta = new_meta; + return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); + }); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + const thisData = data || {}; + return access + .can("streams:get", thisData.id) + .then((access_data) => { + const query = streamModel + .query() + .where("is_deleted", 0) + .andWhere("id", thisData.id) + .allowGraph(streamModel.defaultAllowGraph) + .first(); + + if (access_data.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { + query.withGraphFetched(`[${thisData.expand.join(", ")}]`); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + let thisRow = row; + if (!thisRow || !thisRow.id) { + throw new errs.ItemNotFoundError(thisData.id); + } + thisRow = internalHost.cleanRowCertificateMeta(thisRow); + // Custom omissions + if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { + return _.omit(thisRow, thisData.omit); + } + return thisRow; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: (access, data) => { + return access + .can("streams:delete", data.id) + .then(() => { + return internalStream.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + + return streamModel + .query() + .where("id", row.id) + .patch({ + is_deleted: 1, + }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig("stream", row).then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "deleted", + object_type: "stream", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + enable: (access, data) => { + return access + .can("streams:update", data.id) + .then(() => { + return internalStream.get(access, { + id: data.id, + expand: ["certificate", "owner"], + }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (row.enabled) { + throw new errs.ValidationError("Stream is already enabled"); + } + + row.enabled = 1; + + return streamModel + .query() + .where("id", row.id) + .patch({ + enabled: 1, + }) + .then(() => { + // Configure nginx + return internalNginx.configure(streamModel, "stream", row); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "enabled", + object_type: "stream", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Number} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + disable: (access, data) => { + return access + .can("streams:update", data.id) + .then(() => { + return internalStream.get(access, { id: data.id }); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(data.id); + } + if (!row.enabled) { + throw new errs.ValidationError("Stream is already disabled"); + } + + row.enabled = 0; + + return streamModel + .query() + .where("id", row.id) + .patch({ + enabled: 0, + }) + .then(() => { + // Delete Nginx Config + return internalNginx.deleteConfig("stream", row).then(() => { + return internalNginx.reload(); + }); + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "disabled", + object_type: "stream", + object_id: row.id, + meta: _.omit(row, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * All Streams + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: (access, expand, search_query) => { + return access + .can("streams:list") + .then((access_data) => { + const query = streamModel + .query() + .where("is_deleted", 0) + .groupBy("id") + .allowGraph(streamModel.defaultAllowGraph) + .orderBy("incoming_port", "ASC"); + + if (access_data.permission_visibility !== "all") { + query.andWhere("owner_user_id", access.token.getUserId(1)); + } + + // Query is used for searching + if (typeof search_query === "string" && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed("incoming_port"), "like", `%${search_query}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + return query.then(utils.omitRows(omissions())); + }) + .then((rows) => { + if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) { + return internalHost.cleanAllRowsCertificateMeta(rows); + } + + return rows; + }); + }, + + /** + * Report use + * + * @param {Number} user_id + * @param {String} visibility + * @returns {Promise} + */ + getCount: (user_id, visibility) => { + const query = streamModel.query().count("id AS count").where("is_deleted", 0); + + if (visibility !== "all") { + query.andWhere("owner_user_id", user_id); + } + + return query.first().then((row) => { + return Number.parseInt(row.count, 10); + }); + }, +}; + +export default internalStream; diff --git a/backend/internal/token.js b/backend/internal/token.js new file mode 100644 index 0000000..126283e --- /dev/null +++ b/backend/internal/token.js @@ -0,0 +1,237 @@ +import _ from "lodash"; +import errs from "../lib/error.js"; +import { parseDatePeriod } from "../lib/helpers.js"; +import authModel from "../models/auth.js"; +import TokenModel from "../models/token.js"; +import userModel from "../models/user.js"; +import twoFactor from "./2fa.js"; + +const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password"; +const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth"; +const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code"; +const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa"; + +export default { + /** + * @param {Object} data + * @param {String} data.identity + * @param {String} data.secret + * @param {String} [data.scope] + * @param {String} [data.expiry] + * @param {String} [issuer] + * @returns {Promise} + */ + getTokenFromEmail: async (data, issuer) => { + const Token = TokenModel(); + + data.scope = data.scope || "user"; + data.expiry = data.expiry || "1d"; + + const user = await userModel + .query() + .where("email", data.identity.toLowerCase().trim()) + .andWhere("is_deleted", 0) + .andWhere("is_disabled", 0) + .first(); + + if (!user) { + throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); + } + + const auth = await authModel + .query() + .where("user_id", "=", user.id) + .where("type", "=", "password") + .first(); + + if (!auth) { + throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); + } + + const valid = await auth.verifyPassword(data.secret); + if (!valid) { + throw new errs.AuthError( + ERROR_MESSAGE_INVALID_AUTH, + ERROR_MESSAGE_INVALID_AUTH_I18N, + ); + } + + if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) { + // The scope requested doesn't exist as a role against the user, + // you shall not pass. + throw new errs.AuthError(`Invalid scope: ${data.scope}`); + } + + // Check if 2FA is enabled + const has2FA = await twoFactor.isEnabled(user.id); + if (has2FA) { + // Return challenge token instead of full token + const challengeToken = await Token.create({ + iss: issuer || "api", + attrs: { + id: user.id, + }, + scope: ["2fa-challenge"], + expiresIn: "5m", + }); + + return { + requires_2fa: true, + challenge_token: challengeToken.token, + }; + } + + // Create a moment of the expiry expression + const expiry = parseDatePeriod(data.expiry); + if (expiry === null) { + throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`); + } + + const signed = await Token.create({ + iss: issuer || "api", + attrs: { + id: user.id, + }, + scope: [data.scope], + expiresIn: data.expiry, + }); + + return { + token: signed.token, + expires: expiry.toISOString(), + }; + }, + + /** + * @param {Access} access + * @param {Object} [data] + * @param {String} [data.expiry] + * @param {String} [data.scope] Only considered if existing token scope is admin + * @returns {Promise} + */ + getFreshToken: async (access, data) => { + const Token = TokenModel(); + const thisData = data || {}; + + thisData.expiry = thisData.expiry || "1d"; + + if (access?.token.getUserId(0)) { + // Create a moment of the expiry expression + const expiry = parseDatePeriod(thisData.expiry); + if (expiry === null) { + throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`); + } + + const token_attrs = { + id: access.token.getUserId(0), + }; + + // Only admins can request otherwise scoped tokens + let scope = access.token.get("scope"); + if (thisData.scope && access.token.hasScope("admin")) { + scope = [thisData.scope]; + + if (thisData.scope === "job-board" || thisData.scope === "worker") { + token_attrs.id = 0; + } + } + + const signed = await Token.create({ + iss: "api", + scope: scope, + attrs: token_attrs, + expiresIn: thisData.expiry, + }); + + return { + token: signed.token, + expires: expiry.toISOString(), + }; + } + throw new error.AssertionFailedError("Existing token contained invalid user data"); + }, + + /** + * Verify 2FA code and return full token + * @param {string} challengeToken + * @param {string} code + * @param {string} [expiry] + * @returns {Promise} + */ + verify2FA: async (challengeToken, code, expiry) => { + const Token = TokenModel(); + const tokenExpiry = expiry || "1d"; + + // Verify challenge token + let tokenData; + try { + tokenData = await Token.load(challengeToken); + } catch { + throw new errs.AuthError("Invalid or expired challenge token"); + } + + // Check scope + if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") { + throw new errs.AuthError("Invalid challenge token"); + } + + const userId = tokenData.attrs?.id; + if (!userId) { + throw new errs.AuthError("Invalid challenge token"); + } + + // Verify 2FA code + const valid = await twoFactor.verifyForLogin(userId, code); + if (!valid) { + throw new errs.AuthError( + ERROR_MESSAGE_INVALID_2FA, + ERROR_MESSAGE_INVALID_2FA_I18N, + ); + } + + // Create full token + const expiryDate = parseDatePeriod(tokenExpiry); + if (expiryDate === null) { + throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`); + } + + const signed = await Token.create({ + iss: "api", + attrs: { + id: userId, + }, + scope: ["user"], + expiresIn: tokenExpiry, + }); + + return { + token: signed.token, + expires: expiryDate.toISOString(), + }; + }, + + /** + * @param {Object} user + * @returns {Promise} + */ + getTokenFromUser: async (user) => { + const expire = "1d"; + const Token = TokenModel(); + const expiry = parseDatePeriod(expire); + + const signed = await Token.create({ + iss: "api", + attrs: { + id: user.id, + }, + scope: ["user"], + expiresIn: expire, + }); + + return { + token: signed.token, + expires: expiry.toISOString(), + user: user, + }; + }, +}; diff --git a/backend/internal/user.js b/backend/internal/user.js new file mode 100644 index 0000000..d13931d --- /dev/null +++ b/backend/internal/user.js @@ -0,0 +1,494 @@ +import gravatar from "gravatar"; +import _ from "lodash"; +import errs from "../lib/error.js"; +import utils from "../lib/utils.js"; +import authModel from "../models/auth.js"; +import userModel from "../models/user.js"; +import userPermissionModel from "../models/user_permission.js"; +import internalAuditLog from "./audit-log.js"; +import internalToken from "./token.js"; + +const omissions = () => { + return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"]; +}; + +const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" }); + +const internalUser = { + /** + * Create a user can happen unauthenticated only once and only when no active users exist. + * Otherwise, a valid auth method is required. + * + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: async (access, data) => { + const auth = data.auth || null; + delete data.auth; + + data.avatar = data.avatar || ""; + data.roles = data.roles || []; + + if (typeof data.is_disabled !== "undefined") { + data.is_disabled = data.is_disabled ? 1 : 0; + } + + await access.can("users:create", data); + data.avatar = gravatar.url(data.email, { default: "mm" }); + + let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); + if (auth) { + user = await authModel.query().insert({ + user_id: user.id, + type: auth.type, + secret: auth.secret, + meta: {}, + }); + } + + // Create permissions row as well + const isAdmin = data.roles.indexOf("admin") !== -1; + + await userPermissionModel.query().insert({ + user_id: user.id, + visibility: isAdmin ? "all" : "user", + proxy_hosts: "manage", + redirection_hosts: "manage", + dead_hosts: "manage", + streams: "manage", + access_lists: "manage", + certificates: "manage", + }); + + user = await internalUser.get(access, { id: user.id, expand: ["permissions"] }); + + await internalAuditLog.add(access, { + action: "created", + object_type: "user", + object_id: user.id, + meta: user, + }); + + return user; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.email] + * @param {String} [data.name] + * @return {Promise} + */ + update: (access, data) => { + if (typeof data.is_disabled !== "undefined") { + data.is_disabled = data.is_disabled ? 1 : 0; + } + + return access + .can("users:update", data.id) + .then(() => { + // Make sure that the user being updated doesn't change their email to another user that is already using it + // 1. get user we want to update + return internalUser.get(access, { id: data.id }).then((user) => { + // 2. if email is to be changed, find other users with that email + if (typeof data.email !== "undefined") { + data.email = data.email.toLowerCase().trim(); + + if (user.email !== data.email) { + return internalUser.isEmailAvailable(data.email, data.id).then((available) => { + if (!available) { + throw new errs.ValidationError(`Email address already in use - ${data.email}`); + } + return user; + }); + } + } + + // No change to email: + return user; + }); + }) + .then((user) => { + if (user.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `User could not be updated, IDs do not match: ${user.id} !== ${data.id}`, + ); + } + + data.avatar = gravatar.url(data.email || user.email, { default: "mm" }); + return userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions())); + }) + .then(() => { + return internalUser.get(access, { id: data.id }); + }) + .then((user) => { + // Add to audit log + return internalAuditLog + .add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { ...data, id: user.id, name: user.name }, + }) + .then(() => { + return user; + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} [data] + * @param {Integer} [data.id] Defaults to the token user + * @param {Array} [data.expand] + * @param {Array} [data.omit] + * @return {Promise} + */ + get: (access, data) => { + const thisData = data || {}; + + if (typeof thisData.id === "undefined" || !thisData.id) { + thisData.id = access.token.getUserId(0); + } + + return access + .can("users:get", thisData.id) + .then(() => { + const query = userModel + .query() + .where("is_deleted", 0) + .andWhere("id", thisData.id) + .allowGraph("[permissions]") + .first(); + + if (typeof thisData.expand !== "undefined" && thisData.expand !== null) { + query.withGraphFetched(`[${thisData.expand.join(", ")}]`); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new errs.ItemNotFoundError(thisData.id); + } + // Custom omissions + if (typeof thisData.omit !== "undefined" && thisData.omit !== null) { + return _.omit(row, thisData.omit); + } + + if (row.avatar === "") { + row.avatar = DEFAULT_AVATAR; + } + + return row; + }); + }, + + /** + * Checks if an email address is available, but if a user_id is supplied, it will ignore checking + * against that user. + * + * @param email + * @param user_id + */ + isEmailAvailable: (email, user_id) => { + const query = userModel.query().where("email", "=", email.toLowerCase().trim()).where("is_deleted", 0).first(); + + if (typeof user_id !== "undefined") { + query.where("id", "!=", user_id); + } + + return query.then((user) => { + return !user; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.reason] + * @returns {Promise} + */ + delete: (access, data) => { + return access + .can("users:delete", data.id) + .then(() => { + return internalUser.get(access, { id: data.id }); + }) + .then((user) => { + if (!user) { + throw new errs.ItemNotFoundError(data.id); + } + + // Make sure user can't delete themselves + if (user.id === access.token.getUserId(0)) { + throw new errs.PermissionError("You cannot delete yourself."); + } + + return userModel + .query() + .where("id", user.id) + .patch({ + is_deleted: 1, + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: "deleted", + object_type: "user", + object_id: user.id, + meta: _.omit(user, omissions()), + }); + }); + }) + .then(() => { + return true; + }); + }, + + deleteAll: async () => { + await userModel + .query() + .patch({ + is_deleted: 1, + }); + }, + + /** + * This will only count the users + * + * @param {Access} access + * @param {String} [search_query] + * @returns {*} + */ + getCount: (access, search_query) => { + return access + .can("users:list") + .then(() => { + const query = userModel.query().count("id as count").where("is_deleted", 0).first(); + + // Query is used for searching + if (typeof search_query === "string") { + query.where(function () { + this.where("user.name", "like", `%${search_query}%`).orWhere( + "user.email", + "like", + `%${search_query}%`, + ); + }); + } + + return query; + }) + .then((row) => { + return Number.parseInt(row.count, 10); + }); + }, + + /** + * All users + * + * @param {Access} access + * @param {Array} [expand] + * @param {String} [search_query] + * @returns {Promise} + */ + getAll: async (access, expand, search_query) => { + await access.can("users:list"); + const query = userModel + .query() + .where("is_deleted", 0) + .groupBy("id") + .allowGraph("[permissions]") + .orderBy("name", "ASC"); + + // Query is used for searching + if (typeof search_query === "string") { + query.where(function () { + this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`); + }); + } + + if (typeof expand !== "undefined" && expand !== null) { + query.withGraphFetched(`[${expand.join(", ")}]`); + } + + const res = await query; + return utils.omitRows(omissions())(res); + }, + + /** + * @param {Access} access + * @param {Integer} [id_requested] + * @returns {[String]} + */ + getUserOmisionsByAccess: (access, idRequested) => { + let response = []; // Admin response + + if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) { + response = ["is_deleted"]; // Restricted response + } + + return response; + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} data.type + * @param {String} data.secret + * @return {Promise} + */ + setPassword: (access, data) => { + return access + .can("users:password", data.id) + .then(() => { + return internalUser.get(access, { id: data.id }); + }) + .then((user) => { + if (user.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `User could not be updated, IDs do not match: ${user.id} !== ${data.id}`, + ); + } + + if (user.id === access.token.getUserId(0)) { + // they're setting their own password. Make sure their current password is correct + if (typeof data.current === "undefined" || !data.current) { + throw new errs.ValidationError("Current password was not supplied"); + } + + return internalToken + .getTokenFromEmail({ + identity: user.email, + secret: data.current, + }) + .then(() => { + return user; + }); + } + + return user; + }) + .then((user) => { + // Get auth, patch if it exists + return authModel + .query() + .where("user_id", user.id) + .andWhere("type", data.type) + .first() + .then((existing_auth) => { + if (existing_auth) { + // patch + return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({ + type: data.type, // This is required for the model to encrypt on save + secret: data.secret, + }); + } + // insert + return authModel.query().insert({ + user_id: user.id, + type: data.type, + secret: data.secret, + meta: {}, + }); + }) + .then(() => { + // Add to Audit Log + return internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { + name: user.name, + password_changed: true, + auth_type: data.type, + }, + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @return {Promise} + */ + setPermissions: (access, data) => { + return access + .can("users:permissions", data.id) + .then(() => { + return internalUser.get(access, { id: data.id }); + }) + .then((user) => { + if (user.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new errs.InternalValidationError( + `User could not be updated, IDs do not match: ${user.id} !== ${data.id}`, + ); + } + + return user; + }) + .then((user) => { + // Get perms row, patch if it exists + return userPermissionModel + .query() + .where("user_id", user.id) + .first() + .then((existing_auth) => { + if (existing_auth) { + // patch + return userPermissionModel + .query() + .where("user_id", user.id) + .patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data)); + } + // insert + return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data)); + }) + .then((permissions) => { + // Add to Audit Log + return internalAuditLog.add(access, { + action: "updated", + object_type: "user", + object_id: user.id, + meta: { + name: user.name, + permissions: permissions, + }, + }); + }); + }) + .then(() => { + return true; + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + */ + loginAs: (access, data) => { + return access + .can("users:loginas", data.id) + .then(() => { + return internalUser.get(access, data); + }) + .then((user) => { + return internalToken.getTokenFromUser(user); + }); + }, +}; + +export default internalUser; diff --git a/backend/internal/wireguard.js b/backend/internal/wireguard.js new file mode 100644 index 0000000..f7b8fc0 --- /dev/null +++ b/backend/internal/wireguard.js @@ -0,0 +1,366 @@ +import fs from "fs"; +import { global as logger } from "../logger.js"; +import * as wgHelpers from "../lib/wg-helpers.js"; + +const WG_INTERFACE_NAME = process.env.WG_INTERFACE_NAME || "wg0"; +const WG_DEFAULT_PORT = Number.parseInt(process.env.WG_PORT || "51820", 10); +const WG_DEFAULT_MTU = Number.parseInt(process.env.WG_MTU || "1420", 10); +const WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || "10.8.0.0/24"; +const WG_DEFAULT_DNS = process.env.WG_DNS || "1.1.1.1, 8.8.8.8"; +const WG_HOST = process.env.WG_HOST || ""; +const WG_DEFAULT_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || "0.0.0.0/0, ::/0"; +const WG_DEFAULT_PERSISTENT_KEEPALIVE = Number.parseInt(process.env.WG_PERSISTENT_KEEPALIVE || "25", 10); +const WG_CONFIG_DIR = "/etc/wireguard"; + +let cronTimer = null; + +const internalWireguard = { + + /** + * Get or create the WireGuard interface in DB + */ + async getOrCreateInterface(knex) { + let iface = await knex("wg_interface").first(); + if (!iface) { + // Generate keys + const privateKey = await wgHelpers.generatePrivateKey(); + const publicKey = await wgHelpers.getPublicKey(privateKey); + + const [id] = await knex("wg_interface").insert({ + name: WG_INTERFACE_NAME, + private_key: privateKey, + public_key: publicKey, + ipv4_cidr: WG_DEFAULT_ADDRESS, + listen_port: WG_DEFAULT_PORT, + mtu: WG_DEFAULT_MTU, + dns: WG_DEFAULT_DNS, + host: WG_HOST, + post_up: `iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE`, + post_down: `iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE`, + created_on: knex.fn.now(), + modified_on: knex.fn.now(), + }); + + iface = await knex("wg_interface").where("id", id).first(); + logger.info("WireGuard interface created with new keypair"); + } + return iface; + }, + + /** + * Save WireGuard config to /etc/wireguard/wg0.conf and sync + */ + async saveConfig(knex) { + const iface = await this.getOrCreateInterface(knex); + const clients = await knex("wg_client").where("enabled", true); + + // Generate server interface section + const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr); + const serverAddress = `${parsed.firstHost}/${parsed.prefix}`; + + let configContent = wgHelpers.generateServerInterface({ + privateKey: iface.private_key, + address: serverAddress, + listenPort: iface.listen_port, + mtu: iface.mtu, + dns: null, // DNS is for clients, not server + postUp: iface.post_up, + postDown: iface.post_down, + }); + + // Generate peer sections for each enabled client + for (const client of clients) { + configContent += "\n\n" + wgHelpers.generateServerPeer({ + publicKey: client.public_key, + preSharedKey: client.pre_shared_key, + allowedIps: `${client.ipv4_address}/32`, + }); + } + + configContent += "\n"; + + // Write config file + const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`; + fs.writeFileSync(configPath, configContent, { mode: 0o600 }); + logger.info(`WireGuard config saved to ${configPath}`); + + // Sync config + try { + await wgHelpers.wgSync(iface.name); + logger.info("WireGuard config synced"); + } catch (err) { + logger.warn("WireGuard sync failed, may need full restart:", err.message); + } + }, + + /** + * Start WireGuard interface + */ + async startup(knex) { + try { + const iface = await this.getOrCreateInterface(knex); + + // Ensure config dir exists + if (!fs.existsSync(WG_CONFIG_DIR)) { + fs.mkdirSync(WG_CONFIG_DIR, { recursive: true }); + } + + // Save config first + await this.saveConfig(knex); + + // Bring down if already up, then up + try { + await wgHelpers.wgDown(iface.name); + } catch (_) { + // Ignore if not up + } + + await wgHelpers.wgUp(iface.name); + logger.info(`WireGuard interface ${iface.name} started on port ${iface.listen_port}`); + + // Start cron job for expiration + this.startCronJob(knex); + } catch (err) { + logger.error("WireGuard startup failed:", err.message); + logger.warn("WireGuard features will be unavailable. Ensure the host supports WireGuard kernel module."); + } + }, + + /** + * Shutdown WireGuard interface + */ + async shutdown(knex) { + if (cronTimer) { + clearInterval(cronTimer); + cronTimer = null; + } + try { + const iface = await knex("wg_interface").first(); + if (iface) { + await wgHelpers.wgDown(iface.name); + logger.info(`WireGuard interface ${iface.name} stopped`); + } + } catch (err) { + logger.warn("WireGuard shutdown warning:", err.message); + } + }, + + /** + * Get all clients with live status + */ + async getClients(knex) { + const iface = await this.getOrCreateInterface(knex); + const dbClients = await knex("wg_client").orderBy("created_on", "desc"); + + const clients = dbClients.map((c) => ({ + id: c.id, + name: c.name, + enabled: c.enabled === 1 || c.enabled === true, + ipv4_address: c.ipv4_address, + public_key: c.public_key, + allowed_ips: c.allowed_ips, + persistent_keepalive: c.persistent_keepalive, + created_on: c.created_on, + updated_on: c.modified_on, + expires_at: c.expires_at, + // Live status (populated below) + latest_handshake_at: null, + endpoint: null, + transfer_rx: 0, + transfer_tx: 0, + })); + + // Get live WireGuard status + try { + const dump = await wgHelpers.wgDump(iface.name); + for (const peer of dump) { + const client = clients.find((c) => c.public_key === peer.publicKey); + if (client) { + client.latest_handshake_at = peer.latestHandshakeAt; + client.endpoint = peer.endpoint; + client.transfer_rx = peer.transferRx; + client.transfer_tx = peer.transferTx; + } + } + } catch (_) { + // WireGuard may not be running + } + + return clients; + }, + + /** + * Create a new WireGuard client + */ + async createClient(knex, data) { + const iface = await this.getOrCreateInterface(knex); + + // Generate keys + const privateKey = await wgHelpers.generatePrivateKey(); + const publicKey = await wgHelpers.getPublicKey(privateKey); + const preSharedKey = await wgHelpers.generatePreSharedKey(); + + // Allocate IP + const existingClients = await knex("wg_client").select("ipv4_address"); + const allocatedIPs = existingClients.map((c) => c.ipv4_address); + const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs); + + const clientData = { + name: data.name || "Unnamed Client", + enabled: true, + ipv4_address: ipv4Address, + private_key: privateKey, + public_key: publicKey, + pre_shared_key: preSharedKey, + allowed_ips: data.allowed_ips || WG_DEFAULT_ALLOWED_IPS, + persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE, + expires_at: data.expires_at || null, + created_on: knex.fn.now(), + modified_on: knex.fn.now(), + }; + + const [id] = await knex("wg_client").insert(clientData); + + // Sync WireGuard config + await this.saveConfig(knex); + + return knex("wg_client").where("id", id).first(); + }, + + /** + * Delete a WireGuard client + */ + async deleteClient(knex, clientId) { + const client = await knex("wg_client").where("id", clientId).first(); + if (!client) { + throw new Error("Client not found"); + } + + await knex("wg_client").where("id", clientId).del(); + await this.saveConfig(knex); + + return { success: true }; + }, + + /** + * Toggle a WireGuard client enabled/disabled + */ + async toggleClient(knex, clientId, enabled) { + const client = await knex("wg_client").where("id", clientId).first(); + if (!client) { + throw new Error("Client not found"); + } + + await knex("wg_client").where("id", clientId).update({ + enabled: enabled, + modified_on: knex.fn.now(), + }); + + await this.saveConfig(knex); + + return knex("wg_client").where("id", clientId).first(); + }, + + /** + * Update a WireGuard client + */ + async updateClient(knex, clientId, data) { + const client = await knex("wg_client").where("id", clientId).first(); + if (!client) { + throw new Error("Client not found"); + } + + const updateData = {}; + if (data.name !== undefined) updateData.name = data.name; + if (data.allowed_ips !== undefined) updateData.allowed_ips = data.allowed_ips; + if (data.persistent_keepalive !== undefined) updateData.persistent_keepalive = data.persistent_keepalive; + if (data.expires_at !== undefined) updateData.expires_at = data.expires_at; + updateData.modified_on = knex.fn.now(); + + await knex("wg_client").where("id", clientId).update(updateData); + await this.saveConfig(knex); + + return knex("wg_client").where("id", clientId).first(); + }, + + /** + * Get client configuration file content + */ + async getClientConfiguration(knex, clientId) { + const iface = await this.getOrCreateInterface(knex); + const client = await knex("wg_client").where("id", clientId).first(); + if (!client) { + throw new Error("Client not found"); + } + + const endpoint = `${iface.host || "YOUR_SERVER_IP"}:${iface.listen_port}`; + + return wgHelpers.generateClientConfig({ + clientPrivateKey: client.private_key, + clientAddress: `${client.ipv4_address}/32`, + dns: iface.dns, + mtu: iface.mtu, + serverPublicKey: iface.public_key, + preSharedKey: client.pre_shared_key, + allowedIps: client.allowed_ips, + persistentKeepalive: client.persistent_keepalive, + endpoint: endpoint, + }); + }, + + /** + * Get QR code SVG for client config + */ + async getClientQRCode(knex, clientId) { + const config = await this.getClientConfiguration(knex, clientId); + return wgHelpers.generateQRCodeSVG(config); + }, + + /** + * Get the WireGuard interface info + */ + async getInterfaceInfo(knex) { + const iface = await this.getOrCreateInterface(knex); + return { + id: iface.id, + name: iface.name, + public_key: iface.public_key, + ipv4_cidr: iface.ipv4_cidr, + listen_port: iface.listen_port, + mtu: iface.mtu, + dns: iface.dns, + host: iface.host, + }; + }, + + /** + * Cron job to check client expirations + */ + startCronJob(knex) { + cronTimer = setInterval(async () => { + try { + const clients = await knex("wg_client").where("enabled", true).whereNotNull("expires_at"); + let needsSave = false; + + for (const client of clients) { + if (new Date() > new Date(client.expires_at)) { + logger.info(`WireGuard client "${client.name}" (${client.id}) has expired, disabling.`); + await knex("wg_client").where("id", client.id).update({ + enabled: false, + modified_on: knex.fn.now(), + }); + needsSave = true; + } + } + + if (needsSave) { + await this.saveConfig(knex); + } + } catch (err) { + logger.error("WireGuard cron job error:", err.message); + } + }, 60 * 1000); // every 60 seconds + }, +}; + +export default internalWireguard; diff --git a/backend/knexfile.js b/backend/knexfile.js new file mode 100644 index 0000000..607552f --- /dev/null +++ b/backend/knexfile.js @@ -0,0 +1,19 @@ +module.exports = { + development: { + client: 'mysql2', + migrations: { + tableName: 'migrations', + stub: 'lib/migrate_template.js', + directory: 'migrations' + } + }, + + production: { + client: 'mysql2', + migrations: { + tableName: 'migrations', + stub: 'lib/migrate_template.js', + directory: 'migrations' + } + } +}; diff --git a/backend/lib/access.js b/backend/lib/access.js new file mode 100644 index 0000000..a4dec5c --- /dev/null +++ b/backend/lib/access.js @@ -0,0 +1,278 @@ +/** + * Some Notes: This is a friggin complicated piece of code. + * + * "scope" in this file means "where did this token come from and what is using it", so 99% of the time + * the "scope" is going to be "user" because it would be a user token. This is not to be confused with + * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else. + */ + +import fs from "node:fs"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv from "ajv/dist/2020.js"; +import _ from "lodash"; +import { access as logger } from "../logger.js"; +import proxyHostModel from "../models/proxy_host.js"; +import TokenModel from "../models/token.js"; +import userModel from "../models/user.js"; +import permsSchema from "./access/permissions.json" with { type: "json" }; +import roleSchema from "./access/roles.json" with { type: "json" }; +import errs from "./error.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export default function (tokenString) { + const Token = TokenModel(); + let tokenData = null; + let initialised = false; + const objectCache = {}; + let allowInternalAccess = false; + let userRoles = []; + let permissions = {}; + + /** + * Loads the Token object from the token string + * + * @returns {Promise} + */ + this.init = async () => { + if (initialised) { + return; + } + + if (!tokenString) { + throw new errs.PermissionError("Permission Denied"); + } + + tokenData = await Token.load(tokenString); + + // At this point we need to load the user from the DB and make sure they: + // - exist (and not soft deleted) + // - still have the appropriate scopes for this token + // This is only required when the User ID is supplied or if the token scope has `user` + if ( + tokenData.attrs.id || + (typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1) + ) { + // Has token user id or token user scope + const user = await userModel + .query() + .where("id", tokenData.attrs.id) + .andWhere("is_deleted", 0) + .andWhere("is_disabled", 0) + .allowGraph("[permissions]") + .withGraphFetched("[permissions]") + .first(); + + if (user) { + // make sure user has all scopes of the token + // The `user` role is not added against the user row, so we have to just add it here to get past this check. + user.roles.push("user"); + + let ok = true; + _.forEach(tokenData.scope, (scope_item) => { + if (_.indexOf(user.roles, scope_item) === -1) { + ok = false; + } + }); + + if (!ok) { + throw new errs.AuthError("Invalid token scope for User"); + } + initialised = true; + userRoles = user.roles; + permissions = user.permissions; + } else { + throw new errs.AuthError("User cannot be loaded for Token"); + } + } + initialised = true; + }; + + /** + * Fetches the object ids from the database, only once per object type, for this token. + * This only applies to USER token scopes, as all other tokens are not really bound + * by object scopes + * + * @param {String} objectType + * @returns {Promise} + */ + this.loadObjects = async (objectType) => { + let objects = null; + + if (Token.hasScope("user")) { + if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) { + throw new errs.AuthError("User Token supplied without a User ID"); + } + + const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0; + + if (typeof objectCache[objectType] !== "undefined") { + objects = objectCache[objectType]; + } else { + switch (objectType) { + // USERS - should only return yourself + case "users": + objects = tokenUserId ? [tokenUserId] : []; + break; + + // Proxy Hosts + case "proxy_hosts": { + const query = proxyHostModel + .query() + .select("id") + .andWhere("is_deleted", 0); + + if (permissions.visibility === "user") { + query.andWhere("owner_user_id", tokenUserId); + } + + const rows = await query; + objects = []; + _.forEach(rows, (ruleRow) => { + objects.push(ruleRow.id); + }); + + // enum should not have less than 1 item + if (!objects.length) { + objects.push(0); + } + break; + } + } + objectCache[objectType] = objects; + } + } + return objects; + }; + + /** + * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema + * + * @param {String} permissionLabel + * @returns {Object} + */ + this.getObjectSchema = async (permissionLabel) => { + const baseObjectType = permissionLabel.split(":").shift(); + + const schema = { + $id: "objects", + description: "Actor Properties", + type: "object", + additionalProperties: false, + properties: { + user_id: { + anyOf: [ + { + type: "number", + enum: [Token.get("attrs").id], + }, + ], + }, + scope: { + type: "string", + pattern: `^${Token.get("scope")}$`, + }, + }, + }; + + const result = await this.loadObjects(baseObjectType); + if (typeof result === "object" && result !== null) { + schema.properties[baseObjectType] = { + type: "number", + enum: result, + minimum: 1, + }; + } else { + schema.properties[baseObjectType] = { + type: "number", + minimum: 1, + }; + } + + return schema; + }; + + // here: + + return { + token: Token, + + /** + * + * @param {Boolean} [allowInternal] + * @returns {Promise} + */ + load: async (allowInternal) => { + if (tokenString) { + return await Token.load(tokenString); + } + allowInternalAccess = allowInternal; + return allowInternal || null; + }, + + reloadObjects: this.loadObjects, + + /** + * + * @param {String} permission + * @param {*} [data] + * @returns {Promise} + */ + can: async (permission, data) => { + if (allowInternalAccess === true) { + return true; + } + + try { + await this.init(); + const objectSchema = await this.getObjectSchema(permission); + + const dataSchema = { + [permission]: { + data: data, + scope: Token.get("scope"), + roles: userRoles, + permission_visibility: permissions.visibility, + permission_proxy_hosts: permissions.proxy_hosts, + permission_redirection_hosts: permissions.redirection_hosts, + permission_dead_hosts: permissions.dead_hosts, + permission_streams: permissions.streams, + permission_access_lists: permissions.access_lists, + permission_certificates: permissions.certificates, + }, + }; + + const permissionSchema = { + $async: true, + $id: "permissions", + type: "object", + additionalProperties: false, + properties: {}, + }; + + const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, { + encoding: "utf8", + }); + permissionSchema.properties[permission] = JSON.parse(rawData); + + const ajv = new Ajv({ + verbose: true, + allErrors: true, + breakOnError: true, + coerceTypes: true, + schemas: [roleSchema, permsSchema, objectSchema, permissionSchema], + }); + + const valid = await ajv.validate("permissions", dataSchema); + return valid && dataSchema[permission]; + } catch (err) { + err.permission = permission; + err.permission_data = data; + logger.error(permission, data, err.message); + throw errs.PermissionError("Permission Denied", err); + } + }, + }; +} diff --git a/backend/lib/access/access_lists-create.json b/backend/lib/access/access_lists-create.json new file mode 100644 index 0000000..5a16a86 --- /dev/null +++ b/backend/lib/access/access_lists-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/access_lists-delete.json b/backend/lib/access/access_lists-delete.json new file mode 100644 index 0000000..5a16a86 --- /dev/null +++ b/backend/lib/access/access_lists-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/access_lists-get.json b/backend/lib/access/access_lists-get.json new file mode 100644 index 0000000..8f6dd8c --- /dev/null +++ b/backend/lib/access/access_lists-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/access_lists-list.json b/backend/lib/access/access_lists-list.json new file mode 100644 index 0000000..8f6dd8c --- /dev/null +++ b/backend/lib/access/access_lists-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/access_lists-update.json b/backend/lib/access/access_lists-update.json new file mode 100644 index 0000000..5a16a86 --- /dev/null +++ b/backend/lib/access/access_lists-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_access_lists", "roles"], + "properties": { + "permission_access_lists": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/auditlog-list.json b/backend/lib/access/auditlog-list.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/auditlog-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/certificates-create.json b/backend/lib/access/certificates-create.json new file mode 100644 index 0000000..bcdf667 --- /dev/null +++ b/backend/lib/access/certificates-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/certificates-delete.json b/backend/lib/access/certificates-delete.json new file mode 100644 index 0000000..bcdf667 --- /dev/null +++ b/backend/lib/access/certificates-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/certificates-get.json b/backend/lib/access/certificates-get.json new file mode 100644 index 0000000..9ccfa4f --- /dev/null +++ b/backend/lib/access/certificates-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/certificates-list.json b/backend/lib/access/certificates-list.json new file mode 100644 index 0000000..9ccfa4f --- /dev/null +++ b/backend/lib/access/certificates-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/certificates-update.json b/backend/lib/access/certificates-update.json new file mode 100644 index 0000000..bcdf667 --- /dev/null +++ b/backend/lib/access/certificates-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_certificates", "roles"], + "properties": { + "permission_certificates": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/dead_hosts-create.json b/backend/lib/access/dead_hosts-create.json new file mode 100644 index 0000000..a276c68 --- /dev/null +++ b/backend/lib/access/dead_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/dead_hosts-delete.json b/backend/lib/access/dead_hosts-delete.json new file mode 100644 index 0000000..a276c68 --- /dev/null +++ b/backend/lib/access/dead_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/dead_hosts-get.json b/backend/lib/access/dead_hosts-get.json new file mode 100644 index 0000000..87aa12e --- /dev/null +++ b/backend/lib/access/dead_hosts-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/dead_hosts-list.json b/backend/lib/access/dead_hosts-list.json new file mode 100644 index 0000000..87aa12e --- /dev/null +++ b/backend/lib/access/dead_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/dead_hosts-update.json b/backend/lib/access/dead_hosts-update.json new file mode 100644 index 0000000..a276c68 --- /dev/null +++ b/backend/lib/access/dead_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_dead_hosts", "roles"], + "properties": { + "permission_dead_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/permissions.json b/backend/lib/access/permissions.json new file mode 100644 index 0000000..e7a82ec --- /dev/null +++ b/backend/lib/access/permissions.json @@ -0,0 +1,13 @@ +{ + "$id": "perms", + "definitions": { + "view": { + "type": "string", + "pattern": "^(view|manage)$" + }, + "manage": { + "type": "string", + "pattern": "^(manage)$" + } + } +} diff --git a/backend/lib/access/proxy_hosts-create.json b/backend/lib/access/proxy_hosts-create.json new file mode 100644 index 0000000..166527a --- /dev/null +++ b/backend/lib/access/proxy_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/proxy_hosts-delete.json b/backend/lib/access/proxy_hosts-delete.json new file mode 100644 index 0000000..166527a --- /dev/null +++ b/backend/lib/access/proxy_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/proxy_hosts-get.json b/backend/lib/access/proxy_hosts-get.json new file mode 100644 index 0000000..d88e4cf --- /dev/null +++ b/backend/lib/access/proxy_hosts-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/proxy_hosts-list.json b/backend/lib/access/proxy_hosts-list.json new file mode 100644 index 0000000..d88e4cf --- /dev/null +++ b/backend/lib/access/proxy_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/proxy_hosts-update.json b/backend/lib/access/proxy_hosts-update.json new file mode 100644 index 0000000..166527a --- /dev/null +++ b/backend/lib/access/proxy_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_proxy_hosts", "roles"], + "properties": { + "permission_proxy_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/redirection_hosts-create.json b/backend/lib/access/redirection_hosts-create.json new file mode 100644 index 0000000..342babc --- /dev/null +++ b/backend/lib/access/redirection_hosts-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/redirection_hosts-delete.json b/backend/lib/access/redirection_hosts-delete.json new file mode 100644 index 0000000..342babc --- /dev/null +++ b/backend/lib/access/redirection_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/redirection_hosts-get.json b/backend/lib/access/redirection_hosts-get.json new file mode 100644 index 0000000..ba22920 --- /dev/null +++ b/backend/lib/access/redirection_hosts-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/redirection_hosts-list.json b/backend/lib/access/redirection_hosts-list.json new file mode 100644 index 0000000..ba22920 --- /dev/null +++ b/backend/lib/access/redirection_hosts-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/redirection_hosts-update.json b/backend/lib/access/redirection_hosts-update.json new file mode 100644 index 0000000..342babc --- /dev/null +++ b/backend/lib/access/redirection_hosts-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_redirection_hosts", "roles"], + "properties": { + "permission_redirection_hosts": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/reports-hosts.json b/backend/lib/access/reports-hosts.json new file mode 100644 index 0000000..dbc9e0c --- /dev/null +++ b/backend/lib/access/reports-hosts.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/user" + } + ] +} diff --git a/backend/lib/access/roles.json b/backend/lib/access/roles.json new file mode 100644 index 0000000..c97313d --- /dev/null +++ b/backend/lib/access/roles.json @@ -0,0 +1,38 @@ +{ + "$id": "roles", + "definitions": { + "admin": { + "type": "object", + "required": ["scope", "roles"], + "properties": { + "scope": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^user$" + } + }, + "roles": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^admin$" + } + } + } + }, + "user": { + "type": "object", + "required": ["scope"], + "properties": { + "scope": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^user$" + } + } + } + } + } +} diff --git a/backend/lib/access/settings-get.json b/backend/lib/access/settings-get.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/settings-get.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/settings-list.json b/backend/lib/access/settings-list.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/settings-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/settings-update.json b/backend/lib/access/settings-update.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/settings-update.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/streams-create.json b/backend/lib/access/streams-create.json new file mode 100644 index 0000000..fbeb1cc --- /dev/null +++ b/backend/lib/access/streams-create.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/streams-delete.json b/backend/lib/access/streams-delete.json new file mode 100644 index 0000000..fbeb1cc --- /dev/null +++ b/backend/lib/access/streams-delete.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/streams-get.json b/backend/lib/access/streams-get.json new file mode 100644 index 0000000..7e99628 --- /dev/null +++ b/backend/lib/access/streams-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/streams-list.json b/backend/lib/access/streams-list.json new file mode 100644 index 0000000..7e99628 --- /dev/null +++ b/backend/lib/access/streams-list.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/view" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/streams-update.json b/backend/lib/access/streams-update.json new file mode 100644 index 0000000..fbeb1cc --- /dev/null +++ b/backend/lib/access/streams-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["permission_streams", "roles"], + "properties": { + "permission_streams": { + "$ref": "perms#/definitions/manage" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": ["user"] + } + } + } + } + ] +} diff --git a/backend/lib/access/users-create.json b/backend/lib/access/users-create.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/users-create.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-delete.json b/backend/lib/access/users-delete.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/users-delete.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-get.json b/backend/lib/access/users-get.json new file mode 100644 index 0000000..2a2f042 --- /dev/null +++ b/backend/lib/access/users-get.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["data", "scope"], + "properties": { + "data": { + "$ref": "objects#/properties/users" + }, + "scope": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^user$" + } + } + } + } + ] +} diff --git a/backend/lib/access/users-list.json b/backend/lib/access/users-list.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/users-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-loginas.json b/backend/lib/access/users-loginas.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/users-loginas.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-password.json b/backend/lib/access/users-password.json new file mode 100644 index 0000000..2a2f042 --- /dev/null +++ b/backend/lib/access/users-password.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["data", "scope"], + "properties": { + "data": { + "$ref": "objects#/properties/users" + }, + "scope": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^user$" + } + } + } + } + ] +} diff --git a/backend/lib/access/users-permissions.json b/backend/lib/access/users-permissions.json new file mode 100644 index 0000000..aeadc94 --- /dev/null +++ b/backend/lib/access/users-permissions.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-update.json b/backend/lib/access/users-update.json new file mode 100644 index 0000000..2a2f042 --- /dev/null +++ b/backend/lib/access/users-update.json @@ -0,0 +1,23 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + }, + { + "type": "object", + "required": ["data", "scope"], + "properties": { + "data": { + "$ref": "objects#/properties/users" + }, + "scope": { + "type": "array", + "contains": { + "type": "string", + "pattern": "^user$" + } + } + } + } + ] +} diff --git a/backend/lib/certbot.js b/backend/lib/certbot.js new file mode 100644 index 0000000..3a2dd07 --- /dev/null +++ b/backend/lib/certbot.js @@ -0,0 +1,86 @@ +import batchflow from "batchflow"; +import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" }; +import { certbot as logger } from "../logger.js"; +import errs from "./error.js"; +import utils from "./utils.js"; + +const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')"; + +/** + * Installs a cerbot plugin given the key for the object from + * ../certbot/dns-plugins.json + * + * @param {string} pluginKey + * @returns {Object} + */ +const installPlugin = async (pluginKey) => { + if (typeof dnsPlugins[pluginKey] === "undefined") { + // throw Error(`Certbot plugin ${pluginKey} not found`); + throw new errs.ItemNotFoundError(pluginKey); + } + + const plugin = dnsPlugins[pluginKey]; + logger.start(`Installing ${pluginKey}...`); + + plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); + plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); + + // SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly + // in new versions of Python + let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "stdlib" }); + if (typeof plugin.env === "object") { + env = Object.assign(env, plugin.env); + } + + const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`; + return utils + .exec(cmd, { env }) + .then((result) => { + logger.complete(`Installed ${pluginKey}`); + return result; + }) + .catch((err) => { + throw err; + }); +}; + +/** + * @param {array} pluginKeys + */ +const installPlugins = async (pluginKeys) => { + let hasErrors = false; + + return new Promise((resolve, reject) => { + if (pluginKeys.length === 0) { + resolve(); + return; + } + + batchflow(pluginKeys) + .sequential() + .each((_i, pluginKey, next) => { + installPlugin(pluginKey) + .then(() => { + next(); + }) + .catch((err) => { + hasErrors = true; + next(err); + }); + }) + .error((err) => { + logger.error(err.message); + }) + .end(() => { + if (hasErrors) { + reject( + new errs.CommandError("Some plugins failed to install. Please check the logs above", 1), + ); + } else { + resolve(); + } + }); + }); +}; + +export { installPlugins, installPlugin }; diff --git a/backend/lib/config.js b/backend/lib/config.js new file mode 100644 index 0000000..544bbfe --- /dev/null +++ b/backend/lib/config.js @@ -0,0 +1,262 @@ +import fs from "node:fs"; +import NodeRSA from "node-rsa"; +import { global as logger } from "../logger.js"; + +const keysFile = '/data/keys.json'; +const mysqlEngine = 'mysql2'; +const postgresEngine = 'pg'; +const sqliteClientName = 'better-sqlite3'; + +// Not used for new setups anymore but may exist in legacy setups +const legacySqliteClientName = 'sqlite3'; + +let instance = null; + +// 1. Load from config file first (not recommended anymore) +// 2. Use config env variables next +const configure = () => { + const filename = `${process.env.NODE_CONFIG_DIR || "./config"}/${process.env.NODE_ENV || "default"}.json`; + if (fs.existsSync(filename)) { + let configData; + try { + // Load this json synchronously + const rawData = fs.readFileSync(filename); + configData = JSON.parse(rawData); + } catch (_) { + // do nothing + } + + if (configData?.database) { + logger.info(`Using configuration from file: ${filename}`); + + // Migrate those who have "mysql" engine to "mysql2" + if (configData.database.engine === "mysql") { + configData.database.engine = mysqlEngine; + } + + instance = configData; + instance.keys = getKeys(); + return; + } + } + + const toBool = (v) => /^(1|true|yes|on)$/i.test((v || '').trim()); + + const envMysqlHost = process.env.DB_MYSQL_HOST || null; + const envMysqlUser = process.env.DB_MYSQL_USER || null; + const envMysqlName = process.env.DB_MYSQL_NAME || null; + const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL); + const envMysqlSSLRejectUnauthorized = process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED); + const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY); + if (envMysqlHost && envMysqlUser && envMysqlName) { + // we have enough mysql creds to go with mysql + logger.info("Using MySQL configuration"); + instance = { + database: { + engine: mysqlEngine, + host: envMysqlHost, + port: process.env.DB_MYSQL_PORT || 3306, + user: envMysqlUser, + password: process.env.DB_MYSQL_PASSWORD, + name: envMysqlName, + ssl: envMysqlSSL ? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity } : false, + }, + keys: getKeys(), + }; + return; + } + + const envPostgresHost = process.env.DB_POSTGRES_HOST || null; + const envPostgresUser = process.env.DB_POSTGRES_USER || null; + const envPostgresName = process.env.DB_POSTGRES_NAME || null; + if (envPostgresHost && envPostgresUser && envPostgresName) { + // we have enough postgres creds to go with postgres + logger.info("Using Postgres configuration"); + instance = { + database: { + engine: postgresEngine, + host: envPostgresHost, + port: process.env.DB_POSTGRES_PORT || 5432, + user: envPostgresUser, + password: process.env.DB_POSTGRES_PASSWORD, + name: envPostgresName, + }, + keys: getKeys(), + }; + return; + } + + const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite"; + + logger.info(`Using Sqlite: ${envSqliteFile}`); + instance = { + database: { + engine: "knex-native", + knex: { + client: sqliteClientName, + connection: { + filename: envSqliteFile, + }, + useNullAsDefault: true, + }, + }, + keys: getKeys(), + }; +}; + +const getKeys = () => { + // Get keys from file + if (isDebugMode()) { + logger.debug("Checking for keys file:", keysFile); + } + if (!fs.existsSync(keysFile)) { + generateKeys(); + } else if (process.env.DEBUG) { + logger.info("Keys file exists OK"); + } + try { + // Load this json keysFile synchronously and return the json object + const rawData = fs.readFileSync(keysFile); + return JSON.parse(rawData); + } catch (err) { + logger.error(`Could not read JWT key pair from config file: ${keysFile}`, err); + process.exit(1); + } +}; + +const generateKeys = () => { + logger.info("Creating a new JWT key pair..."); + // Now create the keys and save them in the config. + const key = new NodeRSA({ b: 2048 }); + key.generateKeyPair(); + + const keys = { + key: key.exportKey("private").toString(), + pub: key.exportKey("public").toString(), + }; + + // Write keys config + try { + fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2)); + } catch (err) { + logger.error(`Could not write JWT key pair to config file: ${keysFile}: ${err.message}`); + process.exit(1); + } + logger.info(`Wrote JWT key pair to config file: ${keysFile}`); +}; + +/** + * + * @param {string} key ie: 'database' or 'database.engine' + * @returns {boolean} + */ +const configHas = (key) => { + instance === null && configure(); + const keys = key.split("."); + let level = instance; + let has = true; + keys.forEach((keyItem) => { + if (typeof level[keyItem] === "undefined") { + has = false; + } else { + level = level[keyItem]; + } + }); + + return has; +}; + +/** + * Gets a specific key from the top level + * + * @param {string} key + * @returns {*} + */ +const configGet = (key) => { + instance === null && configure(); + if (key && typeof instance[key] !== "undefined") { + return instance[key]; + } + return instance; +}; + +/** + * Is this a sqlite configuration? + * + * @returns {boolean} + */ +const isSqlite = () => { + instance === null && configure(); + return instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client); +}; + +/** + * Is this a mysql configuration? + * + * @returns {boolean} + */ +const isMysql = () => { + instance === null && configure(); + return instance.database.engine === mysqlEngine; +}; + +/** + * Is this a postgres configuration? + * + * @returns {boolean} + */ +const isPostgres = () => { + instance === null && configure(); + return instance.database.engine === postgresEngine; +}; + +/** + * Are we running in debug mdoe? + * + * @returns {boolean} + */ +const isDebugMode = () => !!process.env.DEBUG; + +/** + * Are we running in CI? + * + * @returns {boolean} + */ +const isCI = () => process.env.CI === 'true' && process.env.DEBUG === 'true'; + +/** + * Returns a public key + * + * @returns {string} + */ +const getPublicKey = () => { + instance === null && configure(); + return instance.keys.pub; +}; + +/** + * Returns a private key + * + * @returns {string} + */ +const getPrivateKey = () => { + instance === null && configure(); + return instance.keys.key; +}; + +/** + * @returns {boolean} + */ +const useLetsencryptStaging = () => !!process.env.LE_STAGING; + +/** + * @returns {string|null} + */ +const useLetsencryptServer = () => { + if (process.env.LE_SERVER) { + return process.env.LE_SERVER; + } + return null; +}; + +export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer }; diff --git a/backend/lib/error.js b/backend/lib/error.js new file mode 100644 index 0000000..d7dbf0c --- /dev/null +++ b/backend/lib/error.js @@ -0,0 +1,103 @@ +import _ from "lodash"; + +const errs = { + PermissionError: function (_, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = "Permission Denied"; + this.public = true; + this.status = 403; + }, + + ItemNotFoundError: function (id, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = "Not Found"; + if (id) { + this.message = `Not Found - ${id}`; + } + this.public = true; + this.status = 404; + }, + + AuthError: function (message, messageI18n, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.message_i18n = messageI18n; + this.public = true; + this.status = 400; + }, + + InternalError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 500; + this.public = false; + }, + + InternalValidationError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 400; + this.public = false; + }, + + ConfigurationError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 400; + this.public = true; + }, + + CacheError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.message = message; + this.previous = previous; + this.status = 500; + this.public = false; + }, + + ValidationError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.public = true; + this.status = 400; + }, + + AssertionFailedError: function (message, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.public = false; + this.status = 400; + }, + + CommandError: function (stdErr, code, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = stdErr; + this.code = code; + this.public = false; + }, +}; + +_.forEach(errs, (err) => { + err.prototype = Object.create(Error.prototype); +}); + +export default errs; diff --git a/backend/lib/express/cors.js b/backend/lib/express/cors.js new file mode 100644 index 0000000..6fbf3ba --- /dev/null +++ b/backend/lib/express/cors.js @@ -0,0 +1,17 @@ +export default (req, res, next) => { + if (req.headers.origin) { + res.set({ + "Access-Control-Allow-Origin": req.headers.origin, + "Access-Control-Allow-Credentials": true, + "Access-Control-Allow-Methods": "OPTIONS, GET, POST", + "Access-Control-Allow-Headers": + "Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit", + "Access-Control-Max-Age": 5 * 60, + "Access-Control-Expose-Headers": "X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit", + }); + next(); + } else { + // No origin + next(); + } +}; diff --git a/backend/lib/express/jwt-decode.js b/backend/lib/express/jwt-decode.js new file mode 100644 index 0000000..90fe241 --- /dev/null +++ b/backend/lib/express/jwt-decode.js @@ -0,0 +1,15 @@ +import Access from "../access.js"; + +export default () => { + return async (_, res, next) => { + try { + res.locals.access = null; + const access = new Access(res.locals.token || null); + await access.load(); + res.locals.access = access; + next(); + } catch (err) { + next(err); + } + }; +}; diff --git a/backend/lib/express/jwt.js b/backend/lib/express/jwt.js new file mode 100644 index 0000000..ce907b6 --- /dev/null +++ b/backend/lib/express/jwt.js @@ -0,0 +1,13 @@ +export default function () { + return (req, res, next) => { + if (req.headers.authorization) { + const parts = req.headers.authorization.split(" "); + + if (parts && parts[0] === "Bearer" && parts[1]) { + res.locals.token = parts[1]; + } + } + + next(); + }; +} diff --git a/backend/lib/express/pagination.js b/backend/lib/express/pagination.js new file mode 100644 index 0000000..188df27 --- /dev/null +++ b/backend/lib/express/pagination.js @@ -0,0 +1,55 @@ +import _ from "lodash"; + +export default (default_sort, default_offset, default_limit, max_limit) => { + /** + * This will setup the req query params with filtered data and defaults + * + * sort will be an array of fields and their direction + * offset will be an int, defaulting to zero if no other default supplied + * limit will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied + * + */ + + return (req, _res, next) => { + req.query.offset = + typeof req.query.limit === "undefined" ? default_offset || 0 : Number.parseInt(req.query.offset, 10); + req.query.limit = + typeof req.query.limit === "undefined" ? default_limit || 50 : Number.parseInt(req.query.limit, 10); + + if (max_limit && req.query.limit > max_limit) { + req.query.limit = max_limit; + } + + // Sorting + let sort = typeof req.query.sort === "undefined" ? default_sort : req.query.sort; + const myRegexp = /.*\.(asc|desc)$/gi; + const sort_array = []; + + sort = sort.split(","); + _.map(sort, (val) => { + const matches = myRegexp.exec(val); + + if (matches !== null) { + const dir = matches[1]; + sort_array.push({ + field: val.substr(0, val.length - (dir.length + 1)), + dir: dir.toLowerCase(), + }); + } else { + sort_array.push({ + field: val, + dir: "asc", + }); + } + }); + + // Sort will now be in this format: + // [ + // { field: 'field1', dir: 'asc' }, + // { field: 'field2', dir: 'desc' } + // ] + + req.query.sort = sort_array; + next(); + }; +}; diff --git a/backend/lib/express/user-id-from-me.js b/backend/lib/express/user-id-from-me.js new file mode 100644 index 0000000..9c29ba2 --- /dev/null +++ b/backend/lib/express/user-id-from-me.js @@ -0,0 +1,8 @@ +export default (req, res, next) => { + if (req.params.user_id === 'me' && res.locals.access) { + req.params.user_id = res.locals.access.token.get('attrs').id; + } else { + req.params.user_id = Number.parseInt(req.params.user_id, 10); + } + next(); +}; diff --git a/backend/lib/helpers.js b/backend/lib/helpers.js new file mode 100644 index 0000000..853a7a5 --- /dev/null +++ b/backend/lib/helpers.js @@ -0,0 +1,58 @@ +import moment from "moment"; +import { ref } from "objection"; +import { isPostgres } from "./config.js"; + +/** + * Takes an expression such as 30d and returns a moment object of that date in future + * + * Key Shorthand + * ================== + * years y + * quarters Q + * months M + * weeks w + * days d + * hours h + * minutes m + * seconds s + * milliseconds ms + * + * @param {String} expression + * @returns {Object} + */ +const parseDatePeriod = (expression) => { + const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m); + if (matches) { + return moment().add(matches[1], matches[2]); + } + + return null; +}; + +const convertIntFieldsToBool = (obj, fields) => { + fields.forEach((field) => { + if (typeof obj[field] !== "undefined") { + obj[field] = obj[field] === 1; + } + }); + return obj; +}; + +const convertBoolFieldsToInt = (obj, fields) => { + fields.forEach((field) => { + if (typeof obj[field] !== "undefined") { + obj[field] = obj[field] ? 1 : 0; + } + }); + return obj; +}; + +/** + * Casts a column to json if using postgres + * + * @param {string} colName + * @returns {string|Objection.ReferenceBuilder} + */ +const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName); + +export { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed }; diff --git a/backend/lib/migrate_template.js b/backend/lib/migrate_template.js new file mode 100644 index 0000000..0b8e284 --- /dev/null +++ b/backend/lib/migrate_template.js @@ -0,0 +1,59 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "identifier_for_migrate"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (_knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + // Create Table example: + + /* + return knex.schema.createTable('notification', (table) => { + table.increments().primary(); + table.string('name').notNull(); + table.string('type').notNull(); + table.integer('created_on').notNull(); + table.integer('modified_on').notNull(); + }) + .then(function () { + logger.info('[' + migrateName + '] Notification Table created'); + }); + */ + + logger.info(`[${migrateName}] Migrating Up Complete`); + + return Promise.resolve(true); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + // Drop table example: + + /* + return knex.schema.dropTable('notification') + .then(() => { + logger.info(`[${migrateName}] Notification Table dropped`); + }); + */ + + logger.info(`[${migrateName}] Migrating Down Complete`); + + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/lib/utils.js b/backend/lib/utils.js new file mode 100644 index 0000000..af7ad3c --- /dev/null +++ b/backend/lib/utils.js @@ -0,0 +1,110 @@ +import { exec as nodeExec, execFile as nodeExecFile } from "node:child_process"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Liquid } from "liquidjs"; +import _ from "lodash"; +import { debug, global as logger } from "../logger.js"; +import errs from "./error.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const exec = async (cmd, options = {}) => { + debug(logger, "CMD:", cmd); + const { stdout, stderr } = await new Promise((resolve, reject) => { + const child = nodeExec(cmd, options, (isError, stdout, stderr) => { + if (isError) { + reject(new errs.CommandError(stderr, isError)); + } else { + resolve({ stdout, stderr }); + } + }); + + child.on("error", (e) => { + reject(new errs.CommandError(stderr, 1, e)); + }); + }); + return stdout; +}; + +/** + * @param {String} cmd + * @param {Array} args + * @param {Object|undefined} options + * @returns {Promise} + */ +const execFile = (cmd, args, options) => { + debug(logger, `CMD: ${cmd} ${args ? args.join(" ") : ""}`); + const opts = options || {}; + + return new Promise((resolve, reject) => { + nodeExecFile(cmd, args, opts, (err, stdout, stderr) => { + if (err && typeof err === "object") { + reject(new errs.CommandError(stderr, 1, err)); + } else { + resolve(stdout.trim()); + } + }); + }); +}; + +/** + * Used in objection query builder + * + * @param {Array} omissions + * @returns {Function} + */ +const omitRow = (omissions) => { + /** + * @param {Object} row + * @returns {Object} + */ + return (row) => { + return _.omit(row, omissions); + }; +}; + +/** + * Used in objection query builder + * + * @param {Array} omissions + * @returns {Function} + */ +const omitRows = (omissions) => { + /** + * @param {Array} rows + * @returns {Object} + */ + return (rows) => { + rows.forEach((row, idx) => { + rows[idx] = _.omit(row, omissions); + }); + return rows; + }; +}; + +/** + * @returns {Object} Liquid render engine + */ +const getRenderEngine = () => { + const renderEngine = new Liquid({ + root: `${__dirname}/../templates/`, + }); + + /** + * nginxAccessRule expects the object given to have 2 properties: + * + * directive string + * address string + */ + renderEngine.registerFilter("nginxAccessRule", (v) => { + if (typeof v.directive !== "undefined" && typeof v.address !== "undefined" && v.directive && v.address) { + return `${v.directive} ${v.address};`; + } + return ""; + }); + + return renderEngine; +}; + +export default { exec, execFile, omitRow, omitRows, getRenderEngine }; diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js new file mode 100644 index 0000000..6c738d5 --- /dev/null +++ b/backend/lib/validator/api.js @@ -0,0 +1,45 @@ +import Ajv from "ajv/dist/2020.js"; +import errs from "../error.js"; + +const ajv = new Ajv({ + verbose: true, + allErrors: true, + allowUnionTypes: true, + strict: false, + coerceTypes: true, +}); + +/** + * @param {Object} schema + * @param {Object} payload + * @returns {Promise} + */ +const apiValidator = async (schema, payload /*, description*/) => { + if (!schema) { + throw new errs.ValidationError("Schema is undefined"); + } + + // Can't use falsy check here as valid payload could be `0` or `false` + if (typeof payload === "undefined") { + throw new errs.ValidationError("Payload is undefined"); + } + + + const validate = ajv.compile(schema); + + const valid = validate(payload); + + + if (valid && !validate.errors) { + return payload; + } + + + + const message = ajv.errorsText(validate.errors); + const err = new errs.ValidationError(message); + err.debug = {validationErrors: validate.errors, payload}; + throw err; +}; + +export default apiValidator; diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js new file mode 100644 index 0000000..5f2586f --- /dev/null +++ b/backend/lib/validator/index.js @@ -0,0 +1,45 @@ +import Ajv from 'ajv/dist/2020.js'; +import _ from "lodash"; +import commonDefinitions from "../../schema/common.json" with { type: "json" }; +import errs from "../error.js"; + +RegExp.prototype.toJSON = RegExp.prototype.toString; + +const ajv = new Ajv({ + verbose: true, + allErrors: true, + allowUnionTypes: true, + coerceTypes: true, + strict: false, + schemas: [commonDefinitions], +}); + +/** + * + * @param {Object} schema + * @param {Object} payload + * @returns {Promise} + */ +const validator = (schema, payload) => { + return new Promise((resolve, reject) => { + if (!payload) { + reject(new errs.InternalValidationError("Payload is falsy")); + } else { + try { + const validate = ajv.compile(schema); + const valid = validate(payload); + + if (valid && !validate.errors) { + resolve(_.cloneDeep(payload)); + } else { + const message = ajv.errorsText(validate.errors); + reject(new errs.InternalValidationError(message)); + } + } catch (err) { + reject(err); + } + } + }); +}; + +export default validator; diff --git a/backend/lib/wg-helpers.js b/backend/lib/wg-helpers.js new file mode 100644 index 0000000..653a0f6 --- /dev/null +++ b/backend/lib/wg-helpers.js @@ -0,0 +1,183 @@ +import { spawn } from "child_process"; + +/** + * Execute a shell command and return stdout + */ +export function exec(cmd) { + return new Promise((resolve, reject) => { + const child = spawn("bash", ["-c", cmd], { + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + child.on("close", (code) => { + if (code !== 0) { + reject(new Error(`Command failed (exit ${code}): ${cmd}\n${stderr}`)); + } else { + resolve(stdout.trim()); + } + }); + child.on("error", reject); + }); +} + +/** + * Generate a WireGuard private key + */ +export async function generatePrivateKey() { + return exec("wg genkey"); +} + +/** + * Derive a public key from a private key + */ +export async function getPublicKey(privateKey) { + return exec(`echo ${privateKey} | wg pubkey`); +} + +/** + * Generate a pre-shared key + */ +export async function generatePreSharedKey() { + return exec("wg genpsk"); +} + +/** + * Bring up the WireGuard interface + */ +export async function wgUp(interfaceName) { + return exec(`wg-quick up ${interfaceName}`); +} + +/** + * Bring down the WireGuard interface + */ +export async function wgDown(interfaceName) { + return exec(`wg-quick down ${interfaceName}`); +} + +/** + * Sync WireGuard config without restarting + */ +export async function wgSync(interfaceName) { + return exec(`wg syncconf ${interfaceName} <(wg-quick strip ${interfaceName})`); +} + +/** + * Get WireGuard status dump + * Returns array of peer objects + */ +export async function wgDump(interfaceName) { + const rawDump = await exec(`wg show ${interfaceName} dump`); + return rawDump + .trim() + .split("\n") + .slice(1) // skip interface line + .map((line) => { + const [publicKey, preSharedKey, endpoint, allowedIps, latestHandshakeAt, transferRx, transferTx, persistentKeepalive] = line.split("\t"); + return { + publicKey, + preSharedKey, + endpoint: endpoint === "(none)" ? null : endpoint, + allowedIps, + latestHandshakeAt: latestHandshakeAt === "0" ? null : new Date(Number.parseInt(`${latestHandshakeAt}000`)), + transferRx: Number.parseInt(transferRx), + transferTx: Number.parseInt(transferTx), + persistentKeepalive, + }; + }); +} + +/** + * Generate the [Interface] section for the server config + */ +export function generateServerInterface({ privateKey, address, listenPort, mtu, dns, postUp, postDown }) { + const lines = ["[Interface]", `PrivateKey = ${privateKey}`, `Address = ${address}`, `ListenPort = ${listenPort}`]; + if (mtu) lines.push(`MTU = ${mtu}`); + if (dns) lines.push(`DNS = ${dns}`); + if (postUp) lines.push(`PostUp = ${postUp}`); + if (postDown) lines.push(`PostDown = ${postDown}`); + return lines.join("\n"); +} + +/** + * Generate a [Peer] section for the server config + */ +export function generateServerPeer({ publicKey, preSharedKey, allowedIps }) { + const lines = ["[Peer]", `PublicKey = ${publicKey}`, `PresharedKey = ${preSharedKey}`, `AllowedIPs = ${allowedIps}`]; + return lines.join("\n"); +} + +/** + * Generate complete client config file + */ +export function generateClientConfig({ clientPrivateKey, clientAddress, dns, mtu, serverPublicKey, preSharedKey, allowedIps, persistentKeepalive, endpoint }) { + const lines = [ + "[Interface]", + `PrivateKey = ${clientPrivateKey}`, + `Address = ${clientAddress}`, + ]; + if (mtu) lines.push(`MTU = ${mtu}`); + if (dns) lines.push(`DNS = ${dns}`); + lines.push("", "[Peer]", `PublicKey = ${serverPublicKey}`, `PresharedKey = ${preSharedKey}`, `AllowedIPs = ${allowedIps}`, `PersistentKeepalive = ${persistentKeepalive}`, `Endpoint = ${endpoint}`); + return lines.join("\n"); +} + +/** + * Simple QR code generator (outputs SVG via qrencode) + */ +export async function generateQRCodeSVG(text) { + return exec(`echo -n '${text.replace(/'/g, "'\\''")}' | qrencode -t SVG -o -`); +} + +/** + * Parse a CIDR string and return the network details + */ +export function parseCIDR(cidr) { + const [ip, prefix] = cidr.split("/"); + const prefixLen = Number.parseInt(prefix, 10); + const parts = ip.split(".").map(Number); + const ipNum = ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0; + const mask = prefixLen === 0 ? 0 : (~0 << (32 - prefixLen)) >>> 0; + const networkNum = (ipNum & mask) >>> 0; + const broadcastNum = (networkNum | ~mask) >>> 0; + return { + network: numToIp(networkNum), + broadcast: numToIp(broadcastNum), + firstHost: numToIp(networkNum + 1), + lastHost: numToIp(broadcastNum - 1), + prefix: prefixLen, + networkNum, + broadcastNum, + }; +} + +function numToIp(num) { + return `${(num >>> 24) & 255}.${(num >>> 16) & 255}.${(num >>> 8) & 255}.${num & 255}`; +} + +/** + * Find next available IP in a CIDR range given existing allocated IPs + * The first IP (network+1) is reserved for the server + */ +export function findNextAvailableIP(cidr, allocatedIPs) { + const parsed = parseCIDR(cidr); + // Start from network+2 (network+1 is server) + const startIP = parsed.networkNum + 2; + const endIP = parsed.broadcastNum - 1; + const allocatedSet = new Set(allocatedIPs); + + for (let ip = startIP; ip <= endIP; ip++) { + const ipStr = numToIp(ip); + if (!allocatedSet.has(ipStr)) { + return ipStr; + } + } + throw new Error("No available IP addresses in the CIDR range"); +} diff --git a/backend/logger.js b/backend/logger.js new file mode 100644 index 0000000..2b60dbf --- /dev/null +++ b/backend/logger.js @@ -0,0 +1,26 @@ +import signale from "signale"; +import { isDebugMode } from "./lib/config.js"; + +const opts = { + logLevel: "info", +}; + +const global = new signale.Signale({ scope: "Global ", ...opts }); +const migrate = new signale.Signale({ scope: "Migrate ", ...opts }); +const express = new signale.Signale({ scope: "Express ", ...opts }); +const access = new signale.Signale({ scope: "Access ", ...opts }); +const nginx = new signale.Signale({ scope: "Nginx ", ...opts }); +const ssl = new signale.Signale({ scope: "SSL ", ...opts }); +const certbot = new signale.Signale({ scope: "Certbot ", ...opts }); +const importer = new signale.Signale({ scope: "Importer ", ...opts }); +const setup = new signale.Signale({ scope: "Setup ", ...opts }); +const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts }); +const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts }); + +const debug = (logger, ...args) => { + if (isDebugMode()) { + logger.debug(...args); + } +}; + +export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion }; diff --git a/backend/migrate.js b/backend/migrate.js new file mode 100644 index 0000000..4c99cab --- /dev/null +++ b/backend/migrate.js @@ -0,0 +1,13 @@ +import db from "./db.js"; +import { migrate as logger } from "./logger.js"; + +const migrateUp = async () => { + const version = await db().migrate.currentVersion(); + logger.info("Current database version:", version); + return await db().migrate.latest({ + tableName: "migrations", + directory: "migrations", + }); +}; + +export { migrateUp }; diff --git a/backend/migrations/20180618015850_initial.js b/backend/migrations/20180618015850_initial.js new file mode 100644 index 0000000..d3c55d9 --- /dev/null +++ b/backend/migrations/20180618015850_initial.js @@ -0,0 +1,206 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "initial-schema"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .createTable("auth", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("user_id").notNull().unsigned(); + table.string("type", 30).notNull(); + table.string("secret").notNull(); + table.json("meta").notNull(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] auth Table created`); + + return knex.schema.createTable("user", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.integer("is_disabled").notNull().unsigned().defaultTo(0); + table.string("email").notNull(); + table.string("name").notNull(); + table.string("nickname").notNull(); + table.string("avatar").notNull(); + table.json("roles").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] user Table created`); + + return knex.schema.createTable("user_permission", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("user_id").notNull().unsigned(); + table.string("visibility").notNull(); + table.string("proxy_hosts").notNull(); + table.string("redirection_hosts").notNull(); + table.string("dead_hosts").notNull(); + table.string("streams").notNull(); + table.string("access_lists").notNull(); + table.string("certificates").notNull(); + table.unique("user_id"); + }); + }) + .then(() => { + logger.info(`[${migrateName}] user_permission Table created`); + + return knex.schema.createTable("proxy_host", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("owner_user_id").notNull().unsigned(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.json("domain_names").notNull(); + table.string("forward_ip").notNull(); + table.integer("forward_port").notNull().unsigned(); + table.integer("access_list_id").notNull().unsigned().defaultTo(0); + table.integer("certificate_id").notNull().unsigned().defaultTo(0); + table.integer("ssl_forced").notNull().unsigned().defaultTo(0); + table.integer("caching_enabled").notNull().unsigned().defaultTo(0); + table.integer("block_exploits").notNull().unsigned().defaultTo(0); + table.text("advanced_config").notNull().defaultTo(""); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table created`); + + return knex.schema.createTable("redirection_host", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("owner_user_id").notNull().unsigned(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.json("domain_names").notNull(); + table.string("forward_domain_name").notNull(); + table.integer("preserve_path").notNull().unsigned().defaultTo(0); + table.integer("certificate_id").notNull().unsigned().defaultTo(0); + table.integer("ssl_forced").notNull().unsigned().defaultTo(0); + table.integer("block_exploits").notNull().unsigned().defaultTo(0); + table.text("advanced_config").notNull().defaultTo(""); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table created`); + + return knex.schema.createTable("dead_host", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("owner_user_id").notNull().unsigned(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.json("domain_names").notNull(); + table.integer("certificate_id").notNull().unsigned().defaultTo(0); + table.integer("ssl_forced").notNull().unsigned().defaultTo(0); + table.text("advanced_config").notNull().defaultTo(""); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] dead_host Table created`); + + return knex.schema.createTable("stream", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("owner_user_id").notNull().unsigned(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.integer("incoming_port").notNull().unsigned(); + table.string("forward_ip").notNull(); + table.integer("forwarding_port").notNull().unsigned(); + table.integer("tcp_forwarding").notNull().unsigned().defaultTo(0); + table.integer("udp_forwarding").notNull().unsigned().defaultTo(0); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] stream Table created`); + + return knex.schema.createTable("access_list", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("owner_user_id").notNull().unsigned(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.string("name").notNull(); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] access_list Table created`); + + return knex.schema.createTable("certificate", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("owner_user_id").notNull().unsigned(); + table.integer("is_deleted").notNull().unsigned().defaultTo(0); + table.string("provider").notNull(); + table.string("nice_name").notNull().defaultTo(""); + table.json("domain_names").notNull(); + table.dateTime("expires_on").notNull(); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] certificate Table created`); + + return knex.schema.createTable("access_list_auth", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("access_list_id").notNull().unsigned(); + table.string("username").notNull(); + table.string("password").notNull(); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] access_list_auth Table created`); + + return knex.schema.createTable("audit_log", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("user_id").notNull().unsigned(); + table.string("object_type").notNull().defaultTo(""); + table.integer("object_id").notNull().unsigned().defaultTo(0); + table.string("action").notNull(); + table.json("meta").notNull(); + }); + }) + .then(() => { + logger.info(`[${migrateName}] audit_log Table created`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down the initial data.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20180929054513_websockets.js b/backend/migrations/20180929054513_websockets.js new file mode 100644 index 0000000..cce80d3 --- /dev/null +++ b/backend/migrations/20180929054513_websockets.js @@ -0,0 +1,36 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "websockets"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.integer("allow_websocket_upgrade").notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20181019052346_forward_host.js b/backend/migrations/20181019052346_forward_host.js new file mode 100644 index 0000000..fe11edc --- /dev/null +++ b/backend/migrations/20181019052346_forward_host.js @@ -0,0 +1,36 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "forward_host"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.renameColumn("forward_ip", "forward_host"); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20181113041458_http2_support.js b/backend/migrations/20181113041458_http2_support.js new file mode 100644 index 0000000..cfa94a9 --- /dev/null +++ b/backend/migrations/20181113041458_http2_support.js @@ -0,0 +1,50 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "http2_support"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.integer("http2_support").notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + + return knex.schema.table("redirection_host", (redirection_host) => { + redirection_host.integer("http2_support").notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + + return knex.schema.table("dead_host", (dead_host) => { + dead_host.integer("http2_support").notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info(`[${migrateName}] dead_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20181213013211_forward_scheme.js b/backend/migrations/20181213013211_forward_scheme.js new file mode 100644 index 0000000..ba3bc56 --- /dev/null +++ b/backend/migrations/20181213013211_forward_scheme.js @@ -0,0 +1,36 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "forward_scheme"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.string("forward_scheme").notNull().defaultTo("http"); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20190104035154_disabled.js b/backend/migrations/20190104035154_disabled.js new file mode 100644 index 0000000..28fcc7b --- /dev/null +++ b/backend/migrations/20190104035154_disabled.js @@ -0,0 +1,57 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "disabled"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.integer("enabled").notNull().unsigned().defaultTo(1); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + + return knex.schema.table("redirection_host", (redirection_host) => { + redirection_host.integer("enabled").notNull().unsigned().defaultTo(1); + }); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + + return knex.schema.table("dead_host", (dead_host) => { + dead_host.integer("enabled").notNull().unsigned().defaultTo(1); + }); + }) + .then(() => { + logger.info(`[${migrateName}] dead_host Table altered`); + + return knex.schema.table("stream", (stream) => { + stream.integer("enabled").notNull().unsigned().defaultTo(1); + }); + }) + .then(() => { + logger.info(`[${migrateName}] stream Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20190215115310_customlocations.js b/backend/migrations/20190215115310_customlocations.js new file mode 100644 index 0000000..c4f7797 --- /dev/null +++ b/backend/migrations/20190215115310_customlocations.js @@ -0,0 +1,37 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "custom_locations"; + +/** + * Migrate + * Extends proxy_host table with locations field + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.json("locations"); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20190218060101_hsts.js b/backend/migrations/20190218060101_hsts.js new file mode 100644 index 0000000..1253130 --- /dev/null +++ b/backend/migrations/20190218060101_hsts.js @@ -0,0 +1,53 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "hsts"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("proxy_host", (proxy_host) => { + proxy_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0); + proxy_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + + return knex.schema.table("redirection_host", (redirection_host) => { + redirection_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0); + redirection_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + + return knex.schema.table("dead_host", (dead_host) => { + dead_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0); + dead_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0); + }); + }) + .then(() => { + logger.info(`[${migrateName}] dead_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20190227065017_settings.js b/backend/migrations/20190227065017_settings.js new file mode 100644 index 0000000..a6cbe2c --- /dev/null +++ b/backend/migrations/20190227065017_settings.js @@ -0,0 +1,39 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "settings"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema.createTable('setting', (table) => { + table.string('id').notNull().primary(); + table.string('name', 100).notNull(); + table.string('description', 255).notNull(); + table.string('value', 255).notNull(); + table.json('meta').notNull(); + }) + .then(() => { + logger.info(`[${migrateName}] setting Table created`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down the initial data.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20200410143839_access_list_client.js b/backend/migrations/20200410143839_access_list_client.js new file mode 100644 index 0000000..e682504 --- /dev/null +++ b/backend/migrations/20200410143839_access_list_client.js @@ -0,0 +1,52 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "access_list_client"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .createTable("access_list_client", (table) => { + table.increments().primary(); + table.dateTime("created_on").notNull(); + table.dateTime("modified_on").notNull(); + table.integer("access_list_id").notNull().unsigned(); + table.string("address").notNull(); + table.string("directive").notNull(); + table.json("meta").notNull(); + }) + .then(() => { + logger.info(`[${migrateName}] access_list_client Table created`); + + return knex.schema.table("access_list", (access_list) => { + access_list.integer("satify_any").notNull().defaultTo(0); + }); + }) + .then(() => { + logger.info(`[${migrateName}] access_list Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema.dropTable("access_list_client").then(() => { + logger.info(`[${migrateName}] access_list_client Table dropped`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20200410143840_access_list_client_fix.js b/backend/migrations/20200410143840_access_list_client_fix.js new file mode 100644 index 0000000..6bdaedb --- /dev/null +++ b/backend/migrations/20200410143840_access_list_client_fix.js @@ -0,0 +1,36 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "access_list_client_fix"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("access_list", (access_list) => { + access_list.renameColumn("satify_any", "satisfy_any"); + }) + .then(() => { + logger.info(`[${migrateName}] access_list Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (_knex) => { + logger.warn(`[${migrateName}] You can't migrate down this one.`); + return Promise.resolve(true); +}; + +export { up, down }; diff --git a/backend/migrations/20201014143841_pass_auth.js b/backend/migrations/20201014143841_pass_auth.js new file mode 100644 index 0000000..dc57e2a --- /dev/null +++ b/backend/migrations/20201014143841_pass_auth.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "pass_auth"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("access_list", (access_list) => { + access_list.integer("pass_auth").notNull().defaultTo(1); + }) + .then(() => { + logger.info(`[${migrateName}] access_list Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .table("access_list", (access_list) => { + access_list.dropColumn("pass_auth"); + }) + .then(() => { + logger.info(`[${migrateName}] access_list pass_auth Column dropped`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20210210154702_redirection_scheme.js b/backend/migrations/20210210154702_redirection_scheme.js new file mode 100644 index 0000000..b3f18ae --- /dev/null +++ b/backend/migrations/20210210154702_redirection_scheme.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "redirection_scheme"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("redirection_host", (table) => { + table.string("forward_scheme").notNull().defaultTo("$scheme"); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .table("redirection_host", (table) => { + table.dropColumn("forward_scheme"); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20210210154703_redirection_status_code.js b/backend/migrations/20210210154703_redirection_status_code.js new file mode 100644 index 0000000..cf84298 --- /dev/null +++ b/backend/migrations/20210210154703_redirection_status_code.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "redirection_status_code"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("redirection_host", (table) => { + table.integer("forward_http_code").notNull().unsigned().defaultTo(302); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .table("redirection_host", (table) => { + table.dropColumn("forward_http_code"); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20210423103500_stream_domain.js b/backend/migrations/20210423103500_stream_domain.js new file mode 100644 index 0000000..b4afabd --- /dev/null +++ b/backend/migrations/20210423103500_stream_domain.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "stream_domain"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("stream", (table) => { + table.renameColumn("forward_ip", "forwarding_host"); + }) + .then(() => { + logger.info(`[${migrateName}] stream Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .table("stream", (table) => { + table.renameColumn("forwarding_host", "forward_ip"); + }) + .then(() => { + logger.info(`[${migrateName}] stream Table altered`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20211108145214_regenerate_default_host.js b/backend/migrations/20211108145214_regenerate_default_host.js new file mode 100644 index 0000000..c280515 --- /dev/null +++ b/backend/migrations/20211108145214_regenerate_default_host.js @@ -0,0 +1,52 @@ +import internalNginx from "../internal/nginx.js"; +import { migrate as logger } from "../logger.js"; + +const migrateName = "stream_domain"; + +async function regenerateDefaultHost(knex) { + const row = await knex("setting").select("*").where("id", "default-site").first(); + + if (!row) { + return Promise.resolve(); + } + + return internalNginx + .deleteConfig("default") + .then(() => { + return internalNginx.generateConfig("default", row); + }) + .then(() => { + return internalNginx.test(); + }) + .then(() => { + return internalNginx.reload(); + }); +} + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return regenerateDefaultHost(knex); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return regenerateDefaultHost(knex); +}; + +export { up, down }; diff --git a/backend/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js new file mode 100644 index 0000000..0fbba11 --- /dev/null +++ b/backend/migrations/20240427161436_stream_ssl.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "stream_ssl"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("stream", (table) => { + table.integer("certificate_id").notNull().unsigned().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] stream Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .table("stream", (table) => { + table.dropColumn("certificate_id"); + }) + .then(() => { + logger.info(`[${migrateName}] stream Table altered`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20251111090000_redirect_auto_scheme.js b/backend/migrations/20251111090000_redirect_auto_scheme.js new file mode 100644 index 0000000..9f5f9d0 --- /dev/null +++ b/backend/migrations/20251111090000_redirect_auto_scheme.js @@ -0,0 +1,50 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "redirect_auto_scheme"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .table("redirection_host", async (table) => { + // change the column default from $scheme to auto + await table.string("forward_scheme").notNull().defaultTo("auto").alter(); + await knex('redirection_host') + .where('forward_scheme', '$scheme') + .update({ forward_scheme: 'auto' }); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .table("redirection_host", async (table) => { + await table.string("forward_scheme").notNull().defaultTo("$scheme").alter(); + await knex('redirection_host') + .where('forward_scheme', 'auto') + .update({ forward_scheme: '$scheme' }); + }) + .then(() => { + logger.info(`[${migrateName}] redirection_host Table altered`); + }); +}; + +export { up, down }; diff --git a/backend/migrations/20260131163528_trust_forwarded_proto.js b/backend/migrations/20260131163528_trust_forwarded_proto.js new file mode 100644 index 0000000..546cbca --- /dev/null +++ b/backend/migrations/20260131163528_trust_forwarded_proto.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "trust_forwarded_proto"; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +const up = function (knex) { + logger.info(`[${migrateName}] Migrating Up...`); + + return knex.schema + .alterTable('proxy_host', (table) => { + table.tinyint('trust_forwarded_proto').notNullable().defaultTo(0); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +const down = function (knex) { + logger.info(`[${migrateName}] Migrating Down...`); + + return knex.schema + .alterTable('proxy_host', (table) => { + table.dropColumn('trust_forwarded_proto'); + }) + .then(() => { + logger.info(`[${migrateName}] proxy_host Table altered`); + }); +}; + +export { up, down }; \ No newline at end of file diff --git a/backend/migrations/20260307000000_wireguard.js b/backend/migrations/20260307000000_wireguard.js new file mode 100644 index 0000000..def38fe --- /dev/null +++ b/backend/migrations/20260307000000_wireguard.js @@ -0,0 +1,41 @@ +const migrate_name = "wireguard"; + +/** + * Wireguard tables migration + */ +export function up(knex) { + return knex.schema + .createTable("wg_interface", (table) => { + table.increments("id").primary(); + table.string("name").notNullable().defaultTo("wg0"); + table.text("private_key").notNullable(); + table.text("public_key").notNullable(); + table.string("ipv4_cidr").notNullable().defaultTo("10.8.0.0/24"); + table.integer("listen_port").notNullable().defaultTo(51820); + table.integer("mtu").notNullable().defaultTo(1420); + table.string("dns").defaultTo("1.1.1.1, 8.8.8.8"); + table.string("host").defaultTo(""); + table.text("post_up").defaultTo(""); + table.text("post_down").defaultTo(""); + table.dateTime("created_on").notNullable(); + table.dateTime("modified_on").notNullable(); + }) + .createTable("wg_client", (table) => { + table.increments("id").primary(); + table.string("name").notNullable().defaultTo("Unnamed Client"); + table.boolean("enabled").notNullable().defaultTo(true); + table.string("ipv4_address").notNullable(); + table.text("private_key").notNullable(); + table.text("public_key").notNullable(); + table.text("pre_shared_key").notNullable(); + table.string("allowed_ips").notNullable().defaultTo("0.0.0.0/0, ::/0"); + table.integer("persistent_keepalive").notNullable().defaultTo(25); + table.dateTime("expires_at").nullable(); + table.dateTime("created_on").notNullable(); + table.dateTime("modified_on").notNullable(); + }); +}; + +export function down(knex) { + return knex.schema.dropTableIfExists("wg_client").dropTableIfExists("wg_interface"); +}; diff --git a/backend/models/access_list.js b/backend/models/access_list.js new file mode 100644 index 0000000..427d447 --- /dev/null +++ b/backend/models/access_list.js @@ -0,0 +1,98 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import AccessListAuth from "./access_list_auth.js"; +import AccessListClient from "./access_list_client.js"; +import now from "./now_helper.js"; +import ProxyHostModel from "./proxy_host.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = ["is_deleted", "satisfy_any", "pass_auth"]; + +class AccessList extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "AccessList"; + } + + static get tableName() { + return "access_list"; + } + + static get jsonAttributes() { + return ["meta"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "access_list.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + items: { + relation: Model.HasManyRelation, + modelClass: AccessListAuth, + join: { + from: "access_list.id", + to: "access_list_auth.access_list_id", + }, + }, + clients: { + relation: Model.HasManyRelation, + modelClass: AccessListClient, + join: { + from: "access_list.id", + to: "access_list_client.access_list_id", + }, + }, + proxy_hosts: { + relation: Model.HasManyRelation, + modelClass: ProxyHostModel, + join: { + from: "access_list.id", + to: "proxy_host.access_list_id", + }, + modify: (qb) => { + qb.where("proxy_host.is_deleted", 0); + }, + }, + }; + } +} + +export default AccessList; diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js new file mode 100644 index 0000000..75bf435 --- /dev/null +++ b/backend/models/access_list_auth.js @@ -0,0 +1,55 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import accessListModel from "./access_list.js"; +import now from "./now_helper.js"; + +Model.knex(db()); + +class AccessListAuth extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + static get name() { + return "AccessListAuth"; + } + + static get tableName() { + return "access_list_auth"; + } + + static get jsonAttributes() { + return ["meta"]; + } + + static get relationMappings() { + return { + access_list: { + relation: Model.HasOneRelation, + modelClass: accessListModel, + join: { + from: "access_list_auth.access_list_id", + to: "access_list.id", + }, + modify: (qb) => { + qb.where("access_list.is_deleted", 0); + }, + }, + }; + } +} + +export default AccessListAuth; diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js new file mode 100644 index 0000000..91165fe --- /dev/null +++ b/backend/models/access_list_client.js @@ -0,0 +1,55 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import accessListModel from "./access_list.js"; +import now from "./now_helper.js"; + +Model.knex(db()); + +class AccessListClient extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + static get name() { + return "AccessListClient"; + } + + static get tableName() { + return "access_list_client"; + } + + static get jsonAttributes() { + return ["meta"]; + } + + static get relationMappings() { + return { + access_list: { + relation: Model.HasOneRelation, + modelClass: accessListModel, + join: { + from: "access_list_client.access_list_id", + to: "access_list.id", + }, + modify: (qb) => { + qb.where("access_list.is_deleted", 0); + }, + }, + }; + } +} + +export default AccessListClient; diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js new file mode 100644 index 0000000..6e2d398 --- /dev/null +++ b/backend/models/audit-log.js @@ -0,0 +1,52 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import now from "./now_helper.js"; +import User from "./user.js"; + +Model.knex(db()); + +class AuditLog extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + static get name() { + return "AuditLog"; + } + + static get tableName() { + return "audit_log"; + } + + static get jsonAttributes() { + return ["meta"]; + } + + static get relationMappings() { + return { + user: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "audit_log.user_id", + to: "user.id", + }, + }, + }; + } +} + +export default AuditLog; diff --git a/backend/models/auth.js b/backend/models/auth.js new file mode 100644 index 0000000..e8af582 --- /dev/null +++ b/backend/models/auth.js @@ -0,0 +1,92 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import bcrypt from "bcrypt"; +import { Model } from "objection"; +import db from "../db.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import now from "./now_helper.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = ["is_deleted"]; + +function encryptPassword() { + if (this.type === "password" && this.secret) { + return bcrypt.hash(this.secret, 13).then((hash) => { + this.secret = hash; + }); + } + + return null; +} + +class Auth extends Model { + $beforeInsert(queryContext) { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + + return encryptPassword.apply(this, queryContext); + } + + $beforeUpdate(queryContext) { + this.modified_on = now(); + return encryptPassword.apply(this, queryContext); + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + /** + * Verify a plain password against the encrypted password + * + * @param {String} password + * @returns {Promise} + */ + verifyPassword(password) { + return bcrypt.compare(password, this.secret); + } + + static get name() { + return "Auth"; + } + + static get tableName() { + return "auth"; + } + + static get jsonAttributes() { + return ["meta"]; + } + + static get relationMappings() { + return { + user: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "auth.user_id", + to: "user.id", + }, + filter: { + is_deleted: 0, + }, + }, + }; + } +} + +export default Auth; diff --git a/backend/models/certificate.js b/backend/models/certificate.js new file mode 100644 index 0000000..ad6e0a6 --- /dev/null +++ b/backend/models/certificate.js @@ -0,0 +1,133 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import deadHostModel from "./dead_host.js"; +import now from "./now_helper.js"; +import proxyHostModel from "./proxy_host.js"; +import redirectionHostModel from "./redirection_host.js"; +import streamModel from "./stream.js"; +import userModel from "./user.js"; + +Model.knex(db()); + +const boolFields = ["is_deleted"]; + +class Certificate extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for expires_on + if (typeof this.expires_on === "undefined") { + this.expires_on = now(); + } + + // Default for domain_names + if (typeof this.domain_names === "undefined") { + this.domain_names = []; + } + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + + this.domain_names.sort(); + } + + $beforeUpdate() { + this.modified_on = now(); + + // Sort domain_names + if (typeof this.domain_names !== "undefined") { + this.domain_names.sort(); + } + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "Certificate"; + } + + static get tableName() { + return "certificate"; + } + + static get jsonAttributes() { + return ["domain_names", "meta"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: userModel, + join: { + from: "certificate.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + proxy_hosts: { + relation: Model.HasManyRelation, + modelClass: proxyHostModel, + join: { + from: "certificate.id", + to: "proxy_host.certificate_id", + }, + modify: (qb) => { + qb.where("proxy_host.is_deleted", 0); + }, + }, + dead_hosts: { + relation: Model.HasManyRelation, + modelClass: deadHostModel, + join: { + from: "certificate.id", + to: "dead_host.certificate_id", + }, + modify: (qb) => { + qb.where("dead_host.is_deleted", 0); + }, + }, + redirection_hosts: { + relation: Model.HasManyRelation, + modelClass: redirectionHostModel, + join: { + from: "certificate.id", + to: "redirection_host.certificate_id", + }, + modify: (qb) => { + qb.where("redirection_host.is_deleted", 0); + }, + }, + streams: { + relation: Model.HasManyRelation, + modelClass: streamModel, + join: { + from: "certificate.id", + to: "stream.certificate_id", + }, + modify: (qb) => { + qb.where("stream.is_deleted", 0); + }, + }, + }; + } +} + +export default Certificate; diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js new file mode 100644 index 0000000..dc7c775 --- /dev/null +++ b/backend/models/dead_host.js @@ -0,0 +1,104 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import Certificate from "./certificate.js"; +import now from "./now_helper.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"]; + +class DeadHost extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for domain_names + if (typeof this.domain_names === "undefined") { + this.domain_names = []; + } + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + + this.domain_names.sort(); + } + + $beforeUpdate() { + this.modified_on = now(); + + // Sort domain_names + if (typeof this.domain_names !== "undefined") { + this.domain_names.sort(); + } + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "DeadHost"; + } + + static get tableName() { + return "dead_host"; + } + + static get jsonAttributes() { + return ["domain_names", "meta"]; + } + + static get defaultAllowGraph() { + return "[owner,certificate]"; + } + + static get defaultExpand() { + return ["certificate", "owner"]; + } + + static get defaultOrder() { + return [castJsonIfNeed("domain_names"), "ASC"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "dead_host.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: "dead_host.certificate_id", + to: "certificate.id", + }, + modify: (qb) => { + qb.where("certificate.is_deleted", 0); + }, + }, + }; + } +} + +export default DeadHost; diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js new file mode 100644 index 0000000..293dcc7 --- /dev/null +++ b/backend/models/now_helper.js @@ -0,0 +1,12 @@ +import { Model } from "objection"; +import db from "../db.js"; +import { isSqlite } from "../lib/config.js"; + +Model.knex(db()); + +export default () => { + if (isSqlite()) { + return Model.raw("datetime('now','localtime')"); + } + return Model.raw("NOW()"); +}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js new file mode 100644 index 0000000..acb8da9 --- /dev/null +++ b/backend/models/proxy_host.js @@ -0,0 +1,127 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import AccessList from "./access_list.js"; +import Certificate from "./certificate.js"; +import now from "./now_helper.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = [ + "is_deleted", + "ssl_forced", + "caching_enabled", + "block_exploits", + "allow_websocket_upgrade", + "http2_support", + "enabled", + "hsts_enabled", + "hsts_subdomains", + "trust_forwarded_proto", +]; + +class ProxyHost extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for domain_names + if (typeof this.domain_names === "undefined") { + this.domain_names = []; + } + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + + this.domain_names.sort(); + } + + $beforeUpdate() { + this.modified_on = now(); + + // Sort domain_names + if (typeof this.domain_names !== "undefined") { + this.domain_names.sort(); + } + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "ProxyHost"; + } + + static get tableName() { + return "proxy_host"; + } + + static get jsonAttributes() { + return ["domain_names", "meta", "locations"]; + } + + static get defaultAllowGraph() { + return "[owner,access_list.[clients,items],certificate]"; + } + + static get defaultExpand() { + return ["owner", "certificate", "access_list.[clients,items]"]; + } + + static get defaultOrder() { + return [castJsonIfNeed("domain_names"), "ASC"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "proxy_host.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + access_list: { + relation: Model.HasOneRelation, + modelClass: AccessList, + join: { + from: "proxy_host.access_list_id", + to: "access_list.id", + }, + modify: (qb) => { + qb.where("access_list.is_deleted", 0); + }, + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: "proxy_host.certificate_id", + to: "certificate.id", + }, + modify: (qb) => { + qb.where("certificate.is_deleted", 0); + }, + }, + }; + } +} + +export default ProxyHost; diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js new file mode 100644 index 0000000..0c47de6 --- /dev/null +++ b/backend/models/redirection_host.js @@ -0,0 +1,113 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import Certificate from "./certificate.js"; +import now from "./now_helper.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = [ + "is_deleted", + "enabled", + "preserve_path", + "ssl_forced", + "block_exploits", + "hsts_enabled", + "hsts_subdomains", + "http2_support", +]; + +class RedirectionHost extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for domain_names + if (typeof this.domain_names === "undefined") { + this.domain_names = []; + } + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + + this.domain_names.sort(); + } + + $beforeUpdate() { + this.modified_on = now(); + + // Sort domain_names + if (typeof this.domain_names !== "undefined") { + this.domain_names.sort(); + } + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "RedirectionHost"; + } + + static get tableName() { + return "redirection_host"; + } + + static get jsonAttributes() { + return ["domain_names", "meta"]; + } + + static get defaultAllowGraph() { + return "[owner,certificate]"; + } + + static get defaultExpand() { + return ["certificate", "owner"]; + } + + static get defaultOrder() { + return [castJsonIfNeed("domain_names"), "ASC"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "redirection_host.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: "redirection_host.certificate_id", + to: "certificate.id", + }, + modify: (qb) => { + qb.where("certificate.is_deleted", 0); + }, + }, + }; + } +} + +export default RedirectionHost; diff --git a/backend/models/setting.js b/backend/models/setting.js new file mode 100644 index 0000000..56f7dc5 --- /dev/null +++ b/backend/models/setting.js @@ -0,0 +1,30 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; + +Model.knex(db()); + +class Setting extends Model { + $beforeInsert () { + // Default for meta + if (typeof this.meta === 'undefined') { + this.meta = {}; + } + } + + static get name () { + return 'Setting'; + } + + static get tableName () { + return 'setting'; + } + + static get jsonAttributes () { + return ['meta']; + } +} + +export default Setting; diff --git a/backend/models/stream.js b/backend/models/stream.js new file mode 100644 index 0000000..20a23a2 --- /dev/null +++ b/backend/models/stream.js @@ -0,0 +1,89 @@ +import { Model } from "objection"; +import db from "../db.js"; +import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import Certificate from "./certificate.js"; +import now from "./now_helper.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"]; + +class Stream extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "Stream"; + } + + static get tableName() { + return "stream"; + } + + static get jsonAttributes() { + return ["meta"]; + } + + static get defaultAllowGraph() { + return "[owner,certificate]"; + } + + static get defaultExpand() { + return ["certificate", "owner"]; + } + + static get defaultOrder() { + return [castJsonIfNeed("incoming_port"), "ASC"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "stream.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: "stream.certificate_id", + to: "certificate.id", + }, + modify: (qb) => { + qb.where("certificate.is_deleted", 0); + }, + }, + }; + } +} + +export default Stream; diff --git a/backend/models/token.js b/backend/models/token.js new file mode 100644 index 0000000..4edab5b --- /dev/null +++ b/backend/models/token.js @@ -0,0 +1,140 @@ +/** + NOTE: This is not a database table, this is a model of a Token object that can be created/loaded + and then has abilities after that. + */ + +import crypto from "node:crypto"; +import jwt from "jsonwebtoken"; +import _ from "lodash"; +import { getPrivateKey, getPublicKey } from "../lib/config.js"; +import errs from "../lib/error.js"; +import { global as logger } from "../logger.js"; + +const ALGO = "RS256"; + +export default () => { + let tokenData = {}; + + const self = { + /** + * @param {Object} payload + * @returns {Promise} + */ + create: (payload) => { + if (!getPrivateKey()) { + logger.error("Private key is empty!"); + } + // sign with RSA SHA256 + const options = { + algorithm: ALGO, + expiresIn: payload.expiresIn || "1d", + }; + + payload.jti = crypto.randomBytes(12).toString("base64").substring(-8); + + return new Promise((resolve, reject) => { + jwt.sign(payload, getPrivateKey(), options, (err, token) => { + if (err) { + reject(err); + } else { + tokenData = payload; + resolve({ + token: token, + payload: payload, + }); + } + }); + }); + }, + + /** + * @param {String} token + * @returns {Promise} + */ + load: (token) => { + if (!getPublicKey()) { + logger.error("Public key is empty!"); + } + return new Promise((resolve, reject) => { + try { + if (!token || token === null || token === "null") { + reject(new errs.AuthError("Empty token")); + } else { + jwt.verify( + token, + getPublicKey(), + { ignoreExpiration: false, algorithms: [ALGO] }, + (err, result) => { + if (err) { + if (err.name === "TokenExpiredError") { + reject(new errs.AuthError("Token has expired", err)); + } else { + reject(err); + } + } else { + tokenData = result; + + // Hack: some tokens out in the wild have a scope of 'all' instead of 'user'. + // For 30 days at least, we need to replace 'all' with user. + if ( + typeof tokenData.scope !== "undefined" && + _.indexOf(tokenData.scope, "all") !== -1 + ) { + tokenData.scope = ["user"]; + } + + resolve(tokenData); + } + }, + ); + } + } catch (err) { + reject(err); + } + }); + }, + + /** + * Does the token have the specified scope? + * + * @param {String} scope + * @returns {Boolean} + */ + hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1, + + /** + * @param {String} key + * @return {*} + */ + get: (key) => { + if (typeof tokenData[key] !== "undefined") { + return tokenData[key]; + } + + return null; + }, + + /** + * @param {String} key + * @param {*} value + */ + set: (key, value) => { + tokenData[key] = value; + }, + + /** + * @param [defaultValue] + * @returns {Integer} + */ + getUserId: (defaultValue) => { + const attrs = self.get("attrs"); + if (attrs?.id) { + return attrs.id; + } + + return defaultValue || 0; + }, + }; + + return self; +}; diff --git a/backend/models/user.js b/backend/models/user.js new file mode 100644 index 0000000..68a3144 --- /dev/null +++ b/backend/models/user.js @@ -0,0 +1,65 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import now from "./now_helper.js"; +import UserPermission from "./user_permission.js"; + +Model.knex(db()); + +const boolFields = ["is_deleted", "is_disabled"]; + +class User extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for roles + if (typeof this.roles === "undefined") { + this.roles = []; + } + } + + $beforeUpdate() { + this.modified_on = now(); + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "User"; + } + + static get tableName() { + return "user"; + } + + static get jsonAttributes() { + return ["roles"]; + } + + static get relationMappings() { + return { + permissions: { + relation: Model.HasOneRelation, + modelClass: UserPermission, + join: { + from: "user.id", + to: "user_permission.user_id", + }, + }, + }; + } +} + +export default User; diff --git a/backend/models/user_permission.js b/backend/models/user_permission.js new file mode 100644 index 0000000..d878471 --- /dev/null +++ b/backend/models/user_permission.js @@ -0,0 +1,29 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import now from "./now_helper.js"; + +Model.knex(db()); + +class UserPermission extends Model { + $beforeInsert () { + this.created_on = now(); + this.modified_on = now(); + } + + $beforeUpdate () { + this.modified_on = now(); + } + + static get name () { + return 'UserPermission'; + } + + static get tableName () { + return 'user_permission'; + } +} + +export default UserPermission; diff --git a/backend/models/wg_client.js b/backend/models/wg_client.js new file mode 100644 index 0000000..9c38afc --- /dev/null +++ b/backend/models/wg_client.js @@ -0,0 +1,43 @@ +// WireGuard Client model - simple Knex queries +// Used directly in internal/wireguard.js via knex + +const tableName = "wg_client"; + +export default { + tableName, + + async getAll(knex) { + return knex(tableName).orderBy("created_on", "desc"); + }, + + async get(knex, id) { + return knex(tableName).where("id", id).first(); + }, + + async create(knex, data) { + const [id] = await knex(tableName).insert({ + ...data, + created_on: knex.fn.now(), + modified_on: knex.fn.now(), + }); + return knex(tableName).where("id", id).first(); + }, + + async update(knex, id, data) { + return knex(tableName).where("id", id).update({ + ...data, + modified_on: knex.fn.now(), + }); + }, + + async delete(knex, id) { + return knex(tableName).where("id", id).del(); + }, + + async toggle(knex, id, enabled) { + return knex(tableName).where("id", id).update({ + enabled: enabled, + modified_on: knex.fn.now(), + }); + }, +}; diff --git a/backend/models/wg_interface.js b/backend/models/wg_interface.js new file mode 100644 index 0000000..7caf222 --- /dev/null +++ b/backend/models/wg_interface.js @@ -0,0 +1,20 @@ +// WireGuard Interface model - simple Knex queries (no Objection.js for simplicity) +// Used directly in internal/wireguard.js via knex +// This file exports table name and basic query helpers + +const tableName = "wg_interface"; + +export default { + tableName, + + async get(knex) { + return knex(tableName).first(); + }, + + async update(knex, id, data) { + return knex(tableName).where("id", id).update({ + ...data, + modified_on: knex.fn.now(), + }); + }, +}; diff --git a/backend/nodemon.json b/backend/nodemon.json new file mode 100644 index 0000000..90223a2 --- /dev/null +++ b/backend/nodemon.json @@ -0,0 +1,7 @@ +{ + "verbose": false, + "ignore": [ + "data" + ], + "ext": "js json ejs cjs" +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..e31bf22 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "nginx-proxy-manager", + "version": "2.0.0", + "description": "A beautiful interface for creating Nginx endpoints", + "author": "Jamie Curnow ", + "license": "MIT", + "main": "index.js", + "type": "module", + "scripts": { + "lint": "biome lint", + "prettier": "biome format --write .", + "validate-schema": "node validate-schema.js", + "regenerate-config": "node scripts/regenerate-config" + }, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^15.3.1", + "ajv": "^8.18.0", + "archiver": "^7.0.1", + "batchflow": "^0.4.0", + "bcrypt": "^6.0.0", + "better-sqlite3": "^12.6.2", + "body-parser": "^2.2.2", + "compression": "^1.8.1", + "express": "^5.2.1", + "express-fileupload": "^1.5.2", + "gravatar": "^1.8.2", + "jsonwebtoken": "^9.0.3", + "knex": "3.1.0", + "liquidjs": "10.24.0", + "lodash": "^4.17.23", + "moment": "^2.30.1", + "mysql2": "^3.18.2", + "node-rsa": "^1.1.1", + "objection": "3.1.5", + "otplib": "^13.3.0", + "path": "^0.12.7", + "pg": "^8.19.0", + "proxy-agent": "^6.5.0", + "signale": "1.4.0", + "sqlite3": "^5.1.7", + "temp-write": "^6.0.1" + }, + "devDependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "@biomejs/biome": "^2.4.5", + "chalk": "5.6.2", + "nodemon": "^3.1.14" + }, + "signale": { + "displayDate": true, + "displayTimestamp": true + } +} diff --git a/backend/routes/audit-log.js b/backend/routes/audit-log.js new file mode 100644 index 0000000..c40b162 --- /dev/null +++ b/backend/routes/audit-log.js @@ -0,0 +1,107 @@ +import express from "express"; +import internalAuditLog from "../internal/audit-log.js"; +import jwtdecode from "../lib/express/jwt-decode.js"; +import validator from "../lib/validator/index.js"; +import { debug, express as logger } from "../logger.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/audit-log + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/audit-log + * + * Retrieve all logs + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalAuditLog.getAll(res.locals.access, data.expand, data.query); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific audit log entry + * + * /api/audit-log/123 + */ +router + .route("/:event_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/audit-log/123 + * + * Retrieve a specific entry + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["event_id"], + additionalProperties: false, + properties: { + event_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + event_id: req.params.event_id, + expand: + typeof req.query.expand === "string" + ? req.query.expand.split(",") + : null, + }, + ); + + const item = await internalAuditLog.get(res.locals.access, { + id: data.event_id, + expand: data.expand, + }); + res.status(200).send(item); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/main.js b/backend/routes/main.js new file mode 100644 index 0000000..a91cf2e --- /dev/null +++ b/backend/routes/main.js @@ -0,0 +1,70 @@ +import express from "express"; +import errs from "../lib/error.js"; +import pjson from "../package.json" with { type: "json" }; +import { isSetup } from "../setup.js"; +import auditLogRoutes from "./audit-log.js"; +import accessListsRoutes from "./nginx/access_lists.js"; +import certificatesHostsRoutes from "./nginx/certificates.js"; +import deadHostsRoutes from "./nginx/dead_hosts.js"; +import proxyHostsRoutes from "./nginx/proxy_hosts.js"; +import redirectionHostsRoutes from "./nginx/redirection_hosts.js"; +import streamsRoutes from "./nginx/streams.js"; +import reportsRoutes from "./reports.js"; +import schemaRoutes from "./schema.js"; +import settingsRoutes from "./settings.js"; +import tokensRoutes from "./tokens.js"; +import usersRoutes from "./users.js"; +import versionRoutes from "./version.js"; +import wireguardRoutes from "./wireguard.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * Health Check + * GET /api + */ +router.get("/", async (_, res /*, next*/) => { + const version = pjson.version.split("-").shift().split("."); + const setup = await isSetup(); + + res.status(200).send({ + status: "OK", + setup, + version: { + major: Number.parseInt(version.shift(), 10), + minor: Number.parseInt(version.shift(), 10), + revision: Number.parseInt(version.shift(), 10), + }, + }); +}); + +router.use("/schema", schemaRoutes); +router.use("/tokens", tokensRoutes); +router.use("/users", usersRoutes); +router.use("/audit-log", auditLogRoutes); +router.use("/reports", reportsRoutes); +router.use("/settings", settingsRoutes); +router.use("/version", versionRoutes); +router.use("/nginx/proxy-hosts", proxyHostsRoutes); +router.use("/nginx/redirection-hosts", redirectionHostsRoutes); +router.use("/nginx/dead-hosts", deadHostsRoutes); +router.use("/nginx/streams", streamsRoutes); +router.use("/nginx/access-lists", accessListsRoutes); +router.use("/nginx/certificates", certificatesHostsRoutes); +router.use("/wireguard", wireguardRoutes); + +/** + * API 404 for all other routes + * + * ALL /api/* + */ +router.all(/(.+)/, (req, _, next) => { + req.params.page = req.params["0"]; + next(new errs.ItemNotFoundError(req.params.page)); +}); + +export default router; diff --git a/backend/routes/nginx/access_lists.js b/backend/routes/nginx/access_lists.js new file mode 100644 index 0000000..9dfcf7e --- /dev/null +++ b/backend/routes/nginx/access_lists.js @@ -0,0 +1,155 @@ +import express from "express"; +import internalAccessList from "../../internal/access-list.js"; +import jwtdecode from "../../lib/express/jwt-decode.js"; +import apiValidator from "../../lib/validator/api.js"; +import validator from "../../lib/validator/index.js"; +import { debug, express as logger } from "../../logger.js"; +import { getValidationSchema } from "../../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/nginx/access-lists + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/access-lists + * + * Retrieve all access-lists + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalAccessList.getAll(res.locals.access, data.expand, data.query); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/nginx/access-lists + * + * Create a new access-list + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body); + const result = await internalAccessList.create(res.locals.access, payload); + res.status(201).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific access-list + * + * /api/nginx/access-lists/123 + */ +router + .route("/:list_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/access-lists/123 + * + * Retrieve a specific access-list + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["list_id"], + additionalProperties: false, + properties: { + list_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + list_id: req.params.list_id, + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + }, + ); + const row = await internalAccessList.get(res.locals.access, { + id: Number.parseInt(data.list_id, 10), + expand: data.expand, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/nginx/access-lists/123 + * + * Update and existing access-list + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body); + payload.id = Number.parseInt(req.params.list_id, 10); + const result = await internalAccessList.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/nginx/access-lists/123 + * + * Delete and existing access-list + */ + .delete(async (req, res, next) => { + try { + const result = await internalAccessList.delete(res.locals.access, { + id: Number.parseInt(req.params.list_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/nginx/certificates.js b/backend/routes/nginx/certificates.js new file mode 100644 index 0000000..99f429b --- /dev/null +++ b/backend/routes/nginx/certificates.js @@ -0,0 +1,355 @@ +import express from "express"; +import dnsPlugins from "../../certbot/dns-plugins.json" with { type: "json" }; +import internalCertificate from "../../internal/certificate.js"; +import errs from "../../lib/error.js"; +import jwtdecode from "../../lib/express/jwt-decode.js"; +import apiValidator from "../../lib/validator/api.js"; +import validator from "../../lib/validator/index.js"; +import { debug, express as logger } from "../../logger.js"; +import { getValidationSchema } from "../../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/nginx/certificates + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/certificates + * + * Retrieve all certificates + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: + typeof req.query.expand === "string" + ? req.query.expand.split(",") + : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalCertificate.getAll( + res.locals.access, + data.expand, + data.query, + ); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/nginx/certificates + * + * Create a new certificate + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator( + getValidationSchema("/nginx/certificates", "post"), + req.body, + ); + req.setTimeout(900000); // 15 minutes timeout + const result = await internalCertificate.create( + res.locals.access, + payload, + ); + res.status(201).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * /api/nginx/certificates/dns-providers + */ +router + .route("/dns-providers") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/certificates/dns-providers + * + * Get list of all supported DNS providers + */ + .get(async (req, res, next) => { + try { + if (!res.locals.access.token.getUserId()) { + throw new errs.PermissionError("Login required"); + } + const clean = Object.keys(dnsPlugins).map((key) => ({ + id: key, + name: dnsPlugins[key].name, + credentials: dnsPlugins[key].credentials, + })); + + clean.sort((a, b) => a.name.localeCompare(b.name)); + res.status(200).send(clean); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Test HTTP challenge for domains + * + * /api/nginx/certificates/test-http + */ +router + .route("/test-http") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/certificates/test-http + * + * Test HTTP challenge for domains + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator( + getValidationSchema("/nginx/certificates/test-http", "post"), + req.body, + ); + req.setTimeout(60000); // 1 minute timeout + + const result = await internalCertificate.testHttpsChallenge( + res.locals.access, + payload, + ); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Validate Certs before saving + * + * /api/nginx/certificates/validate + */ +router + .route("/validate") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/certificates/validate + * + * Validate certificates + */ + .post(async (req, res, next) => { + if (!req.files) { + res.status(400).send({ error: "No files were uploaded" }); + return; + } + + try { + const result = await internalCertificate.validate({ + files: req.files, + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific certificate + * + * /api/nginx/certificates/123 + */ +router + .route("/:certificate_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/certificates/123 + * + * Retrieve a specific certificate + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["certificate_id"], + additionalProperties: false, + properties: { + certificate_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + certificate_id: req.params.certificate_id, + expand: + typeof req.query.expand === "string" + ? req.query.expand.split(",") + : null, + }, + ); + const row = await internalCertificate.get(res.locals.access, { + id: Number.parseInt(data.certificate_id, 10), + expand: data.expand, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/nginx/certificates/123 + * + * Update and existing certificate + */ + .delete(async (req, res, next) => { + try { + const result = await internalCertificate.delete(res.locals.access, { + id: Number.parseInt(req.params.certificate_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Upload Certs + * + * /api/nginx/certificates/123/upload + */ +router + .route("/:certificate_id/upload") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/certificates/123/upload + * + * Upload certificates + */ + .post(async (req, res, next) => { + if (!req.files) { + res.status(400).send({ error: "No files were uploaded" }); + return; + } + + try { + const result = await internalCertificate.upload(res.locals.access, { + id: Number.parseInt(req.params.certificate_id, 10), + files: req.files, + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Renew LE Certs + * + * /api/nginx/certificates/123/renew + */ +router + .route("/:certificate_id/renew") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/certificates/123/renew + * + * Renew certificate + */ + .post(async (req, res, next) => { + req.setTimeout(900000); // 15 minutes timeout + try { + const result = await internalCertificate.renew(res.locals.access, { + id: Number.parseInt(req.params.certificate_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Download LE Certs + * + * /api/nginx/certificates/123/download + */ +router + .route("/:certificate_id/download") + .options((_req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/certificates/123/download + * + * Renew certificate + */ + .get(async (req, res, next) => { + try { + const result = await internalCertificate.download(res.locals.access, { + id: Number.parseInt(req.params.certificate_id, 10), + }); + res.status(200).download(result.fileName); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/nginx/dead_hosts.js b/backend/routes/nginx/dead_hosts.js new file mode 100644 index 0000000..31f7043 --- /dev/null +++ b/backend/routes/nginx/dead_hosts.js @@ -0,0 +1,207 @@ +import express from "express"; +import internalDeadHost from "../../internal/dead-host.js"; +import jwtdecode from "../../lib/express/jwt-decode.js"; +import apiValidator from "../../lib/validator/api.js"; +import validator from "../../lib/validator/index.js"; +import { debug, express as logger } from "../../logger.js"; +import { getValidationSchema } from "../../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/nginx/dead-hosts + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/dead-hosts + * + * Retrieve all dead-hosts + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalDeadHost.getAll(res.locals.access, data.expand, data.query); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/nginx/dead-hosts + * + * Create a new dead-host + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts", "post"), req.body); + const result = await internalDeadHost.create(res.locals.access, payload); + res.status(201).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific dead-host + * + * /api/nginx/dead-hosts/123 + */ +router + .route("/:host_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/dead-hosts/123 + * + * Retrieve a specific dead-host + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["host_id"], + additionalProperties: false, + properties: { + host_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + host_id: req.params.host_id, + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + }, + ); + const row = await internalDeadHost.get(res.locals.access, { + id: Number.parseInt(data.host_id, 10), + expand: data.expand, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/nginx/dead-hosts/123 + * + * Update an existing dead-host + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/dead-hosts/{hostID}", "put"), req.body); + payload.id = Number.parseInt(req.params.host_id, 10); + const result = await internalDeadHost.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/nginx/dead-hosts/123 + * + * Delete a dead-host + */ + .delete(async (req, res, next) => { + try { + const result = await internalDeadHost.delete(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Enable dead-host + * + * /api/nginx/dead-hosts/123/enable + */ +router + .route("/:host_id/enable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/dead-hosts/123/enable + */ + .post(async (req, res, next) => { + try { + const result = await internalDeadHost.enable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Disable dead-host + * + * /api/nginx/dead-hosts/123/disable + */ +router + .route("/:host_id/disable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/dead-hosts/123/disable + */ + .post((req, res, next) => { + try { + const result = internalDeadHost.disable(res.locals.access, { id: Number.parseInt(req.params.host_id, 10) }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/nginx/proxy_hosts.js b/backend/routes/nginx/proxy_hosts.js new file mode 100644 index 0000000..7045a19 --- /dev/null +++ b/backend/routes/nginx/proxy_hosts.js @@ -0,0 +1,209 @@ +import express from "express"; +import internalProxyHost from "../../internal/proxy-host.js"; +import jwtdecode from "../../lib/express/jwt-decode.js"; +import apiValidator from "../../lib/validator/api.js"; +import validator from "../../lib/validator/index.js"; +import { debug, express as logger } from "../../logger.js"; +import { getValidationSchema } from "../../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/nginx/proxy-hosts + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/proxy-hosts + * + * Retrieve all proxy-hosts + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalProxyHost.getAll(res.locals.access, data.expand, data.query); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/nginx/proxy-hosts + * + * Create a new proxy-host + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts", "post"), req.body); + const result = await internalProxyHost.create(res.locals.access, payload); + res.status(201).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err} ${JSON.stringify(err.debug, null, 2)}`); + next(err); + } + }); + +/** + * Specific proxy-host + * + * /api/nginx/proxy-hosts/123 + */ +router + .route("/:host_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/proxy-hosts/123 + * + * Retrieve a specific proxy-host + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["host_id"], + additionalProperties: false, + properties: { + host_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + host_id: req.params.host_id, + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + }, + ); + const row = await internalProxyHost.get(res.locals.access, { + id: Number.parseInt(data.host_id, 10), + expand: data.expand, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/nginx/proxy-hosts/123 + * + * Update and existing proxy-host + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/proxy-hosts/{hostID}", "put"), req.body); + payload.id = Number.parseInt(req.params.host_id, 10); + const result = await internalProxyHost.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/nginx/proxy-hosts/123 + * + * Update and existing proxy-host + */ + .delete(async (req, res, next) => { + try { + const result = await internalProxyHost.delete(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Enable proxy-host + * + * /api/nginx/proxy-hosts/123/enable + */ +router + .route("/:host_id/enable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/proxy-hosts/123/enable + */ + .post(async (req, res, next) => { + try { + const result = await internalProxyHost.enable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Disable proxy-host + * + * /api/nginx/proxy-hosts/123/disable + */ +router + .route("/:host_id/disable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/proxy-hosts/123/disable + */ + .post(async (req, res, next) => { + try { + const result = await internalProxyHost.disable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/nginx/redirection_hosts.js b/backend/routes/nginx/redirection_hosts.js new file mode 100644 index 0000000..9b5b5b3 --- /dev/null +++ b/backend/routes/nginx/redirection_hosts.js @@ -0,0 +1,212 @@ +import express from "express"; +import internalRedirectionHost from "../../internal/redirection-host.js"; +import jwtdecode from "../../lib/express/jwt-decode.js"; +import apiValidator from "../../lib/validator/api.js"; +import validator from "../../lib/validator/index.js"; +import { debug, express as logger } from "../../logger.js"; +import { getValidationSchema } from "../../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/nginx/redirection-hosts + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/redirection-hosts + * + * Retrieve all redirection-hosts + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalRedirectionHost.getAll(res.locals.access, data.expand, data.query); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/nginx/redirection-hosts + * + * Create a new redirection-host + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/redirection-hosts", "post"), req.body); + const result = await internalRedirectionHost.create(res.locals.access, payload); + res.status(201).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific redirection-host + * + * /api/nginx/redirection-hosts/123 + */ +router + .route("/:host_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/redirection-hosts/123 + * + * Retrieve a specific redirection-host + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["host_id"], + additionalProperties: false, + properties: { + host_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + host_id: req.params.host_id, + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + }, + ); + const row = await internalRedirectionHost.get(res.locals.access, { + id: Number.parseInt(data.host_id, 10), + expand: data.expand, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/nginx/redirection-hosts/123 + * + * Update and existing redirection-host + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator( + getValidationSchema("/nginx/redirection-hosts/{hostID}", "put"), + req.body, + ); + payload.id = Number.parseInt(req.params.host_id, 10); + const result = await internalRedirectionHost.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/nginx/redirection-hosts/123 + * + * Update and existing redirection-host + */ + .delete(async (req, res, next) => { + try { + const result = await internalRedirectionHost.delete(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Enable redirection-host + * + * /api/nginx/redirection-hosts/123/enable + */ +router + .route("/:host_id/enable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/redirection-hosts/123/enable + */ + .post(async (req, res, next) => { + try { + const result = await internalRedirectionHost.enable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Disable redirection-host + * + * /api/nginx/redirection-hosts/123/disable + */ +router + .route("/:host_id/disable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/redirection-hosts/123/disable + */ + .post(async (req, res, next) => { + try { + const result = await internalRedirectionHost.disable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/nginx/streams.js b/backend/routes/nginx/streams.js new file mode 100644 index 0000000..dec2e1a --- /dev/null +++ b/backend/routes/nginx/streams.js @@ -0,0 +1,209 @@ +import express from "express"; +import internalStream from "../../internal/stream.js"; +import jwtdecode from "../../lib/express/jwt-decode.js"; +import apiValidator from "../../lib/validator/api.js"; +import validator from "../../lib/validator/index.js"; +import { debug, express as logger } from "../../logger.js"; +import { getValidationSchema } from "../../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/nginx/streams + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/streams + * + * Retrieve all streams + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const rows = await internalStream.getAll(res.locals.access, data.expand, data.query); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/nginx/streams + * + * Create a new stream + */ + .post(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/streams", "post"), req.body); + const result = await internalStream.create(res.locals.access, payload); + res.status(201).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific stream + * + * /api/nginx/streams/123 + */ +router + .route("/:stream_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * GET /api/nginx/streams/123 + * + * Retrieve a specific stream + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["stream_id"], + additionalProperties: false, + properties: { + stream_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + stream_id: req.params.stream_id, + expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null, + }, + ); + const row = await internalStream.get(res.locals.access, { + id: Number.parseInt(data.stream_id, 10), + expand: data.expand, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/nginx/streams/123 + * + * Update and existing stream + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/nginx/streams/{streamID}", "put"), req.body); + payload.id = Number.parseInt(req.params.stream_id, 10); + const result = await internalStream.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/nginx/streams/123 + * + * Update and existing stream + */ + .delete(async (req, res, next) => { + try { + const result = await internalStream.delete(res.locals.access, { + id: Number.parseInt(req.params.stream_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Enable stream + * + * /api/nginx/streams/123/enable + */ +router + .route("/:host_id/enable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/streams/123/enable + */ + .post(async (req, res, next) => { + try { + const result = await internalStream.enable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Disable stream + * + * /api/nginx/streams/123/disable + */ +router + .route("/:host_id/disable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/nginx/streams/123/disable + */ + .post(async (req, res, next) => { + try { + const result = await internalStream.disable(res.locals.access, { + id: Number.parseInt(req.params.host_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/reports.js b/backend/routes/reports.js new file mode 100644 index 0000000..df9962a --- /dev/null +++ b/backend/routes/reports.js @@ -0,0 +1,32 @@ +import express from "express"; +import internalReport from "../internal/report.js"; +import jwtdecode from "../lib/express/jwt-decode.js"; +import { debug, express as logger } from "../logger.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +router + .route("/hosts") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /reports/hosts + */ + .get(async (req, res, next) => { + try { + const data = await internalReport.getHostsReport(res.locals.access); + res.status(200).send(data); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/schema.js b/backend/routes/schema.js new file mode 100644 index 0000000..86bc186 --- /dev/null +++ b/backend/routes/schema.js @@ -0,0 +1,44 @@ +import express from "express"; +import { debug, express as logger } from "../logger.js"; +import PACKAGE from "../package.json" with { type: "json" }; +import { getCompiledSchema } from "../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + + /** + * GET /schema + */ + .get(async (req, res) => { + try { + const swaggerJSON = await getCompiledSchema(); + + let proto = req.protocol; + if (typeof req.headers["x-forwarded-proto"] !== "undefined" && req.headers["x-forwarded-proto"]) { + proto = req.headers["x-forwarded-proto"]; + } + + let origin = `${proto}://${req.hostname}`; + if (typeof req.headers.origin !== "undefined" && req.headers.origin) { + origin = req.headers.origin; + } + + swaggerJSON.info.version = PACKAGE.version; + swaggerJSON.servers[0].url = `${origin}/api`; + res.status(200).send(swaggerJSON); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/settings.js b/backend/routes/settings.js new file mode 100644 index 0000000..ca4af65 --- /dev/null +++ b/backend/routes/settings.js @@ -0,0 +1,101 @@ +import express from "express"; +import internalSetting from "../internal/setting.js"; +import jwtdecode from "../lib/express/jwt-decode.js"; +import apiValidator from "../lib/validator/api.js"; +import validator from "../lib/validator/index.js"; +import { debug, express as logger } from "../logger.js"; +import { getValidationSchema } from "../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/settings + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/settings + * + * Retrieve all settings + */ + .get(async (req, res, next) => { + try { + const rows = await internalSetting.getAll(res.locals.access); + res.status(200).send(rows); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific setting + * + * /api/settings/something + */ +router + .route("/:setting_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /settings/something + * + * Retrieve a specific setting + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["setting_id"], + additionalProperties: false, + properties: { + setting_id: { + type: "string", + minLength: 1, + }, + }, + }, + { + setting_id: req.params.setting_id, + }, + ); + const row = await internalSetting.get(res.locals.access, { + id: data.setting_id, + }); + res.status(200).send(row); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/settings/something + * + * Update and existing setting + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator(getValidationSchema("/settings/{settingID}", "put"), req.body); + payload.id = req.params.setting_id; + const result = await internalSetting.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/tokens.js b/backend/routes/tokens.js new file mode 100644 index 0000000..8a6a1bc --- /dev/null +++ b/backend/routes/tokens.js @@ -0,0 +1,78 @@ +import express from "express"; +import internalToken from "../internal/token.js"; +import jwtdecode from "../lib/express/jwt-decode.js"; +import apiValidator from "../lib/validator/api.js"; +import { debug, express as logger } from "../logger.js"; +import { getValidationSchema } from "../schema/index.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + + /** + * GET /tokens + * + * Get a new Token, given they already have a token they want to refresh + * We also piggy back on to this method, allowing admins to get tokens + * for services like Job board and Worker. + */ + .get(jwtdecode(), async (req, res, next) => { + try { + const data = await internalToken.getFreshToken(res.locals.access, { + expiry: typeof req.query.expiry !== "undefined" ? req.query.expiry : null, + scope: typeof req.query.scope !== "undefined" ? req.query.scope : null, + }); + res.status(200).send(data); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /tokens + * + * Create a new Token + */ + .post(async (req, res, next) => { + try { + const data = await apiValidator(getValidationSchema("/tokens", "post"), req.body); + const result = await internalToken.getTokenFromEmail(data); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +router + .route("/2fa") + .options((_, res) => { + res.sendStatus(204); + }) + + /** + * POST /tokens/2fa + * + * Verify 2FA code and get full token + */ + .post(async (req, res, next) => { + try { + const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body); + const result = await internalToken.verify2FA(challenge_token, code); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/users.js b/backend/routes/users.js new file mode 100644 index 0000000..7c3da8c --- /dev/null +++ b/backend/routes/users.js @@ -0,0 +1,455 @@ +import express from "express"; +import internal2FA from "../internal/2fa.js"; +import internalUser from "../internal/user.js"; +import Access from "../lib/access.js"; +import { isCI } from "../lib/config.js"; +import errs from "../lib/error.js"; +import jwtdecode from "../lib/express/jwt-decode.js"; +import userIdFromMe from "../lib/express/user-id-from-me.js"; +import apiValidator from "../lib/validator/api.js"; +import validator from "../lib/validator/index.js"; +import { debug, express as logger } from "../logger.js"; +import { getValidationSchema } from "../schema/index.js"; +import { isSetup } from "../setup.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/users + */ +router + .route("/") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/users + * + * Retrieve all users + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + additionalProperties: false, + properties: { + expand: { + $ref: "common#/properties/expand", + }, + query: { + $ref: "common#/properties/query", + }, + }, + }, + { + expand: + typeof req.query.expand === "string" + ? req.query.expand.split(",") + : null, + query: typeof req.query.query === "string" ? req.query.query : null, + }, + ); + const users = await internalUser.getAll( + res.locals.access, + data.expand, + data.query, + ); + res.status(200).send(users); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * POST /api/users + * + * Create a new User + */ + .post(async (req, res, next) => { + const body = req.body; + + try { + // If we are in setup mode, we don't check access for current user + const setup = await isSetup(); + if (!setup) { + logger.info("Creating a new user in setup mode"); + const access = new Access(null); + await access.load(true); + res.locals.access = access; + + // We are in setup mode, set some defaults for this first new user, such as making + // them an admin. + body.is_disabled = false; + if (typeof body.roles !== "object" || body.roles === null) { + body.roles = []; + } + if (body.roles.indexOf("admin") === -1) { + body.roles.push("admin"); + } + } + + const payload = await apiValidator( + getValidationSchema("/users", "post"), + body, + ); + const user = await internalUser.create(res.locals.access, payload); + res.status(201).send(user); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/users + * + * Deletes ALL users. This is NOT GENERALLY AVAILABLE! + * (!) It is NOT an authenticated endpoint. + * (!) Only CI should be able to call this endpoint. As a result, + * + * it will only work when the env vars DEBUG=true and CI=true + * + * Do NOT set those env vars in a production environment! + */ + .delete(async (_, res, next) => { + if (isCI()) { + try { + logger.warn("Deleting all users - CI environment detected, allowing this operation"); + await internalUser.deleteAll(); + res.status(200).send(true); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + return; + } + + next(new errs.ItemNotFoundError()); + }); + +/** + * Specific user + * + * /api/users/123 + */ +router + .route("/:user_id") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * GET /users/123 or /users/me + * + * Retrieve a specific user + */ + .get(async (req, res, next) => { + try { + const data = await validator( + { + required: ["user_id"], + additionalProperties: false, + properties: { + user_id: { + $ref: "common#/properties/id", + }, + expand: { + $ref: "common#/properties/expand", + }, + }, + }, + { + user_id: req.params.user_id, + expand: + typeof req.query.expand === "string" + ? req.query.expand.split(",") + : null, + }, + ); + + const user = await internalUser.get(res.locals.access, { + id: data.user_id, + expand: data.expand, + omit: internalUser.getUserOmisionsByAccess( + res.locals.access, + data.user_id, + ), + }); + res.status(200).send(user); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * PUT /api/users/123 + * + * Update and existing user + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator( + getValidationSchema("/users/{userID}", "put"), + req.body, + ); + payload.id = req.params.user_id; + const result = await internalUser.update(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/users/123 + * + * Update and existing user + */ + .delete(async (req, res, next) => { + try { + const result = await internalUser.delete(res.locals.access, { + id: req.params.user_id, + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific user auth + * + * /api/users/123/auth + */ +router + .route("/:user_id/auth") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * PUT /api/users/123/auth + * + * Update password for a user + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator( + getValidationSchema("/users/{userID}/auth", "put"), + req.body, + ); + payload.id = req.params.user_id; + const result = await internalUser.setPassword(res.locals.access, payload); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific user permissions + * + * /api/users/123/permissions + */ +router + .route("/:user_id/permissions") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * PUT /api/users/123/permissions + * + * Set some or all permissions for a user + */ + .put(async (req, res, next) => { + try { + const payload = await apiValidator( + getValidationSchema("/users/{userID}/permissions", "put"), + req.body, + ); + payload.id = req.params.user_id; + const result = await internalUser.setPermissions( + res.locals.access, + payload, + ); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * Specific user login as + * + * /api/users/123/login + */ +router + .route("/:user_id/login") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * POST /api/users/123/login + * + * Log in as a user + */ + .post(async (req, res, next) => { + try { + const result = await internalUser.loginAs(res.locals.access, { + id: Number.parseInt(req.params.user_id, 10), + }); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * User 2FA status + * + * /api/users/123/2fa + */ +router + .route("/:user_id/2fa") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * POST /api/users/123/2fa + * + * Start 2FA setup, returns QR code URL + */ + .post(async (req, res, next) => { + try { + const result = await internal2FA.startSetup(res.locals.access, req.params.user_id); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * GET /api/users/123/2fa + * + * Get 2FA status for a user + */ + .get(async (req, res, next) => { + try { + const status = await internal2FA.getStatus(res.locals.access, req.params.user_id); + res.status(200).send(status); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }) + + /** + * DELETE /api/users/123/2fa?code=XXXXXX + * + * Disable 2FA for a user + */ + .delete(async (req, res, next) => { + try { + const code = typeof req.query.code === "string" ? req.query.code : null; + if (!code) { + throw new errs.ValidationError("Missing required parameter: code"); + } + await internal2FA.disable(res.locals.access, req.params.user_id, code); + res.status(200).send(true); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * User 2FA enable + * + * /api/users/123/2fa/enable + */ +router + .route("/:user_id/2fa/enable") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * POST /api/users/123/2fa/enable + * + * Verify code and enable 2FA + */ + .post(async (req, res, next) => { + try { + const { code } = await apiValidator( + getValidationSchema("/users/{userID}/2fa/enable", "post"), + req.body, + ); + const result = await internal2FA.enable(res.locals.access, req.params.user_id, code); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +/** + * User 2FA backup codes + * + * /api/users/123/2fa/backup-codes + */ +router + .route("/:user_id/2fa/backup-codes") + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + .all(userIdFromMe) + + /** + * POST /api/users/123/2fa/backup-codes + * + * Regenerate backup codes + */ + .post(async (req, res, next) => { + try { + const { code } = await apiValidator( + getValidationSchema("/users/{userID}/2fa/backup-codes", "post"), + req.body, + ); + const result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code); + res.status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + +export default router; diff --git a/backend/routes/version.js b/backend/routes/version.js new file mode 100644 index 0000000..266e56f --- /dev/null +++ b/backend/routes/version.js @@ -0,0 +1,40 @@ +import express from "express"; +import internalRemoteVersion from "../internal/remote-version.js"; +import { debug, express as logger } from "../logger.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/version/check + */ +router + .route("/check") + .options((_, res) => { + res.sendStatus(204); + }) + + /** + * GET /api/version/check + * + * Check for available updates + */ + .get(async (req, res, _next) => { + try { + const data = await internalRemoteVersion.get(); + res.status(200).send(data); + } catch (error) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`); + // Send 200 even though there's an error to avoid triggering update checks repeatedly + res.status(200).send({ + current: null, + latest: null, + update_available: false, + }); + } + }); + +export default router; diff --git a/backend/routes/wireguard.js b/backend/routes/wireguard.js new file mode 100644 index 0000000..8fb9cb4 --- /dev/null +++ b/backend/routes/wireguard.js @@ -0,0 +1,162 @@ +import express from "express"; +import internalWireguard from "../internal/wireguard.js"; +import db from "../db.js"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * GET /api/wireguard + * Get WireGuard interface info + */ +router.get("/", async (req, res, next) => { + try { + const knex = db(); + const iface = await internalWireguard.getInterfaceInfo(knex); + res.status(200).json(iface); + } catch (err) { + next(err); + } +}); + +/** + * GET /api/wireguard/client + * List all WireGuard clients with live status + */ +router.get("/client", async (req, res, next) => { + try { + const knex = db(); + const clients = await internalWireguard.getClients(knex); + res.status(200).json(clients); + } catch (err) { + next(err); + } +}); + +/** + * POST /api/wireguard/client + * Create a new WireGuard client + */ +router.post("/client", async (req, res, next) => { + try { + const knex = db(); + const client = await internalWireguard.createClient(knex, req.body); + res.status(201).json(client); + } catch (err) { + next(err); + } +}); + +/** + * GET /api/wireguard/client/:id + * Get a specific WireGuard client + */ +router.get("/client/:id", async (req, res, next) => { + try { + const knex = db(); + const client = await knex("wg_client").where("id", req.params.id).first(); + if (!client) { + return res.status(404).json({ error: { message: "Client not found" } }); + } + res.status(200).json(client); + } catch (err) { + next(err); + } +}); + +/** + * PUT /api/wireguard/client/:id + * Update a WireGuard client + */ +router.put("/client/:id", async (req, res, next) => { + try { + const knex = db(); + const client = await internalWireguard.updateClient(knex, req.params.id, req.body); + res.status(200).json(client); + } catch (err) { + next(err); + } +}); + +/** + * DELETE /api/wireguard/client/:id + * Delete a WireGuard client + */ +router.delete("/client/:id", async (req, res, next) => { + try { + const knex = db(); + const result = await internalWireguard.deleteClient(knex, req.params.id); + res.status(200).json(result); + } catch (err) { + next(err); + } +}); + +/** + * POST /api/wireguard/client/:id/enable + * Enable a WireGuard client + */ +router.post("/client/:id/enable", async (req, res, next) => { + try { + const knex = db(); + const client = await internalWireguard.toggleClient(knex, req.params.id, true); + res.status(200).json(client); + } catch (err) { + next(err); + } +}); + +/** + * POST /api/wireguard/client/:id/disable + * Disable a WireGuard client + */ +router.post("/client/:id/disable", async (req, res, next) => { + try { + const knex = db(); + const client = await internalWireguard.toggleClient(knex, req.params.id, false); + res.status(200).json(client); + } catch (err) { + next(err); + } +}); + +/** + * GET /api/wireguard/client/:id/configuration + * Download WireGuard client configuration file + */ +router.get("/client/:id/configuration", async (req, res, next) => { + try { + const knex = db(); + const client = await knex("wg_client").where("id", req.params.id).first(); + if (!client) { + return res.status(404).json({ error: { message: "Client not found" } }); + } + const config = await internalWireguard.getClientConfiguration(knex, req.params.id); + const safeName = client.name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32); + res.set("Content-Disposition", `attachment; filename="${safeName}.conf"`); + res.set("Content-Type", "text/plain"); + res.status(200).send(config); + } catch (err) { + next(err); + } +}); + +/** + * GET /api/wireguard/client/:id/qrcode.svg + * Get QR code SVG for client configuration + */ +router.get("/client/:id/qrcode.svg", async (req, res, next) => { + try { + const knex = db(); + const svg = await internalWireguard.getClientQRCode(knex, req.params.id); + res.set("Content-Type", "image/svg+xml"); + res.status(200).send(svg); + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/backend/schema/common.json b/backend/schema/common.json new file mode 100644 index 0000000..00b06e0 --- /dev/null +++ b/backend/schema/common.json @@ -0,0 +1,241 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common", + "type": "object", + "properties": { + "id": { + "description": "Unique identifier", + "readOnly": true, + "type": "integer", + "minimum": 1, + "example": 11 + }, + "expand": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + }, + "query": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + ] + }, + "created_on": { + "description": "Date and time of creation", + "readOnly": true, + "type": "string", + "example": "2025-10-28T04:17:54.000Z" + }, + "modified_on": { + "description": "Date and time of last update", + "readOnly": true, + "type": "string", + "example": "2025-10-28T04:17:54.000Z" + }, + "user_id": { + "description": "User ID", + "type": "integer", + "minimum": 1, + "example": 2 + }, + "certificate_id": { + "description": "Certificate ID", + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "example": 5 + }, + { + "type": "string", + "pattern": "^new$", + "example": "new" + } + ], + "example": 5 + }, + "access_list_id": { + "description": "Access List ID", + "type": "integer", + "minimum": 0, + "example": 3 + }, + "domain_names": { + "description": "Domain Names separated by a comma", + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" + }, + "example": ["example.com", "www.example.com"] + }, + "enabled": { + "description": "Is Enabled", + "type": "boolean", + "example": false + }, + "ssl_forced": { + "description": "Is SSL Forced", + "type": "boolean", + "example": true + }, + "hsts_enabled": { + "description": "Is HSTS Enabled", + "type": "boolean", + "example": true + }, + "hsts_subdomains": { + "description": "Is HSTS applicable to all subdomains", + "type": "boolean", + "example": true + }, + "ssl_provider": { + "type": "string", + "pattern": "^(letsencrypt|other)$", + "example": "letsencrypt" + }, + "http2_support": { + "description": "HTTP2 Protocol Support", + "type": "boolean", + "example": true + }, + "block_exploits": { + "description": "Should we block common exploits", + "type": "boolean", + "example": false + }, + "caching_enabled": { + "description": "Should we cache assets", + "type": "boolean", + "example": true + }, + "email": { + "description": "Email address", + "type": "string", + "pattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$", + "example": "me@example.com" + }, + "directive": { + "type": "string", + "enum": ["allow", "deny"], + "example": "allow" + }, + "address": { + "oneOf": [ + { + "type": "string", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" + }, + { + "type": "string", + "pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$" + }, + { + "type": "string", + "pattern": "^all$" + } + ], + "example": "192.168.0.11" + }, + "access_items": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string" + } + }, + "example": { + "username": "admin", + "password": "pass" + } + }, + "example": [ + { + "username": "admin", + "password": "pass" + } + ] + }, + "access_clients": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "address": { + "$ref": "#/properties/address" + }, + "directive": { + "$ref": "#/properties/directive" + } + }, + "example": { + "directive": "allow", + "address": "192.168.0.0/24" + } + }, + "example": [ + { + "directive": "allow", + "address": "192.168.0.0/24" + } + ] + }, + "certificate_files": { + "description": "Certificate Files", + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "string", + "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" + }, + "certificate_key": { + "type": "string", + "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" + }, + "intermediate_certificate": { + "type": "string", + "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" + } + } + }, + "example": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----", + "certificate_key": "-----BEGIN PRIVATE\nMIID...-----END CERTIFICATE-----" + } + } + } + } + } +} diff --git a/backend/schema/components/access-list-object.json b/backend/schema/components/access-list-object.json new file mode 100644 index 0000000..d80eb06 --- /dev/null +++ b/backend/schema/components/access-list-object.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "description": "Access List object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "name", "meta", "satisfy_any", "pass_auth", "proxy_host_count"], + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "name": { + "type": "string", + "minLength": 1, + "example": "My Access List" + }, + "meta": { + "type": "object", + "example": {} + }, + "satisfy_any": { + "type": "boolean", + "example": true + }, + "pass_auth": { + "type": "boolean", + "example": false + }, + "proxy_host_count": { + "type": "integer", + "minimum": 0, + "example": 3 + } + } +} diff --git a/backend/schema/components/audit-log-list.json b/backend/schema/components/audit-log-list.json new file mode 100644 index 0000000..7436852 --- /dev/null +++ b/backend/schema/components/audit-log-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Audit Log list", + "items": { + "$ref": "./audit-log-object.json" + } +} diff --git a/backend/schema/components/audit-log-object.json b/backend/schema/components/audit-log-object.json new file mode 100644 index 0000000..307cac8 --- /dev/null +++ b/backend/schema/components/audit-log-object.json @@ -0,0 +1,47 @@ +{ + "type": "object", + "description": "Audit Log object", + "required": [ + "id", + "created_on", + "modified_on", + "user_id", + "object_type", + "object_id", + "action", + "meta" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "object_type": { + "type": "string", + "example": "certificate" + }, + "object_id": { + "$ref": "../common.json#/properties/id" + }, + "action": { + "type": "string", + "example": "created" + }, + "meta": { + "type": "object", + "example": {} + }, + "user": { + "$ref": "./user-object.json" + } + } +} diff --git a/backend/schema/components/certificate-list.json b/backend/schema/components/certificate-list.json new file mode 100644 index 0000000..cec4db8 --- /dev/null +++ b/backend/schema/components/certificate-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Certificates list", + "items": { + "$ref": "./certificate-object.json" + } +} diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json new file mode 100644 index 0000000..80cd92b --- /dev/null +++ b/backend/schema/components/certificate-object.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "description": "Certificate object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "provider", "nice_name", "domain_names", "expires_on", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "provider": { + "$ref": "../common.json#/properties/ssl_provider" + }, + "nice_name": { + "type": "string", + "description": "Nice Name for the custom certificate", + "example": "My Custom Cert" + }, + "domain_names": { + "description": "Domain Names separated by a comma", + "type": "array", + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" + }, + "example": ["example.com", "www.example.com"] + }, + "expires_on": { + "description": "Date and time of expiration", + "readOnly": true, + "type": "string", + "example": "2025-10-28T04:17:54.000Z" + }, + "owner": { + "$ref": "./user-object.json" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "certificate": { + "type": "string", + "minLength": 1 + }, + "certificate_key": { + "type": "string", + "minLength": 1 + }, + "dns_challenge": { + "type": "boolean" + }, + "dns_provider_credentials": { + "type": "string" + }, + "dns_provider": { + "type": "string" + }, + "letsencrypt_certificate": { + "type": "object" + }, + "propagation_seconds": { + "type": "integer", + "minimum": 0 + }, + "key_type": { + "type": "string", + "enum": ["rsa", "ecdsa"], + "default": "rsa" + } + }, + "example": { + "dns_challenge": false + } + } + } +} diff --git a/backend/schema/components/check-version-object.json b/backend/schema/components/check-version-object.json new file mode 100644 index 0000000..ef2ffac --- /dev/null +++ b/backend/schema/components/check-version-object.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "description": "Check Version object", + "additionalProperties": false, + "required": ["current", "latest", "update_available"], + "properties": { + "current": { + "type": ["string", "null"], + "description": "Current version string", + "example": "v2.10.1" + }, + "latest": { + "type": ["string", "null"], + "description": "Latest version string", + "example": "v2.13.4" + }, + "update_available": { + "type": "boolean", + "description": "Whether there's an update available", + "example": true + } + } +} diff --git a/backend/schema/components/dead-host-list.json b/backend/schema/components/dead-host-list.json new file mode 100644 index 0000000..56ff303 --- /dev/null +++ b/backend/schema/components/dead-host-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "404 Hosts list", + "items": { + "$ref": "./dead-host-object.json" + } +} diff --git a/backend/schema/components/dead-host-object.json b/backend/schema/components/dead-host-object.json new file mode 100644 index 0000000..b876ebf --- /dev/null +++ b/backend/schema/components/dead-host-object.json @@ -0,0 +1,64 @@ +{ + "type": "object", + "description": "404 Host object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "domain_names", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "advanced_config", "enabled", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "$ref": "../common.json#/properties/domain_names" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "advanced_config": { + "type": "string", + "example": "" + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "meta": { + "type": "object", + "example": {} + }, + "certificate": { + "oneOf": [ + { + "type": "null", + "example": null + }, + { + "$ref": "./certificate-object.json" + } + ], + "example": null + }, + "owner": { + "$ref": "./user-object.json" + } + } +} diff --git a/backend/schema/components/dns-providers-list.json b/backend/schema/components/dns-providers-list.json new file mode 100644 index 0000000..c240db1 --- /dev/null +++ b/backend/schema/components/dns-providers-list.json @@ -0,0 +1,23 @@ +{ + "type": "array", + "description": "DNS Providers list", + "items": { + "type": "object", + "required": ["id", "name", "credentials"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the DNS provider, matching the python package" + }, + "name": { + "type": "string", + "description": "Human-readable name of the DNS provider" + }, + "credentials": { + "type": "string", + "description": "Instructions on how to format the credentials for this DNS provider" + } + } + } +} diff --git a/backend/schema/components/error-object.json b/backend/schema/components/error-object.json new file mode 100644 index 0000000..6f350a0 --- /dev/null +++ b/backend/schema/components/error-object.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "description": "Error object", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "Bad Request" + } + } +} diff --git a/backend/schema/components/error.json b/backend/schema/components/error.json new file mode 100644 index 0000000..ceb3e14 --- /dev/null +++ b/backend/schema/components/error.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "description": "Error", + "properties": { + "error": { + "$ref": "./error-object.json" + } + } +} diff --git a/backend/schema/components/health-object.json b/backend/schema/components/health-object.json new file mode 100644 index 0000000..592ead2 --- /dev/null +++ b/backend/schema/components/health-object.json @@ -0,0 +1,46 @@ +{ + "type": "object", + "description": "Health object", + "additionalProperties": false, + "required": ["status", "version"], + "properties": { + "status": { + "type": "string", + "description": "Healthy", + "example": "OK" + }, + "setup": { + "type": "boolean", + "description": "Whether the initial setup has been completed", + "example": true + }, + "version": { + "type": "object", + "description": "The version object", + "example": { + "major": 2, + "minor": 0, + "revision": 0 + }, + "additionalProperties": false, + "required": ["major", "minor", "revision"], + "properties": { + "major": { + "type": "integer", + "minimum": 0, + "example": 2 + }, + "minor": { + "type": "integer", + "minimum": 0, + "example": 10 + }, + "revision": { + "type": "integer", + "minimum": 0, + "example": 1 + } + } + } + } +} diff --git a/backend/schema/components/permission-object.json b/backend/schema/components/permission-object.json new file mode 100644 index 0000000..cae9d26 --- /dev/null +++ b/backend/schema/components/permission-object.json @@ -0,0 +1,48 @@ +{ + "type": "object", + "minProperties": 1, + "properties": { + "visibility": { + "type": "string", + "description": "Visibility Type", + "enum": ["all", "user"], + "example": "all" + }, + "access_lists": { + "type": "string", + "description": "Access Lists Permissions", + "enum": ["hidden", "view", "manage"], + "example": "view" + }, + "dead_hosts": { + "type": "string", + "description": "404 Hosts Permissions", + "enum": ["hidden", "view", "manage"], + "example": "manage" + }, + "proxy_hosts": { + "type": "string", + "description": "Proxy Hosts Permissions", + "enum": ["hidden", "view", "manage"], + "example": "hidden" + }, + "redirection_hosts": { + "type": "string", + "description": "Redirection Permissions", + "enum": ["hidden", "view", "manage"], + "example": "view" + }, + "streams": { + "type": "string", + "description": "Streams Permissions", + "enum": ["hidden", "view", "manage"], + "example": "manage" + }, + "certificates": { + "type": "string", + "description": "Certificates Permissions", + "enum": ["hidden", "view", "manage"], + "example": "hidden" + } + } +} diff --git a/backend/schema/components/proxy-host-list.json b/backend/schema/components/proxy-host-list.json new file mode 100644 index 0000000..39789b4 --- /dev/null +++ b/backend/schema/components/proxy-host-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Proxy Hosts list", + "items": { + "$ref": "./proxy-host-object.json" + } +} diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json new file mode 100644 index 0000000..3ac6462 --- /dev/null +++ b/backend/schema/components/proxy-host-object.json @@ -0,0 +1,178 @@ +{ + "type": "object", + "description": "Proxy Host object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "domain_names", + "forward_host", + "forward_port", + "access_list_id", + "certificate_id", + "ssl_forced", + "caching_enabled", + "block_exploits", + "advanced_config", + "meta", + "allow_websocket_upgrade", + "http2_support", + "forward_scheme", + "enabled", + "locations", + "hsts_enabled", + "hsts_subdomains", + "trust_forwarded_proto" + ], + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "$ref": "../common.json#/properties/domain_names" + }, + "forward_host": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "127.0.0.1" + }, + "forward_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "example": 8080 + }, + "access_list_id": { + "$ref": "../common.json#/properties/access_list_id" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "caching_enabled": { + "$ref": "../common.json#/properties/caching_enabled" + }, + "block_exploits": { + "$ref": "../common.json#/properties/block_exploits" + }, + "advanced_config": { + "type": "string", + "example": "" + }, + "meta": { + "type": "object", + "example": { + "nginx_online": true, + "nginx_err": null + } + }, + "allow_websocket_upgrade": { + "description": "Allow Websocket Upgrade for all paths", + "type": "boolean", + "example": true + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "forward_scheme": { + "type": "string", + "enum": ["http", "https"], + "example": "http" + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "locations": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "required": ["forward_scheme", "forward_host", "forward_port", "path"], + "additionalProperties": false, + "properties": { + "id": { + "type": ["integer", "null"] + }, + "path": { + "type": "string", + "minLength": 1 + }, + "forward_scheme": { + "$ref": "#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "#/properties/forward_host" + }, + "forward_port": { + "$ref": "#/properties/forward_port" + }, + "forward_path": { + "type": "string" + }, + "advanced_config": { + "type": "string" + } + } + }, + "example": [ + { + "path": "/app", + "forward_scheme": "http", + "forward_host": "example.com", + "forward_port": 80 + } + ] + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "trust_forwarded_proto":{ + "type": "boolean", + "description": "Trust the forwarded headers", + "example": false + }, + "certificate": { + "oneOf": [ + { + "type": "null", + "example": null + }, + { + "$ref": "./certificate-object.json" + } + ], + "example": null + }, + "owner": { + "$ref": "./user-object.json" + }, + "access_list": { + "oneOf": [ + { + "type": "null", + "example": null + }, + { + "$ref": "./access-list-object.json" + } + ], + "example": null + } + } +} diff --git a/backend/schema/components/redirection-host-list.json b/backend/schema/components/redirection-host-list.json new file mode 100644 index 0000000..716dcfa --- /dev/null +++ b/backend/schema/components/redirection-host-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Redirection Hosts list", + "items": { + "$ref": "./redirection-host-object.json" + } +} diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json new file mode 100644 index 0000000..5816972 --- /dev/null +++ b/backend/schema/components/redirection-host-object.json @@ -0,0 +1,116 @@ +{ + "type": "object", + "description": "Redirection Host object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "domain_names", + "forward_http_code", + "forward_scheme", + "forward_domain_name", + "preserve_path", + "certificate_id", + "ssl_forced", + "hsts_enabled", + "hsts_subdomains", + "http2_support", + "block_exploits", + "advanced_config", + "enabled", + "meta" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "$ref": "../common.json#/properties/domain_names" + }, + "forward_http_code": { + "description": "Redirect HTTP Status Code", + "type": "integer", + "minimum": 300, + "maximum": 308, + "example": 302 + }, + "forward_scheme": { + "type": "string", + "enum": [ + "auto", + "http", + "https" + ], + "example": "http" + }, + "forward_domain_name": { + "description": "Domain Name", + "type": "string", + "pattern": "^(?:[^.*]+\\.?)+[^.]$", + "example": "jc21.com" + }, + "preserve_path": { + "description": "Should the path be preserved", + "type": "boolean", + "example": true + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../common.json#/properties/block_exploits" + }, + "advanced_config": { + "type": "string", + "example": "" + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "meta": { + "type": "object", + "example": { + "nginx_online": true, + "nginx_err": null + } + }, + "certificate": { + "oneOf": [ + { + "type": "null", + "example": null + }, + { + "$ref": "./certificate-object.json" + } + ], + "example": null + }, + "owner": { + "$ref": "./user-object.json" + } + } +} diff --git a/backend/schema/components/security-schemes.json b/backend/schema/components/security-schemes.json new file mode 100644 index 0000000..4ae57bd --- /dev/null +++ b/backend/schema/components/security-schemes.json @@ -0,0 +1,8 @@ +{ + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT Bearer Token authentication" + } +} diff --git a/backend/schema/components/setting-list.json b/backend/schema/components/setting-list.json new file mode 100644 index 0000000..c66f099 --- /dev/null +++ b/backend/schema/components/setting-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Setting list", + "items": { + "$ref": "./setting-object.json" + } +} diff --git a/backend/schema/components/setting-object.json b/backend/schema/components/setting-object.json new file mode 100644 index 0000000..b9c6a10 --- /dev/null +++ b/backend/schema/components/setting-object.json @@ -0,0 +1,56 @@ +{ + "type": "object", + "description": "Setting object", + "required": ["id", "name", "description", "value", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "Setting ID", + "minLength": 1, + "example": "default-site" + }, + "name": { + "type": "string", + "description": "Setting Display Name", + "minLength": 1, + "example": "Default Site" + }, + "description": { + "type": "string", + "description": "Meaningful description", + "minLength": 1, + "example": "What to show when Nginx is hit with an unknown Host" + }, + "value": { + "description": "Value in almost any form", + "example": "congratulations", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "integer" + }, + { + "type": "object" + }, + { + "type": "number" + }, + { + "type": "array" + } + ] + }, + "meta": { + "description": "Extra metadata", + "example": { + "redirect": "http://example.com", + "html": "

404

" + }, + "type": "object" + } + } +} diff --git a/backend/schema/components/stream-list.json b/backend/schema/components/stream-list.json new file mode 100644 index 0000000..b6e8b6d --- /dev/null +++ b/backend/schema/components/stream-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Streams list", + "items": { + "$ref": "./stream-object.json" + } +} diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json new file mode 100644 index 0000000..602073c --- /dev/null +++ b/backend/schema/components/stream-object.json @@ -0,0 +1,95 @@ +{ + "type": "object", + "description": "Stream object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "incoming_port", + "forwarding_host", + "forwarding_port", + "tcp_forwarding", + "udp_forwarding", + "enabled", + "meta" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "incoming_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "example": 9090 + }, + "forwarding_host": { + "anyOf": [ + { + "description": "Domain Name", + "type": "string", + "pattern": "^(?:[^.*]+\\.?)+[^.]$", + "example": "example.com" + }, + { + "type": "string", + "format": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" + }, + { + "type": "string", + "format": "ipv6" + } + ], + "example": "example.com" + }, + "forwarding_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "example": 80 + }, + "tcp_forwarding": { + "type": "boolean", + "example": true + }, + "udp_forwarding": { + "type": "boolean", + "example": false + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "meta": { + "type": "object", + "example": {} + }, + "certificate": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "./certificate-object.json" + } + ], + "example": null + }, + "owner": { + "$ref": "./user-object.json" + } + } +} diff --git a/backend/schema/components/token-challenge.json b/backend/schema/components/token-challenge.json new file mode 100644 index 0000000..9d79cd9 --- /dev/null +++ b/backend/schema/components/token-challenge.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "description": "Token object", + "required": ["requires_2fa", "challenge_token"], + "additionalProperties": false, + "properties": { + "requires_2fa": { + "description": "Whether this token request requires two-factor authentication", + "example": true, + "type": "boolean" + }, + "challenge_token": { + "description": "Challenge Token used in subsequent 2FA verification", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "type": "string" + } + } +} diff --git a/backend/schema/components/token-object.json b/backend/schema/components/token-object.json new file mode 100644 index 0000000..6ec4e43 --- /dev/null +++ b/backend/schema/components/token-object.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "description": "Token object", + "required": ["expires", "token"], + "additionalProperties": false, + "properties": { + "expires": { + "description": "Token Expiry ISO Time String", + "example": "2025-02-04T20:40:46.340Z", + "type": "string" + }, + "token": { + "description": "JWT Token", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "type": "string" + } + } +} diff --git a/backend/schema/components/user-list.json b/backend/schema/components/user-list.json new file mode 100644 index 0000000..c5c0f71 --- /dev/null +++ b/backend/schema/components/user-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "User list", + "items": { + "$ref": "./user-object.json" + } +} diff --git a/backend/schema/components/user-object.json b/backend/schema/components/user-object.json new file mode 100644 index 0000000..7acd0a4 --- /dev/null +++ b/backend/schema/components/user-object.json @@ -0,0 +1,116 @@ +{ + "type": "object", + "description": "User object", + "required": ["id", "created_on", "modified_on", "is_disabled", "email", "name", "nickname", "avatar", "roles"], + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "description": "User ID", + "minimum": 1, + "example": 1 + }, + "created_on": { + "type": "string", + "description": "Created Date", + "example": "2020-01-30T09:36:08.000Z" + }, + "modified_on": { + "type": "string", + "description": "Modified Date", + "example": "2020-01-30T09:41:04.000Z" + }, + "is_disabled": { + "type": "boolean", + "description": "Is user Disabled", + "example": true + }, + "email": { + "type": "string", + "description": "Email", + "minLength": 3, + "example": "jc@jc21.com" + }, + "name": { + "type": "string", + "description": "Name", + "minLength": 1, + "example": "Jamie Curnow" + }, + "nickname": { + "type": "string", + "description": "Nickname", + "example": "James" + }, + "avatar": { + "type": "string", + "description": "Gravatar URL based on email, without scheme", + "example": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm" + }, + "roles": { + "description": "Roles applied", + "example": ["admin"], + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "type": "object", + "description": "Permissions if expanded in request", + "required": [ + "visibility", + "proxy_hosts", + "redirection_hosts", + "dead_hosts", + "streams", + "access_lists", + "certificates" + ], + "properties": { + "visibility": { + "type": "string", + "description": "Visibility level", + "example": "all", + "pattern": "^(all|user)$" + }, + "proxy_hosts": { + "type": "string", + "description": "Proxy Hosts access level", + "example": "manage", + "pattern": "^(manage|view|hidden)$" + }, + "redirection_hosts": { + "type": "string", + "description": "Redirection Hosts access level", + "example": "manage", + "pattern": "^(manage|view|hidden)$" + }, + "dead_hosts": { + "type": "string", + "description": "Dead Hosts access level", + "example": "manage", + "pattern": "^(manage|view|hidden)$" + }, + "streams": { + "type": "string", + "description": "Streams access level", + "example": "manage", + "pattern": "^(manage|view|hidden)$" + }, + "access_lists": { + "type": "string", + "description": "Access Lists access level", + "example": "hidden", + "pattern": "^(manage|view|hidden)$" + }, + "certificates": { + "type": "string", + "description": "Certificates access level", + "example": "view", + "pattern": "^(manage|view|hidden)$" + } + } + } + } +} diff --git a/backend/schema/index.js b/backend/schema/index.js new file mode 100644 index 0000000..0478486 --- /dev/null +++ b/backend/schema/index.js @@ -0,0 +1,46 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import $RefParser from "@apidevtools/json-schema-ref-parser"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +let compiledSchema = null; + +/** + * Compiles the schema, by dereferencing it, only once + * and returns the memory cached value + */ +const getCompiledSchema = async () => { + if (compiledSchema === null) { + compiledSchema = await $RefParser.dereference(`${__dirname}/swagger.json`, { + mutateInputSchema: false, + }); + } + return compiledSchema; +}; + +/** + * Scans the schema for the validation schema for the given path and method + * and returns it. + * + * @param {string} path + * @param {string} method + * @returns string|null + */ +const getValidationSchema = (path, method) => { + if ( + compiledSchema !== null && + typeof compiledSchema.paths[path] !== "undefined" && + typeof compiledSchema.paths[path][method] !== "undefined" && + typeof compiledSchema.paths[path][method].requestBody !== "undefined" && + typeof compiledSchema.paths[path][method].requestBody.content !== "undefined" && + typeof compiledSchema.paths[path][method].requestBody.content["application/json"] !== "undefined" && + typeof compiledSchema.paths[path][method].requestBody.content["application/json"].schema !== "undefined" + ) { + return compiledSchema.paths[path][method].requestBody.content["application/json"].schema; + } + return null; +}; + +export { getCompiledSchema, getValidationSchema }; diff --git a/backend/schema/paths/audit-log/get.json b/backend/schema/paths/audit-log/get.json new file mode 100644 index 0000000..62c09ce --- /dev/null +++ b/backend/schema/paths/audit-log/get.json @@ -0,0 +1,53 @@ +{ + "operationId": "getAuditLogs", + "summary": "Get Audit Logs", + "tags": ["audit-log"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 7, + "created_on": "2024-10-08T13:09:54.000Z", + "modified_on": "2024-10-08T13:09:54.000Z", + "user_id": 1, + "object_type": "user", + "object_id": 3, + "action": "updated", + "meta": { + "name": "John Doe", + "permissions": { + "user_id": 3, + "visibility": "all", + "access_lists": "manage", + "dead_hosts": "hidden", + "proxy_hosts": "manage", + "redirection_hosts": "view", + "streams": "hidden", + "certificates": "manage", + "id": 3, + "modified_on": "2024-10-08T13:09:54.000Z", + "created_on": "2024-10-08T13:09:51.000Z" + } + } + } + ] + } + }, + "schema": { + "$ref": "../../components/audit-log-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/audit-log/id/get.json b/backend/schema/paths/audit-log/id/get.json new file mode 100644 index 0000000..38ff1c7 --- /dev/null +++ b/backend/schema/paths/audit-log/id/get.json @@ -0,0 +1,72 @@ +{ + "operationId": "getAuditLog", + "summary": "Get Audit Log Event", + "tags": ["audit-log"], + "security": [ + { + "bearerAuth": [ + "admin" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "Audit Log Event ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2025-09-15T17:27:45.000Z", + "modified_on": "2025-09-15T17:27:45.000Z", + "user_id": 1, + "object_type": "user", + "object_id": 1, + "action": "created", + "meta": { + "id": 1, + "created_on": "2025-09-15T17:27:45.000Z", + "modified_on": "2025-09-15T17:27:45.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie", + "nickname": "Jamie", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ], + "permissions": { + "visibility": "all", + "proxy_hosts": "manage", + "redirection_hosts": "manage", + "dead_hosts": "manage", + "streams": "manage", + "access_lists": "manage", + "certificates": "manage" + } + } + } + } + }, + "schema": { + "$ref": "../../../components/audit-log-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/get.json b/backend/schema/paths/get.json new file mode 100644 index 0000000..9f6ba2a --- /dev/null +++ b/backend/schema/paths/get.json @@ -0,0 +1,30 @@ +{ + "operationId": "health", + "summary": "Returns the API health status", + "tags": ["public"], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "status": "OK", + "setup": true, + "version": { + "major": 2, + "minor": 1, + "revision": 0 + } + } + } + }, + "schema": { + "$ref": "../components/health-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/get.json b/backend/schema/paths/nginx/access-lists/get.json new file mode 100644 index 0000000..ada40f5 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/get.json @@ -0,0 +1,51 @@ +{ + "operationId": "getAccessLists", + "summary": "Get all access lists", + "tags": ["access-lists"], + "security": [ + { + "bearerAuth": [ + "access_lists.view" + ] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": [ + "owner", + "items", + "clients", + "proxy_hosts" + ] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "example": { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "owner_user_id": 1, + "name": "test1234", + "meta": {}, + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0 + }, + "schema": { + "$ref": "../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/listID/delete.json b/backend/schema/paths/nginx/access-lists/listID/delete.json new file mode 100644 index 0000000..182a883 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/listID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteAccessList", + "summary": "Delete a Access List", + "tags": ["access-lists"], + "security": [ + { + "bearerAuth": ["access_lists.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "listID", + "description": "Access List ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/listID/get.json b/backend/schema/paths/nginx/access-lists/listID/get.json new file mode 100644 index 0000000..9705826 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/listID/get.json @@ -0,0 +1,54 @@ +{ + "operationId": "getAccessList", + "summary": "Get a access List", + "tags": [ + "access-lists" + ], + "security": [ + { + "bearerAuth": [ + "access_lists.view" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "listID", + "description": "Access List ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2025-10-28T04:06:55.000Z", + "modified_on": "2025-10-29T22:48:20.000Z", + "owner_user_id": 1, + "name": "My Access List", + "meta": {}, + "satisfy_any": false, + "pass_auth": false, + "proxy_host_count": 1 + } + } + }, + "schema": { + "$ref": "../../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/listID/put.json b/backend/schema/paths/nginx/access-lists/listID/put.json new file mode 100644 index 0000000..61e8044 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/listID/put.json @@ -0,0 +1,142 @@ +{ + "operationId": "updateAccessList", + "summary": "Update a Access List", + "tags": ["access-lists"], + "security": [ + { + "bearerAuth": ["access_lists.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "listID", + "description": "Access List ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Access List Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": { + "$ref": "../../../../components/access-list-object.json#/properties/name" + }, + "satisfy_any": { + "$ref": "../../../../components/access-list-object.json#/properties/satisfy_any" + }, + "pass_auth": { + "$ref": "../../../../components/access-list-object.json#/properties/pass_auth" + }, + "items": { + "$ref": "../../../../common.json#/properties/access_items" + }, + "clients": { + "$ref": "../../../../common.json#/properties/access_clients" + } + } + }, + "example": { + "name": "My Access List", + "satisfy_any": true, + "pass_auth": false, + "items": [ + { + "username": "admin2", + "password": "pass2" + } + ], + "clients": [ + { + "directive": "allow", + "address": "192.168.0.0/24" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:34:34.000Z", + "owner_user_id": 1, + "name": "test123!!", + "meta": {}, + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0, + "owner": { + "id": 1, + "created_on": "2024-10-07T22:43:55.000Z", + "modified_on": "2024-10-08T12:52:54.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "some guy", + "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", + "roles": ["admin"] + }, + "items": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "admin", + "password": "", + "meta": {}, + "hint": "a****" + }, + { + "id": 2, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "asdad", + "password": "", + "meta": {}, + "hint": "a*****" + } + ], + "clients": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "address": "127.0.0.1", + "directive": "allow", + "meta": {} + } + ], + "proxy_hosts": [] + } + } + }, + "schema": { + "$ref": "../../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/post.json b/backend/schema/paths/nginx/access-lists/post.json new file mode 100644 index 0000000..38b7003 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/post.json @@ -0,0 +1,135 @@ +{ + "operationId": "createAccessList", + "summary": "Create a Access List", + "tags": ["access-lists"], + "security": [ + { + "bearerAuth": [ + "access_lists.manage" + ] + } + ], + "requestBody": { + "description": "Access List Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "$ref": "../../../components/access-list-object.json#/properties/name" + }, + "satisfy_any": { + "$ref": "../../../components/access-list-object.json#/properties/satisfy_any" + }, + "pass_auth": { + "$ref": "../../../components/access-list-object.json#/properties/pass_auth" + }, + "items": { + "$ref": "../../../common.json#/properties/access_items" + }, + "clients": { + "$ref": "../../../common.json#/properties/access_clients" + } + } + }, + "example": { + "name": "My Access List", + "satisfy_any": true, + "pass_auth": false, + "items": [ + { + "username": "admin", + "password": "pass" + } + ], + "clients": [ + { + "directive": "allow", + "address": "192.168.0.0/24" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "owner_user_id": 1, + "name": "test1234", + "meta": {}, + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0, + "owner": { + "id": 1, + "created_on": "2024-10-07T22:43:55.000Z", + "modified_on": "2024-10-08T12:52:54.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "some guy", + "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", + "roles": [ + "admin" + ] + }, + "items": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "admin", + "password": "", + "meta": {}, + "hint": "a****" + }, + { + "id": 2, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "asdad", + "password": "", + "meta": {}, + "hint": "a*****" + } + ], + "proxy_hosts": [], + "clients": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "address": "127.0.0.1", + "directive": "allow", + "meta": {} + } + ] + } + } + }, + "schema": { + "$ref": "../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/delete.json b/backend/schema/paths/nginx/certificates/certID/delete.json new file mode 100644 index 0000000..a99f619 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteCertificate", + "summary": "Delete a Certificate", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "description": "Certificate ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/download/get.json b/backend/schema/paths/nginx/certificates/certID/download/get.json new file mode 100644 index 0000000..7c18d01 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/download/get.json @@ -0,0 +1,36 @@ +{ + "operationId": "downloadCertificate", + "summary": "Downloads a Certificate", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "description": "Certificate ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/get.json b/backend/schema/paths/nginx/certificates/certID/get.json new file mode 100644 index 0000000..46afbf8 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/get.json @@ -0,0 +1,52 @@ +{ + "operationId": "getCertificate", + "summary": "Get a Certificate", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.view"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "description": "Certificate ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "modified_on": "2024-10-09T05:32:11.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": ["test.example.com"], + "expires_on": "2025-01-07T04:34:18.000Z", + "meta": { + "dns_challenge": false + } + } + } + }, + "schema": { + "$ref": "../../../../components/certificate-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/renew/post.json b/backend/schema/paths/nginx/certificates/certID/renew/post.json new file mode 100644 index 0000000..4466d38 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/renew/post.json @@ -0,0 +1,52 @@ +{ + "operationId": "renewCertificate", + "summary": "Renews a Certificate", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "description": "Certificate ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires_on": "2025-01-07T06:41:58.000Z", + "modified_on": "2024-10-09T07:39:51.000Z", + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "My Test Cert", + "domain_names": ["test.jc21.supernerd.pro"], + "meta": { + "dns_challenge": false + } + } + } + }, + "schema": { + "$ref": "../../../../../components/certificate-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/upload/post.json b/backend/schema/paths/nginx/certificates/certID/upload/post.json new file mode 100644 index 0000000..2b1ba3e --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/upload/post.json @@ -0,0 +1,65 @@ +{ + "operationId": "uploadCertificate", + "summary": "Uploads a custom Certificate", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "description": "Certificate ID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "requestBody": { + "$ref": "../../../../../common.json#/properties/certificate_files" + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw\ngZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1\ncm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD\nDDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu\nb3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe\nbWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93\nQEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu\nGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+\n2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU\nQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB\nIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE\nOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB\n/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy\nl8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s\nVXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn\nATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt\nIImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u\nm+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV\nXxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp\n1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw\nhp8bJUp/QN7pnOVCDbjTQ+HVMXw=\n-----END CERTIFICATE-----\n", + "certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n" + } + } + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "string", + "minLength": 1, + "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" + }, + "certificate_key": { + "type": "string", + "minLength": 1, + "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" + }, + "intermediate_certificate": { + "type": "string", + "minLength": 1, + "example": "-----BEGIN CERTIFICATE-----\nMIID...-----END CERTIFICATE-----" + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/dns-providers/get.json b/backend/schema/paths/nginx/certificates/dns-providers/get.json new file mode 100644 index 0000000..3efb1a0 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/dns-providers/get.json @@ -0,0 +1,48 @@ +{ + "operationId": "getDNSProviders", + "summary": "Get DNS Providers for Certificates", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.view"] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": "vultr", + "name": "Vultr", + "credentials": "dns_vultr_key = YOUR_VULTR_API_KEY" + }, + { + "id": "websupport", + "name": "Websupport.sk", + "credentials": "dns_websupport_identifier = \ndns_websupport_secret_key = " + }, + { + "id": "wedos", + "name": "Wedos", + "credentials": "dns_wedos_user = \ndns_wedos_auth = " + }, + { + "id": "zoneedit", + "name": "ZoneEdit", + "credentials": "dns_zoneedit_user = \ndns_zoneedit_token = " + } + ] + } + }, + "schema": { + "$ref": "../../../../components/dns-providers-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/get.json b/backend/schema/paths/nginx/certificates/get.json new file mode 100644 index 0000000..5884c97 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/get.json @@ -0,0 +1,52 @@ +{ + "operationId": "getCertificates", + "summary": "Get all certificates", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.view"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "modified_on": "2024-10-09T05:32:11.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": ["test.example.com"], + "expires_on": "2025-01-07T04:34:18.000Z", + "meta": { + "dns_challenge": false + } + } + ] + } + }, + "schema": { + "$ref": "../../../components/certificate-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/post.json b/backend/schema/paths/nginx/certificates/post.json new file mode 100644 index 0000000..15406c8 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/post.json @@ -0,0 +1,101 @@ +{ + "operationId": "createCertificate", + "summary": "Create a Certificate", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.manage"] + } + ], + "requestBody": { + "description": "Certificate Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["provider"], + "properties": { + "provider": { + "$ref": "../../../components/certificate-object.json#/properties/provider" + }, + "nice_name": { + "$ref": "../../../components/certificate-object.json#/properties/nice_name" + }, + "domain_names": { + "$ref": "../../../components/certificate-object.json#/properties/domain_names" + }, + "meta": { + "$ref": "../../../components/certificate-object.json#/properties/meta" + } + } + }, + "example": { + "provider": "letsencrypt", + "domain_names": ["test.example.com"], + "meta": { + "dns_challenge": false + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires_on": "2025-01-07 04:30:17", + "modified_on": "2024-10-09 05:28:51", + "id": 5, + "created_on": "2024-10-09 05:28:35", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": ["test.example.com"], + "meta": { + "dns_challenge": false, + "letsencrypt_certificate": { + "cn": "test.example.com", + "issuer": "C = US, O = Let's Encrypt, CN = E5", + "dates": { + "from": 1728448218, + "to": 1736224217 + } + } + } + } + } + }, + "schema": { + "$ref": "../../../components/certificate-object.json" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Domains are invalid" + } + } + } + }, + "schema": { + "$ref": "../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/test-http/post.json b/backend/schema/paths/nginx/certificates/test-http/post.json new file mode 100644 index 0000000..4d738d0 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/test-http/post.json @@ -0,0 +1,46 @@ +{ + "operationId": "testHttpReach", + "summary": "Test HTTP Reachability", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.view"] + } + ], + "requestBody": { + "description": "Test Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["domains"], + "properties": { + "domains": { + "$ref": "../../../../common.json#/properties/domain_names" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "test.example.org": "ok", + "test.example.com": "other:Invalid domain or IP", + "nonexistent.example.com": "404" + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/validate/post.json b/backend/schema/paths/nginx/certificates/validate/post.json new file mode 100644 index 0000000..9fa2bd1 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/validate/post.json @@ -0,0 +1,100 @@ +{ + "operationId": "validateCertificates", + "summary": "Validates given Custom Certificates", + "tags": ["certificates"], + "security": [ + { + "bearerAuth": ["certificates.manage"] + } + ], + "requestBody": { + "$ref": "../../../../common.json#/properties/certificate_files" + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "certificate": { + "cn": "mkcert", + "issuer": "O = mkcert development CA, OU = jc@jc-Laptop.local (John Doe), CN = mkcert jc@jc-Laptop.local (John Doe)", + "dates": { + "from": 1728458537, + "to": 1799479337 + } + }, + "certificate_key": true + } + } + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "object", + "additionalProperties": false, + "required": ["cn", "issuer", "dates"], + "properties": { + "cn": { + "type": "string", + "example": "example.com" + }, + "issuer": { + "type": "string", + "example": "C = US, O = Let's Encrypt, CN = E5" + }, + "dates": { + "type": "object", + "additionalProperties": false, + "required": ["from", "to"], + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "example": { + "from": 1728448218, + "to": 1736224217 + } + } + } + }, + "certificate_key": { + "type": "boolean", + "example": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Certificate is not valid" + } + } + } + }, + "schema": { + "$ref": "../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/get.json b/backend/schema/paths/nginx/dead-hosts/get.json new file mode 100644 index 0000000..feb04ff --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/get.json @@ -0,0 +1,57 @@ +{ + "operationId": "getDeadHosts", + "summary": "Get all 404 hosts", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": ["dead_hosts.view"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:38:52.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false + } + ] + } + }, + "schema": { + "$ref": "../../../components/dead-host-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/delete.json b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json new file mode 100644 index 0000000..eed0ebc --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteDeadHost", + "summary": "Delete a 404 Host", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": ["dead_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the 404 Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json new file mode 100644 index 0000000..2a4d08a --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "disableDeadHost", + "summary": "Disable a 404 Host", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": ["dead_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the 404 Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json new file mode 100644 index 0000000..512c2a0 --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "enableDeadHost", + "summary": "Enable a 404 Host", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": ["dead_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the 404 Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/get.json b/backend/schema/paths/nginx/dead-hosts/hostID/get.json new file mode 100644 index 0000000..a3c24ed --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/get.json @@ -0,0 +1,57 @@ +{ + "operationId": "getDeadHost", + "summary": "Get a 404 Host", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": ["dead_hosts.view"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the 404 Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:38:52.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false + } + } + }, + "schema": { + "$ref": "../../../../components/dead-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/put.json b/backend/schema/paths/nginx/dead-hosts/hostID/put.json new file mode 100644 index 0000000..e07217c --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/put.json @@ -0,0 +1,108 @@ +{ + "operationId": "updateDeadHost", + "summary": "Update a 404 Host", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": ["dead_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the 404 Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "404 Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/dead-host-object.json#/properties/domain_names" + }, + "certificate_id": { + "$ref": "../../../../components/dead-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/dead-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/dead-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/dead-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../../components/dead-host-object.json#/properties/http2_support" + }, + "advanced_config": { + "$ref": "../../../../components/dead-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../../components/dead-host-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:46:06.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate": null + } + } + }, + "schema": { + "$ref": "../../../../components/dead-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/post.json b/backend/schema/paths/nginx/dead-hosts/post.json new file mode 100644 index 0000000..1ec3872 --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/post.json @@ -0,0 +1,112 @@ +{ + "operationId": "create404Host", + "summary": "Create a 404 Host", + "tags": ["404-hosts"], + "security": [ + { + "bearerAuth": [ + "dead_hosts.manage" + ] + } + ], + "requestBody": { + "description": "404 Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain_names" + ], + "properties": { + "domain_names": { + "$ref": "../../../components/dead-host-object.json#/properties/domain_names" + }, + "certificate_id": { + "$ref": "../../../components/dead-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/dead-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/dead-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/dead-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../components/dead-host-object.json#/properties/http2_support" + }, + "advanced_config": { + "$ref": "../../../components/dead-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../components/dead-host-object.json#/properties/meta" + } + } + }, + "example": { + "domain_names": [ + "test.example.com" + ], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "meta": {} + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:38:52.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": {}, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": [ + "admin" + ] + } + } + } + }, + "schema": { + "$ref": "../../../components/dead-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/get.json b/backend/schema/paths/nginx/proxy-hosts/get.json new file mode 100644 index 0000000..301e28b --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/get.json @@ -0,0 +1,74 @@ +{ + "operationId": "getProxyHosts", + "summary": "Get all proxy hosts", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": [ + "proxy_hosts.view" + ] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": [ + "access_list", + "owner", + "certificate" + ] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2025-10-28T01:10:26.000Z", + "modified_on": "2025-10-28T04:07:16.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.jc21com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8081, + "access_list_id": 1, + "certificate_id": 1, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false + } + ] + } + }, + "schema": { + "$ref": "../../../components/proxy-host-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json new file mode 100644 index 0000000..da67944 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteProxyHost", + "summary": "Delete a Proxy Host", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": ["proxy_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Proxy Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json new file mode 100644 index 0000000..14c2689 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "disableProxyHost", + "summary": "Disable a Proxy Host", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": ["proxy_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Proxy Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json new file mode 100644 index 0000000..fe504b7 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "enableProxyHost", + "summary": "Enable a Proxy Host", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": ["proxy_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Proxy Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json new file mode 100644 index 0000000..2e677fe --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json @@ -0,0 +1,83 @@ +{ + "operationId": "getProxyHost", + "summary": "Get a Proxy Host", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": [ + "proxy_hosts.view" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Proxy Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:12:05.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false, + "owner": { + "id": 1, + "created_on": "2025-10-28T00:50:24.000Z", + "modified_on": "2025-10-28T00:50:24.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "jamiec", + "nickname": "jamiec", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + } + } + } + }, + "schema": { + "$ref": "../../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json new file mode 100644 index 0000000..fc31984 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -0,0 +1,154 @@ +{ + "operationId": "updateProxyHost", + "summary": "Update a Proxy Host", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": [ + "proxy_hosts.manage" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Proxy Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Proxy Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/proxy-host-object.json#/properties/domain_names" + }, + "forward_scheme": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_host" + }, + "forward_port": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_port" + }, + "certificate_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains" + }, + "trust_forwarded_proto": { + "$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" + }, + "http2_support": { + "$ref": "../../../../components/proxy-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits" + }, + "caching_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled" + }, + "allow_websocket_upgrade": { + "$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" + }, + "access_list_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id" + }, + "advanced_config": { + "$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config" + }, + "enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/enabled" + }, + "meta": { + "$ref": "../../../../components/proxy-host-object.json#/properties/meta" + }, + "locations": { + "$ref": "../../../../components/proxy-host-object.json#/properties/locations" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:17:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false, + "owner": { + "id": 1, + "created_on": "2025-10-28T00:50:24.000Z", + "modified_on": "2025-10-28T00:50:24.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "jamiec", + "nickname": "jamiec", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + }, + "certificate": null, + "access_list": null + } + } + }, + "schema": { + "$ref": "../../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json new file mode 100644 index 0000000..28ddad8 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -0,0 +1,151 @@ +{ + "operationId": "createProxyHost", + "summary": "Create a Proxy Host", + "tags": ["proxy-hosts"], + "security": [ + { + "bearerAuth": [ + "proxy_hosts.manage" + ] + } + ], + "requestBody": { + "description": "Proxy Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain_names", + "forward_scheme", + "forward_host", + "forward_port" + ], + "properties": { + "domain_names": { + "$ref": "../../../components/proxy-host-object.json#/properties/domain_names" + }, + "forward_scheme": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_host" + }, + "forward_port": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_port" + }, + "certificate_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains" + }, + "trust_forwarded_proto": { + "$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" + }, + "http2_support": { + "$ref": "../../../components/proxy-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../components/proxy-host-object.json#/properties/block_exploits" + }, + "caching_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled" + }, + "allow_websocket_upgrade": { + "$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" + }, + "access_list_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/access_list_id" + }, + "advanced_config": { + "$ref": "../../../components/proxy-host-object.json#/properties/advanced_config" + }, + "enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/enabled" + }, + "meta": { + "$ref": "../../../components/proxy-host-object.json#/properties/meta" + }, + "locations": { + "$ref": "../../../components/proxy-host-object.json#/properties/locations" + } + } + }, + "example": { + "domain_names": [ + "test.example.com" + ], + "forward_scheme": "http", + "forward_host": "127.0.0.1", + "forward_port": 8080 + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:12:05.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": {}, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2025-10-28T00:50:24.000Z", + "modified_on": "2025-10-28T00:50:24.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "jamiec", + "nickname": "jamiec", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + }, + "access_list": null + } + } + }, + "schema": { + "$ref": "../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/get.json b/backend/schema/paths/nginx/redirection-hosts/get.json new file mode 100644 index 0000000..dfeb604 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/get.json @@ -0,0 +1,62 @@ +{ + "operationId": "getRedirectionHosts", + "summary": "Get all Redirection hosts", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": ["redirection_hosts.view"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:13:13.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } + ] + } + }, + "schema": { + "$ref": "../../../components/redirection-host-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json new file mode 100644 index 0000000..0b3232e --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteRedirectionHost", + "summary": "Delete a Redirection Host", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": ["redirection_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Redirection Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json new file mode 100644 index 0000000..f44af11 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "disableRedirectionHost", + "summary": "Disable a Redirection Host", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": ["redirection_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Redirection Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json new file mode 100644 index 0000000..07bf44d --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "enableRedirectionHost", + "summary": "Enable a Redirection Host", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": ["redirection_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Redirection Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/get.json b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json new file mode 100644 index 0000000..577b514 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json @@ -0,0 +1,62 @@ +{ + "operationId": "getRedirectionHost", + "summary": "Get a Redirection Host", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": ["redirection_hosts.view"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Redirection Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:13:13.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } + } + }, + "schema": { + "$ref": "../../../../components/redirection-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json new file mode 100644 index 0000000..454a388 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json @@ -0,0 +1,128 @@ +{ + "operationId": "updateRedirectionHost", + "summary": "Update a Redirection Host", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": ["redirection_hosts.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Redirection Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Redirection Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/redirection-host-object.json#/properties/domain_names" + }, + "forward_http_code": { + "$ref": "../../../../components/redirection-host-object.json#/properties/forward_http_code" + }, + "forward_scheme": { + "$ref": "../../../../components/redirection-host-object.json#/properties/forward_scheme" + }, + "forward_domain_name": { + "$ref": "../../../../components/redirection-host-object.json#/properties/forward_domain_name" + }, + "preserve_path": { + "$ref": "../../../../components/redirection-host-object.json#/properties/preserve_path" + }, + "certificate_id": { + "$ref": "../../../../components/redirection-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/redirection-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/redirection-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/redirection-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../../components/redirection-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../../components/redirection-host-object.json#/properties/block_exploits" + }, + "advanced_config": { + "$ref": "../../../../components/redirection-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../../components/redirection-host-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:18:11.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate": null + } + } + }, + "schema": { + "$ref": "../../../../components/redirection-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json new file mode 100644 index 0000000..6aa5330 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/post.json @@ -0,0 +1,140 @@ +{ + "operationId": "createRedirectionHost", + "summary": "Create a Redirection Host", + "tags": ["redirection-hosts"], + "security": [ + { + "bearerAuth": [ + "redirection_hosts.manage" + ] + } + ], + "requestBody": { + "description": "Redirection Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain_names", + "forward_scheme", + "forward_http_code", + "forward_domain_name" + ], + "properties": { + "domain_names": { + "$ref": "../../../components/redirection-host-object.json#/properties/domain_names" + }, + "forward_http_code": { + "$ref": "../../../components/redirection-host-object.json#/properties/forward_http_code" + }, + "forward_scheme": { + "$ref": "../../../components/redirection-host-object.json#/properties/forward_scheme" + }, + "forward_domain_name": { + "$ref": "../../../components/redirection-host-object.json#/properties/forward_domain_name" + }, + "preserve_path": { + "$ref": "../../../components/redirection-host-object.json#/properties/preserve_path" + }, + "certificate_id": { + "$ref": "../../../components/redirection-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/redirection-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/redirection-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/redirection-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../components/redirection-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../components/redirection-host-object.json#/properties/block_exploits" + }, + "advanced_config": { + "$ref": "../../../components/redirection-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../components/redirection-host-object.json#/properties/meta" + } + } + }, + "example": { + "domain_names": [ + "test.example.com" + ], + "forward_domain_name": "example.com", + "forward_scheme": "auto", + "forward_http_code": 301, + "preserve_path": false, + "block_exploits": false, + "certificate_id": 0, + "ssl_forced": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "advanced_config": "", + "meta": {} + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 2, + "created_on": "2025-10-30T01:27:04.000Z", + "modified_on": "2025-10-30T01:27:04.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_domain_name": "example.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": {}, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "auto", + "forward_http_code": 301, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2025-10-28T00:50:24.000Z", + "modified_on": "2025-10-28T00:50:24.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "jamiec", + "nickname": "jamiec", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + } + } + } + }, + "schema": { + "$ref": "../../../components/redirection-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/get.json b/backend/schema/paths/nginx/streams/get.json new file mode 100644 index 0000000..6dda8e3 --- /dev/null +++ b/backend/schema/paths/nginx/streams/get.json @@ -0,0 +1,56 @@ +{ + "operationId": "getStreams", + "summary": "Get all streams", + "tags": ["streams"], + "security": [ + { + "bearerAuth": ["streams.view"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "certificate_id": 0 + } + ] + } + }, + "schema": { + "$ref": "../../../components/stream-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/post.json b/backend/schema/paths/nginx/streams/post.json new file mode 100644 index 0000000..0c986de --- /dev/null +++ b/backend/schema/paths/nginx/streams/post.json @@ -0,0 +1,110 @@ +{ + "operationId": "createStream", + "summary": "Create a Stream", + "tags": ["streams"], + "security": [ + { + "bearerAuth": [ + "streams.manage" + ] + } + ], + "requestBody": { + "description": "Stream Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "incoming_port", + "forwarding_host", + "forwarding_port" + ], + "properties": { + "incoming_port": { + "$ref": "../../../components/stream-object.json#/properties/incoming_port" + }, + "forwarding_host": { + "$ref": "../../../components/stream-object.json#/properties/forwarding_host" + }, + "forwarding_port": { + "$ref": "../../../components/stream-object.json#/properties/forwarding_port" + }, + "tcp_forwarding": { + "$ref": "../../../components/stream-object.json#/properties/tcp_forwarding" + }, + "udp_forwarding": { + "$ref": "../../../components/stream-object.json#/properties/udp_forwarding" + }, + "certificate_id": { + "$ref": "../../../components/stream-object.json#/properties/certificate_id" + }, + "meta": { + "$ref": "../../../components/stream-object.json#/properties/meta" + }, + "domain_names": { + "$ref": "../../../components/dead-host-object.json#/properties/domain_names" + } + } + }, + "example": { + "incoming_port": 8888, + "forwarding_host": "127.0.0.1", + "forwarding_port": 8080, + "tcp_forwarding": true, + "udp_forwarding": false, + "certificate_id": 0, + "meta": {} + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "owner": { + "id": 1, + "created_on": "2024-10-09T02:33:16.000Z", + "modified_on": "2024-10-09T02:33:16.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": [ + "admin" + ] + }, + "certificate_id": 0 + } + } + }, + "schema": { + "$ref": "../../../components/stream-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/delete.json b/backend/schema/paths/nginx/streams/streamID/delete.json new file mode 100644 index 0000000..585cd82 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteStream", + "summary": "Delete a Stream", + "tags": ["streams"], + "security": [ + { + "bearerAuth": ["streams.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "description": "The ID of the Stream", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/disable/post.json b/backend/schema/paths/nginx/streams/streamID/disable/post.json new file mode 100644 index 0000000..61de851 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/disable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "disableStream", + "summary": "Disable a Stream", + "tags": ["streams"], + "security": [ + { + "bearerAuth": ["streams.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "description": "The ID of the Stream", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/enable/post.json b/backend/schema/paths/nginx/streams/streamID/enable/post.json new file mode 100644 index 0000000..d27ce52 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/enable/post.json @@ -0,0 +1,60 @@ +{ + "operationId": "enableStream", + "summary": "Enable a Stream", + "tags": ["streams"], + "security": [ + { + "bearerAuth": ["streams.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "description": "The ID of the Stream", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/get.json b/backend/schema/paths/nginx/streams/streamID/get.json new file mode 100644 index 0000000..22fae88 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/get.json @@ -0,0 +1,56 @@ +{ + "operationId": "getStream", + "summary": "Get a Stream", + "tags": ["streams"], + "security": [ + { + "bearerAuth": ["streams.view"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "description": "The ID of the Stream", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "certificate_id": 0 + } + } + }, + "schema": { + "$ref": "../../../../components/stream-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json new file mode 100644 index 0000000..21ae71e --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/put.json @@ -0,0 +1,103 @@ +{ + "operationId": "updateStream", + "summary": "Update a Stream", + "tags": ["streams"], + "security": [ + { + "bearerAuth": ["streams.manage"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "description": "The ID of the Stream", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Stream Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "incoming_port": { + "$ref": "../../../../components/stream-object.json#/properties/incoming_port" + }, + "forwarding_host": { + "$ref": "../../../../components/stream-object.json#/properties/forwarding_host" + }, + "forwarding_port": { + "$ref": "../../../../components/stream-object.json#/properties/forwarding_port" + }, + "tcp_forwarding": { + "$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding" + }, + "udp_forwarding": { + "$ref": "../../../../components/stream-object.json#/properties/udp_forwarding" + }, + "certificate_id": { + "$ref": "../../../../components/stream-object.json#/properties/certificate_id" + }, + "meta": { + "$ref": "../../../../components/stream-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "owner": { + "id": 1, + "created_on": "2024-10-09T02:33:16.000Z", + "modified_on": "2024-10-09T02:33:16.000Z", + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate_id": 0 + } + } + }, + "schema": { + "$ref": "../../../../components/stream-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/reports/hosts/get.json b/backend/schema/paths/reports/hosts/get.json new file mode 100644 index 0000000..682a97b --- /dev/null +++ b/backend/schema/paths/reports/hosts/get.json @@ -0,0 +1,54 @@ +{ + "operationId": "reportsHosts", + "summary": "Report on Host Statistics", + "tags": ["reports"], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "proxy": 20, + "redirection": 1, + "stream": 0, + "dead": 1 + } + } + }, + "schema": { + "type": "object", + "properties": { + "proxy": { + "type": "integer", + "description": "Proxy Hosts Count", + "example": 20 + }, + "redirection": { + "type": "integer", + "description": "Redirection Hosts Count", + "example": 2 + }, + "stream": { + "type": "integer", + "description": "Streams Count", + "example": 0 + }, + "dead": { + "type": "integer", + "description": "404 Hosts Count", + "example": 3 + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/schema/get.json b/backend/schema/paths/schema/get.json new file mode 100644 index 0000000..c9e601e --- /dev/null +++ b/backend/schema/paths/schema/get.json @@ -0,0 +1,10 @@ +{ + "operationId": "schema", + "summary": "Returns this swagger API schema", + "tags": ["public"], + "responses": { + "200": { + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/settings/get.json b/backend/schema/paths/settings/get.json new file mode 100644 index 0000000..a99f7f2 --- /dev/null +++ b/backend/schema/paths/settings/get.json @@ -0,0 +1,35 @@ +{ + "operationId": "getSettings", + "summary": "Get all settings", + "tags": ["settings"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": "default-site", + "name": "Default Site", + "description": "What to show when Nginx is hit with an unknown Host", + "value": "congratulations", + "meta": {} + } + ] + } + }, + "schema": { + "$ref": "../../components/setting-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/settings/settingID/get.json b/backend/schema/paths/settings/settingID/get.json new file mode 100644 index 0000000..929f3e5 --- /dev/null +++ b/backend/schema/paths/settings/settingID/get.json @@ -0,0 +1,46 @@ +{ + "operationId": "getSetting", + "summary": "Get a setting", + "tags": ["settings"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "settingID", + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "description": "Setting ID", + "example": "default-site" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": "default-site", + "name": "Default Site", + "description": "What to show when Nginx is hit with an unknown Host", + "value": "congratulations", + "meta": {} + } + } + }, + "schema": { + "$ref": "../../../components/setting-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/settings/settingID/put.json b/backend/schema/paths/settings/settingID/put.json new file mode 100644 index 0000000..050ad44 --- /dev/null +++ b/backend/schema/paths/settings/settingID/put.json @@ -0,0 +1,87 @@ +{ + "operationId": "updateSetting", + "summary": "Update a setting", + "tags": ["settings"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "settingID", + "schema": { + "type": "string", + "minLength": 1, + "enum": ["default-site"] + }, + "required": true, + "description": "Setting ID", + "example": "default-site" + } + ], + "requestBody": { + "description": "Setting Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "value": { + "type": "string", + "minLength": 1, + "enum": ["congratulations", "404", "444", "redirect", "html"], + "example": "html" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "redirect": { + "type": "string" + }, + "html": { + "type": "string" + } + }, + "example": { + "html": "

hello world

" + } + } + } + }, + "example": { + "value": "congratulations", + "meta": {} + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": "default-site", + "name": "Default Site", + "description": "What to show when Nginx is hit with an unknown Host", + "value": "congratulations", + "meta": {} + } + } + }, + "schema": { + "$ref": "../../../components/setting-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/tokens/2fa/post.json b/backend/schema/paths/tokens/2fa/post.json new file mode 100644 index 0000000..c37af8d --- /dev/null +++ b/backend/schema/paths/tokens/2fa/post.json @@ -0,0 +1,55 @@ +{ + "operationId": "loginWith2FA", + "summary": "Verify 2FA code and get full token", + "tags": ["tokens"], + "requestBody": { + "description": "2fa Challenge Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "challenge_token": { + "minLength": 1, + "type": "string", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + }, + "code": { + "minLength": 6, + "maxLength": 8, + "type": "string", + "example": "012345" + } + }, + "required": ["challenge_token", "code"], + "type": "object" + }, + "example": { + "challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "code": "012345" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires": "2025-02-04T20:40:46.340Z", + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } + }, + "schema": { + "$ref": "../../../components/token-object.json" + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/tokens/get.json b/backend/schema/paths/tokens/get.json new file mode 100644 index 0000000..4e6ae24 --- /dev/null +++ b/backend/schema/paths/tokens/get.json @@ -0,0 +1,30 @@ +{ + "operationId": "refreshToken", + "summary": "Refresh your access token", + "tags": ["tokens"], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires": "2025-02-04T20:40:46.340Z", + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } + }, + "schema": { + "$ref": "../../components/token-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/tokens/post.json b/backend/schema/paths/tokens/post.json new file mode 100644 index 0000000..470be30 --- /dev/null +++ b/backend/schema/paths/tokens/post.json @@ -0,0 +1,67 @@ +{ + "operationId": "requestToken", + "summary": "Request a new access token from credentials", + "tags": ["tokens"], + "requestBody": { + "description": "Credentials Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "identity": { + "minLength": 1, + "type": "string", + "example": "me@example.com" + }, + "scope": { + "minLength": 1, + "type": "string", + "enum": ["user"], + "example": "user" + }, + "secret": { + "minLength": 1, + "type": "string", + "example": "bigredhorsebanana" + } + }, + "required": ["identity", "secret"], + "type": "object" + }, + "example": { + "identity": "me@example.com", + "secret": "bigredhorsebanana" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires": "2025-02-04T20:40:46.340Z", + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } + }, + "schema": { + "oneOf": [ + { + "$ref": "../../components/token-object.json" + }, + { + "$ref": "../../components/token-challenge.json" + } + ] + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/users/get.json b/backend/schema/paths/users/get.json new file mode 100644 index 0000000..79c31ba --- /dev/null +++ b/backend/schema/paths/users/get.json @@ -0,0 +1,74 @@ +{ + "operationId": "getUsers", + "summary": "Get all users", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["permissions"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2020-01-30T09:36:08.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"] + } + ] + }, + "withPermissions": { + "value": [ + { + "id": 1, + "created_on": "2020-01-30T09:36:08.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"], + "permissions": { + "visibility": "all", + "proxy_hosts": "manage", + "redirection_hosts": "manage", + "dead_hosts": "manage", + "streams": "manage", + "access_lists": "manage", + "certificates": "manage" + } + } + ] + } + }, + "schema": { + "$ref": "../../components/user-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/post.json b/backend/schema/paths/users/post.json new file mode 100644 index 0000000..49025e3 --- /dev/null +++ b/backend/schema/paths/users/post.json @@ -0,0 +1,88 @@ +{ + "operationId": "createUser", + "summary": "Create a User", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "requestBody": { + "description": "User Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "nickname", "email"], + "properties": { + "name": { + "$ref": "../../components/user-object.json#/properties/name" + }, + "nickname": { + "$ref": "../../components/user-object.json#/properties/nickname" + }, + "email": { + "$ref": "../../components/user-object.json#/properties/email" + }, + "roles": { + "$ref": "../../components/user-object.json#/properties/roles" + }, + "is_disabled": { + "$ref": "../../components/user-object.json#/properties/is_disabled" + }, + "auth": { + "type": "object", + "description": "Auth Credentials", + "example": { + "type": "password", + "secret": "bigredhorsebanana" + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 2, + "created_on": "2020-01-30T09:41:04.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"], + "permissions": { + "id": 3, + "created_on": "2020-01-30T09:41:04.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "user_id": 2, + "visibility": "user", + "proxy_hosts": "manage", + "redirection_hosts": "manage", + "dead_hosts": "manage", + "streams": "manage", + "access_lists": "manage", + "certificates": "manage" + } + } + } + }, + "schema": { + "$ref": "../../components/user-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/2fa/backup-codes/post.json b/backend/schema/paths/users/userID/2fa/backup-codes/post.json new file mode 100644 index 0000000..00cb9d5 --- /dev/null +++ b/backend/schema/paths/users/userID/2fa/backup-codes/post.json @@ -0,0 +1,92 @@ +{ + "operationId": "regenUser2faCodes", + "summary": "Regenerate 2FA backup codes", + "tags": ["users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "requestBody": { + "description": "Verification Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "code": { + "minLength": 6, + "maxLength": 8, + "type": "string", + "example": "123456" + } + }, + "required": ["code"], + "type": "object" + }, + "example": { + "code": "123456" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "backup_codes": [ + "6CD7CB06", + "495302F3", + "D8037852", + "A6FFC956", + "BC1A1851", + "A05E644F", + "A406D2E8", + "0AE3C522" + ] + } + } + }, + "schema": { + "type": "object", + "required": ["backup_codes"], + "additionalProperties": false, + "properties": { + "backup_codes": { + "description": "Backup codes", + "example": [ + "6CD7CB06", + "495302F3", + "D8037852", + "A6FFC956", + "BC1A1851", + "A05E644F", + "A406D2E8", + "0AE3C522" + ], + "type": "array", + "items": { + "type": "string", + "example": "6CD7CB06" + } + } + } + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/users/userID/2fa/delete.json b/backend/schema/paths/users/userID/2fa/delete.json new file mode 100644 index 0000000..bff292d --- /dev/null +++ b/backend/schema/paths/users/userID/2fa/delete.json @@ -0,0 +1,48 @@ +{ + "operationId": "disableUser2fa", + "summary": "Disable 2fa for user", + "tags": ["users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + }, + { + "in": "query", + "name": "code", + "schema": { + "type": "string", + "minLength": 6, + "maxLength": 6, + "example": "012345" + }, + "required": true, + "description": "2fa Code", + "example": "012345" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/users/userID/2fa/enable/post.json b/backend/schema/paths/users/userID/2fa/enable/post.json new file mode 100644 index 0000000..74c9854 --- /dev/null +++ b/backend/schema/paths/users/userID/2fa/enable/post.json @@ -0,0 +1,92 @@ +{ + "operationId": "enableUser2fa", + "summary": "Verify code and enable 2FA", + "tags": ["users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "requestBody": { + "description": "Verification Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "code": { + "minLength": 6, + "maxLength": 8, + "type": "string", + "example": "123456" + } + }, + "required": ["code"], + "type": "object" + }, + "example": { + "code": "123456" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "backup_codes": [ + "6CD7CB06", + "495302F3", + "D8037852", + "A6FFC956", + "BC1A1851", + "A05E644F", + "A406D2E8", + "0AE3C522" + ] + } + } + }, + "schema": { + "type": "object", + "required": ["backup_codes"], + "additionalProperties": false, + "properties": { + "backup_codes": { + "description": "Backup codes", + "example": [ + "6CD7CB06", + "495302F3", + "D8037852", + "A6FFC956", + "BC1A1851", + "A05E644F", + "A406D2E8", + "0AE3C522" + ], + "type": "array", + "items": { + "type": "string", + "example": "6CD7CB06" + } + } + } + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/users/userID/2fa/get.json b/backend/schema/paths/users/userID/2fa/get.json new file mode 100644 index 0000000..78ce188 --- /dev/null +++ b/backend/schema/paths/users/userID/2fa/get.json @@ -0,0 +1,57 @@ +{ + "operationId": "getUser2faStatus", + "summary": "Get user 2fa Status", + "tags": ["users"], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "enabled": false, + "backup_codes_remaining": 0 + } + } + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["enabled", "backup_codes_remaining"], + "properties": { + "enabled": { + "type": "boolean", + "description": "Is 2FA enabled for this user", + "example": true + }, + "backup_codes_remaining": { + "type": "integer", + "description": "Number of remaining backup codes for this user", + "example": 5 + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/2fa/post.json b/backend/schema/paths/users/userID/2fa/post.json new file mode 100644 index 0000000..cef5d19 --- /dev/null +++ b/backend/schema/paths/users/userID/2fa/post.json @@ -0,0 +1,52 @@ +{ + "operationId": "setupUser2fa", + "summary": "Start 2FA setup, returns QR code URL", + "tags": ["users"], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "secret": "JZYCEBIEEJYUGPQM", + "otpauth_url": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager" + } + } + }, + "schema": { + "type": "object", + "required": ["secret", "otpauth_url"], + "additionalProperties": false, + "properties": { + "secret": { + "description": "TOTP Secret", + "example": "JZYCEBIEEJYUGPQM", + "type": "string" + }, + "otpauth_url": { + "description": "OTP Auth URL for QR Code generation", + "example": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager", + "type": "string" + } + } + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/users/userID/auth/put.json b/backend/schema/paths/users/userID/auth/put.json new file mode 100644 index 0000000..3dba45b --- /dev/null +++ b/backend/schema/paths/users/userID/auth/put.json @@ -0,0 +1,79 @@ +{ + "operationId": "updateUserAuth", + "summary": "Update a User's Authentication", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "oneOf": [ + { + "type": "string", + "pattern": "^me$" + }, + { + "type": "integer", + "minimum": 1 + } + ] + }, + "required": true, + "description": "User ID or 'me' for yourself", + "example": 2 + } + ], + "requestBody": { + "description": "Auth Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["type", "secret"], + "properties": { + "type": { + "type": "string", + "pattern": "^password$", + "example": "password" + }, + "current": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "example": "changeme" + }, + "secret": { + "type": "string", + "minLength": 8, + "maxLength": 64, + "example": "mySuperN3wP@ssword!" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/delete.json b/backend/schema/paths/users/userID/delete.json new file mode 100644 index 0000000..767edfe --- /dev/null +++ b/backend/schema/paths/users/userID/delete.json @@ -0,0 +1,40 @@ +{ + "operationId": "deleteUser", + "summary": "Delete a User", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/get.json b/backend/schema/paths/users/userID/get.json new file mode 100644 index 0000000..2cf5587 --- /dev/null +++ b/backend/schema/paths/users/userID/get.json @@ -0,0 +1,58 @@ +{ + "operationId": "getUser", + "summary": "Get a user", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "oneOf": [ + { + "type": "string", + "pattern": "^me$" + }, + { + "type": "integer", + "minimum": 1 + } + ] + }, + "required": true, + "description": "User ID or 'me' for yourself", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2020-01-30T09:36:08.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"] + } + } + }, + "schema": { + "$ref": "../../../components/user-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/login/post.json b/backend/schema/paths/users/userID/login/post.json new file mode 100644 index 0000000..46001d2 --- /dev/null +++ b/backend/schema/paths/users/userID/login/post.json @@ -0,0 +1,72 @@ +{ + "operationId": "loginAsUser", + "summary": "Login as this user", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "token": "eyJhbGciOiJSUzI1NiIsInR...16OjT8B3NLyXg", + "expires": "2020-01-31T10:56:23.239Z", + "user": { + "id": 1, + "created_on": "2020-01-30T10:43:44.000Z", + "modified_on": "2020-01-30T10:43:44.000Z", + "is_disabled": false, + "email": "user2@example.com", + "name": "John Doe", + "nickname": "Jonny", + "avatar": "//www.gravatar.com/avatar/3c8d73f45fd8763f827b964c76e6032a?default=mm", + "roles": [] + } + } + } + }, + "schema": { + "type": "object", + "description": "Login object", + "required": ["expires", "token", "user"], + "additionalProperties": false, + "properties": { + "token": { + "description": "JWT Token", + "type": "string", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + }, + "expires": { + "description": "Token Expiry Timestamp", + "type": "string", + "example": "2020-01-30T10:43:44.000Z" + }, + "user": { + "$ref": "../../../../components/user-object.json" + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/permissions/put.json b/backend/schema/paths/users/userID/permissions/put.json new file mode 100644 index 0000000..764e746 --- /dev/null +++ b/backend/schema/paths/users/userID/permissions/put.json @@ -0,0 +1,60 @@ +{ + "operationId": "updateUserPermissions", + "summary": "Update a User's Permissions", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "User ID", + "example": 2 + } + ], + "requestBody": { + "description": "Permissions Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../../../components/permission-object.json" + }, + "example": { + "visibility": "all", + "access_lists": "view", + "certificates": "hidden", + "dead_hosts": "hidden", + "proxy_hosts": "manage", + "redirection_hosts": "hidden", + "streams": "hidden" + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/put.json b/backend/schema/paths/users/userID/put.json new file mode 100644 index 0000000..eabaa3a --- /dev/null +++ b/backend/schema/paths/users/userID/put.json @@ -0,0 +1,88 @@ +{ + "operationId": "updateUser", + "summary": "Update a User", + "tags": ["users"], + "security": [ + { + "bearerAuth": ["admin"] + } + ], + "parameters": [ + { + "in": "path", + "name": "userID", + "schema": { + "oneOf": [ + { + "type": "string", + "pattern": "^me$" + }, + { + "type": "integer", + "minimum": 1 + } + ] + }, + "required": true, + "description": "User ID or 'me' for yourself", + "example": 2 + } + ], + "requestBody": { + "description": "User Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": { + "$ref": "../../../components/user-object.json#/properties/name" + }, + "nickname": { + "$ref": "../../../components/user-object.json#/properties/nickname" + }, + "email": { + "$ref": "../../../components/user-object.json#/properties/email" + }, + "roles": { + "$ref": "../../../components/user-object.json#/properties/roles" + }, + "is_disabled": { + "$ref": "../../../components/user-object.json#/properties/is_disabled" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 2, + "created_on": "2020-01-30T09:36:08.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"] + } + } + }, + "schema": { + "$ref": "../../../components/user-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/version/check/get.json b/backend/schema/paths/version/check/get.json new file mode 100644 index 0000000..cbc576a --- /dev/null +++ b/backend/schema/paths/version/check/get.json @@ -0,0 +1,26 @@ +{ + "operationId": "checkVersion", + "summary": "Returns any new version data from github", + "tags": ["public"], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "current": "v2.12.0", + "latest": "v2.13.4", + "update_available": true + } + } + }, + "schema": { + "$ref": "../../../components/check-version-object.json" + } + } + } + } + } +} diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json new file mode 100644 index 0000000..4222f19 --- /dev/null +++ b/backend/schema/swagger.json @@ -0,0 +1,362 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Nginx Proxy Manager API", + "version": "2.x.x", + "description": "This is the official API documentation for Nginx Proxy Manager.\n\nMost endpoints require authentication via Bearer Token (JWT). You can generate a token by logging in via the `POST /tokens` endpoint.\n\nFor more information, visit the [Nginx Proxy Manager Documentation](https://nginxproxymanager.com)." + }, + "servers": [ + { + "url": "http://127.0.0.1:81/api" + } + ], + "components": { + "securitySchemes": { + "$ref": "./components/security-schemes.json" + } + }, + "tags": [ + { + "name": "public", + "description": "Endpoints that do not require authentication" + }, + { + "name": "audit-log", + "description": "Endpoints related to Audit Logs" + }, + { + "name": "access-lists", + "description": "Endpoints related to Access Lists" + }, + { + "name": "certificates", + "description": "Endpoints related to Certificates" + }, + { + "name": "404-hosts", + "description": "Endpoints related to 404 Hosts" + }, + { + "name": "proxy-hosts", + "description": "Endpoints related to Proxy Hosts" + }, + { + "name": "redirection-hosts", + "description": "Endpoints related to Redirection Hosts" + }, + { + "name": "streams", + "description": "Endpoints related to Streams" + }, + { + "name": "reports", + "description": "Endpoints for viewing reports" + }, + { + "name": "settings", + "description": "Endpoints for managing application settings" + }, + { + "name": "tokens", + "description": "Endpoints for managing authentication tokens" + }, + { + "name": "users", + "description": "Endpoints for managing users" + } + ], + "paths": { + "/": { + "get": { + "$ref": "./paths/get.json" + } + }, + "/audit-log": { + "get": { + "$ref": "./paths/audit-log/get.json" + } + }, + "/audit-log/{id}": { + "get": { + "$ref": "./paths/audit-log/id/get.json" + } + }, + "/nginx/access-lists": { + "get": { + "$ref": "./paths/nginx/access-lists/get.json" + }, + "post": { + "$ref": "./paths/nginx/access-lists/post.json" + } + }, + "/nginx/access-lists/{listID}": { + "get": { + "$ref": "./paths/nginx/access-lists/listID/get.json" + }, + "put": { + "$ref": "./paths/nginx/access-lists/listID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/access-lists/listID/delete.json" + } + }, + "/nginx/certificates": { + "get": { + "$ref": "./paths/nginx/certificates/get.json" + }, + "post": { + "$ref": "./paths/nginx/certificates/post.json" + } + }, + "/nginx/certificates/dns-providers": { + "get": { + "$ref": "./paths/nginx/certificates/dns-providers/get.json" + } + }, + "/nginx/certificates/validate": { + "post": { + "$ref": "./paths/nginx/certificates/validate/post.json" + } + }, + "/nginx/certificates/test-http": { + "post": { + "$ref": "./paths/nginx/certificates/test-http/post.json" + } + }, + "/nginx/certificates/{certID}": { + "get": { + "$ref": "./paths/nginx/certificates/certID/get.json" + }, + "delete": { + "$ref": "./paths/nginx/certificates/certID/delete.json" + } + }, + "/nginx/certificates/{certID}/download": { + "get": { + "$ref": "./paths/nginx/certificates/certID/download/get.json" + } + }, + "/nginx/certificates/{certID}/renew": { + "post": { + "$ref": "./paths/nginx/certificates/certID/renew/post.json" + } + }, + "/nginx/certificates/{certID}/upload": { + "post": { + "$ref": "./paths/nginx/certificates/certID/upload/post.json" + } + }, + "/nginx/proxy-hosts": { + "get": { + "$ref": "./paths/nginx/proxy-hosts/get.json" + }, + "post": { + "$ref": "./paths/nginx/proxy-hosts/post.json" + } + }, + "/nginx/proxy-hosts/{hostID}": { + "get": { + "$ref": "./paths/nginx/proxy-hosts/hostID/get.json" + }, + "put": { + "$ref": "./paths/nginx/proxy-hosts/hostID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/proxy-hosts/hostID/delete.json" + } + }, + "/nginx/proxy-hosts/{hostID}/enable": { + "post": { + "$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json" + } + }, + "/nginx/proxy-hosts/{hostID}/disable": { + "post": { + "$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json" + } + }, + "/nginx/redirection-hosts": { + "get": { + "$ref": "./paths/nginx/redirection-hosts/get.json" + }, + "post": { + "$ref": "./paths/nginx/redirection-hosts/post.json" + } + }, + "/nginx/redirection-hosts/{hostID}": { + "get": { + "$ref": "./paths/nginx/redirection-hosts/hostID/get.json" + }, + "put": { + "$ref": "./paths/nginx/redirection-hosts/hostID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/redirection-hosts/hostID/delete.json" + } + }, + "/nginx/redirection-hosts/{hostID}/enable": { + "post": { + "$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json" + } + }, + "/nginx/redirection-hosts/{hostID}/disable": { + "post": { + "$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json" + } + }, + "/nginx/dead-hosts": { + "get": { + "$ref": "./paths/nginx/dead-hosts/get.json" + }, + "post": { + "$ref": "./paths/nginx/dead-hosts/post.json" + } + }, + "/nginx/dead-hosts/{hostID}": { + "get": { + "$ref": "./paths/nginx/dead-hosts/hostID/get.json" + }, + "put": { + "$ref": "./paths/nginx/dead-hosts/hostID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/dead-hosts/hostID/delete.json" + } + }, + "/nginx/dead-hosts/{hostID}/enable": { + "post": { + "$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json" + } + }, + "/nginx/dead-hosts/{hostID}/disable": { + "post": { + "$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json" + } + }, + "/nginx/streams": { + "get": { + "$ref": "./paths/nginx/streams/get.json" + }, + "post": { + "$ref": "./paths/nginx/streams/post.json" + } + }, + "/nginx/streams/{streamID}": { + "get": { + "$ref": "./paths/nginx/streams/streamID/get.json" + }, + "put": { + "$ref": "./paths/nginx/streams/streamID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/streams/streamID/delete.json" + } + }, + "/nginx/streams/{streamID}/enable": { + "post": { + "$ref": "./paths/nginx/streams/streamID/enable/post.json" + } + }, + "/nginx/streams/{streamID}/disable": { + "post": { + "$ref": "./paths/nginx/streams/streamID/disable/post.json" + } + }, + "/reports/hosts": { + "get": { + "$ref": "./paths/reports/hosts/get.json" + } + }, + "/schema": { + "get": { + "$ref": "./paths/schema/get.json" + } + }, + "/settings": { + "get": { + "$ref": "./paths/settings/get.json" + } + }, + "/settings/{settingID}": { + "get": { + "$ref": "./paths/settings/settingID/get.json" + }, + "put": { + "$ref": "./paths/settings/settingID/put.json" + } + }, + "/tokens": { + "get": { + "$ref": "./paths/tokens/get.json" + }, + "post": { + "$ref": "./paths/tokens/post.json" + } + }, + "/tokens/2fa": { + "post": { + "$ref": "./paths/tokens/2fa/post.json" + } + }, + "/version/check": { + "get": { + "$ref": "./paths/version/check/get.json" + } + }, + "/users": { + "get": { + "$ref": "./paths/users/get.json" + }, + "post": { + "$ref": "./paths/users/post.json" + } + }, + "/users/{userID}": { + "get": { + "$ref": "./paths/users/userID/get.json" + }, + "put": { + "$ref": "./paths/users/userID/put.json" + }, + "delete": { + "$ref": "./paths/users/userID/delete.json" + } + }, + "/users/{userID}/2fa": { + "post": { + "$ref": "./paths/users/userID/2fa/post.json" + }, + "get": { + "$ref": "./paths/users/userID/2fa/get.json" + }, + "delete": { + "$ref": "./paths/users/userID/2fa/delete.json" + } + }, + "/users/{userID}/2fa/enable": { + "post": { + "$ref": "./paths/users/userID/2fa/enable/post.json" + } + }, + "/users/{userID}/2fa/backup-codes": { + "post": { + "$ref": "./paths/users/userID/2fa/backup-codes/post.json" + } + }, + "/users/{userID}/auth": { + "put": { + "$ref": "./paths/users/userID/auth/put.json" + } + }, + "/users/{userID}/permissions": { + "put": { + "$ref": "./paths/users/userID/permissions/put.json" + } + }, + "/users/{userID}/login": { + "post": { + "$ref": "./paths/users/userID/login/post.json" + } + } + } +} diff --git a/backend/scripts/install-certbot-plugins b/backend/scripts/install-certbot-plugins new file mode 100644 index 0000000..6acb022 --- /dev/null +++ b/backend/scripts/install-certbot-plugins @@ -0,0 +1,54 @@ +#!/usr/bin/node + +// Usage: +// Install all plugins defined in `../certbot/dns-plugins.json`: +// ./install-certbot-plugins +// Install one or more specific plugins: +// ./install-certbot-plugins route53 cloudflare +// +// Usage with a running docker container: +// docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins" +// + +import batchflow from "batchflow"; +import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" }; +import { installPlugin } from "../lib/certbot.js"; +import { certbot as logger } from "../logger.js"; + +let hasErrors = false; +const failingPlugins = []; + +let pluginKeys = Object.keys(dnsPlugins); +if (process.argv.length > 2) { + pluginKeys = process.argv.slice(2); +} + +batchflow(pluginKeys) + .sequential() + .each((i, pluginKey, next) => { + installPlugin(pluginKey) + .then(() => { + next(); + }) + .catch((err) => { + hasErrors = true; + failingPlugins.push(pluginKey); + next(err); + }); + }) + .error((err) => { + logger.error(err.message); + }) + .end(() => { + if (hasErrors) { + logger.error( + "Some plugins failed to install. Please check the logs above. Failing plugins: " + + "\n - " + + failingPlugins.join("\n - "), + ); + process.exit(1); + } else { + logger.complete("Plugins installed successfully"); + process.exit(0); + } + }); diff --git a/backend/scripts/regenerate-config b/backend/scripts/regenerate-config new file mode 100644 index 0000000..00f8411 --- /dev/null +++ b/backend/scripts/regenerate-config @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +import * as process from "node:process"; // Use the node: protocol for built-ins +import internalNginx from "../internal/nginx.js"; +import { global as logger } from "../logger.js"; +import deadHostModel from "../models/dead_host.js"; +import proxyHostModel from "../models/proxy_host.js"; +import redirectionHostModel from "../models/redirection_host.js"; +import streamModel from "../models/stream.js"; + +const args = process.argv.slice(2); +const UNATTENDED = args.includes("-y") || args.includes("--yes"); +const DRY_RUN = args.includes("--dry-run"); + +if (args.includes("--help") || args.includes("-h")) { + console.log("\nThis will iterate over all Hosts and regnerate their Nginx configs.\n") + console.log("Usage: ./regenerate-config [-h|--help] [-y|--yes] [--dry-run]\n"); + process.exit(0); +} + +// ask for the user to confirm the action if not in unattended mode +if (!UNATTENDED && !DRY_RUN) { + const readline = await import("node:readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const question = (query) => + new Promise((resolve) => rl.question(query, resolve)); + + const answer = await question( + "This will iterate over all Hosts and regnerate their Nginx configs.\n\nAre you sure you want to proceed? (y/N) ", + ); + rl.close(); + + if (answer.toLowerCase() !== "y") { + console.log("Aborting."); + process.exit(0); + } +} + +const logIt = (msg, type = "info") => logger[type]( + `${DRY_RUN ? '[DRY RUN] ' : ''}${msg}`, +); + +// Let's do it. + +const processItems = async (model, type) => { + const rows = await model + .query() + .where("is_deleted", 0) + .andWhere("enabled", 1) + .groupBy("id") + .allowGraph(model.defaultAllowGraph) + .withGraphFetched(`[${model.defaultExpand.join(", ")}]`) + .orderBy(...model.defaultOrder); + + logIt(`[${type}] Found ${rows.length} rows to process...`); + for (const row of rows) { + if (!DRY_RUN) { + logIt(`[${type}] Regenerating config #${row.id}: ${row.domain_names ? row.domain_names.join(", ") : 'port ' + row.incoming_port}`); + await internalNginx.configure(proxyHostModel, "proxy_host", row); + } else { + logIt(`[${type}] Skipping generation of config #${row.id}: ${row.domain_names ? row.domain_names.join(", ") : 'port ' + row.incoming_port}`); + } + } +}; + +await processItems(proxyHostModel, "Proxy Host"); +await processItems(redirectionHostModel, "Redirection Host"); +await processItems(deadHostModel, "404 Host"); +await processItems(streamModel, "Stream"); + +logIt("Completed", "success"); +process.exit(0); diff --git a/backend/setup.js b/backend/setup.js new file mode 100644 index 0000000..84f4279 --- /dev/null +++ b/backend/setup.js @@ -0,0 +1,166 @@ +import { installPlugins } from "./lib/certbot.js"; +import utils from "./lib/utils.js"; +import { setup as logger } from "./logger.js"; +import authModel from "./models/auth.js"; +import certificateModel from "./models/certificate.js"; +import settingModel from "./models/setting.js"; +import userModel from "./models/user.js"; +import userPermissionModel from "./models/user_permission.js"; + +export const isSetup = async () => { + const row = await userModel.query().select("id").where("is_deleted", 0).first(); + return row?.id > 0; +} + +/** + * Creates a default admin users if one doesn't already exist in the database + * + * @returns {Promise} + */ +const setupDefaultUser = async () => { + const initialAdminEmail = process.env.INITIAL_ADMIN_EMAIL; + const initialAdminPassword = process.env.INITIAL_ADMIN_PASSWORD; + + // This will only create a new user when there are no active users in the database + // and the INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD environment variables are set. + // Otherwise, users should be shown the setup wizard in the frontend. + // I'm keeping this legacy behavior in case some people are automating deployments. + + if (!initialAdminEmail || !initialAdminPassword) { + return Promise.resolve(); + } + + const userIsetup = await isSetup(); + if (!userIsetup) { + // Create a new user and set password + logger.info(`Creating a new user: ${initialAdminEmail} with password: ${initialAdminPassword}`); + + const data = { + is_deleted: 0, + email: initialAdminEmail, + name: "Administrator", + nickname: "Admin", + avatar: "", + roles: ["admin"], + }; + + const user = await userModel + .query() + .insertAndFetch(data); + + await authModel + .query() + .insert({ + user_id: user.id, + type: "password", + secret: initialAdminPassword, + meta: {}, + }); + + await userPermissionModel.query().insert({ + user_id: user.id, + visibility: "all", + proxy_hosts: "manage", + redirection_hosts: "manage", + dead_hosts: "manage", + streams: "manage", + access_lists: "manage", + certificates: "manage", + }); + logger.info("Initial admin setup completed"); + } +}; + +/** + * Creates default settings if they don't already exist in the database + * + * @returns {Promise} + */ +const setupDefaultSettings = async () => { + const row = await settingModel + .query() + .select("id") + .where({ id: "default-site" }) + .first(); + + if (!row?.id) { + await settingModel + .query() + .insert({ + id: "default-site", + name: "Default Site", + description: "What to show when Nginx is hit with an unknown Host", + value: "congratulations", + meta: {}, + }); + logger.info("Default settings added"); + } +}; + +/** + * Installs all Certbot plugins which are required for an installed certificate + * + * @returns {Promise} + */ +const setupCertbotPlugins = async () => { + const certificates = await certificateModel + .query() + .where("is_deleted", 0) + .andWhere("provider", "letsencrypt"); + + if (certificates?.length) { + const plugins = []; + const promises = []; + + certificates.map((certificate) => { + if (certificate.meta && certificate.meta.dns_challenge === true) { + if (plugins.indexOf(certificate.meta.dns_provider) === -1) { + plugins.push(certificate.meta.dns_provider); + } + + // Make sure credentials file exists + const credentials_loc = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; + // Escape single quotes and backslashes + if (typeof certificate.meta.dns_provider_credentials === "string") { + const escapedCredentials = certificate.meta.dns_provider_credentials + .replaceAll("'", "\\'") + .replaceAll("\\", "\\\\"); + const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`; + promises.push(utils.exec(credentials_cmd)); + } + } + return true; + }); + + await installPlugins(plugins); + + if (promises.length) { + await Promise.all(promises); + logger.info(`Added Certbot plugins ${plugins.join(", ")}`); + } + } +}; + +/** + * Starts a timer to call run the logrotation binary every two days + * @returns {Promise} + */ +const setupLogrotation = () => { + const intervalTimeout = 1000 * 60 * 60 * 24 * 2; // 2 days + + const runLogrotate = async () => { + try { + await utils.exec("logrotate /etc/logrotate.d/nginx-proxy-manager"); + logger.info("Logrotate completed."); + } catch (e) { + logger.warn(e); + } + }; + + logger.info("Logrotate Timer initialized"); + setInterval(runLogrotate, intervalTimeout); + // And do this now as well + return runLogrotate(); +}; + +export default () => setupDefaultUser().then(setupDefaultSettings).then(setupCertbotPlugins).then(setupLogrotation); diff --git a/backend/templates/_access.conf b/backend/templates/_access.conf new file mode 100644 index 0000000..4f38854 --- /dev/null +++ b/backend/templates/_access.conf @@ -0,0 +1,25 @@ +{% if access_list_id > 0 %} + {% if access_list.items.length > 0 %} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file /data/access/{{ access_list_id }}; + + {% if access_list.pass_auth == 0 or access_list.pass_auth == false %} + proxy_set_header Authorization ""; + {% endif %} + + {% endif %} + + # Access Rules: {{ access_list.clients | size }} total + {% for client in access_list.clients %} + {{client | nginxAccessRule}} + {% endfor %} + deny all; + + # Access checks must... + {% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %} + satisfy any; + {% else %} + satisfy all; + {% endif %} +{% endif %} diff --git a/backend/templates/_assets.conf b/backend/templates/_assets.conf new file mode 100644 index 0000000..dcb183c --- /dev/null +++ b/backend/templates/_assets.conf @@ -0,0 +1,4 @@ +{% if caching_enabled == 1 or caching_enabled == true -%} + # Asset Caching + include conf.d/include/assets.conf; +{% endif %} \ No newline at end of file diff --git a/backend/templates/_certificates.conf b/backend/templates/_certificates.conf new file mode 100644 index 0000000..efcca5c --- /dev/null +++ b/backend/templates/_certificates.conf @@ -0,0 +1,15 @@ +{% if certificate and certificate_id > 0 -%} +{% if certificate.provider == "letsencrypt" %} + # Let's Encrypt SSL + include conf.d/include/letsencrypt-acme-challenge.conf; + include conf.d/include/ssl-cache.conf; + include conf.d/include/ssl-ciphers.conf; + ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; +{% else %} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem; +{% endif %} +{% endif %} + diff --git a/backend/templates/_certificates_stream.conf b/backend/templates/_certificates_stream.conf new file mode 100644 index 0000000..ba7812f --- /dev/null +++ b/backend/templates/_certificates_stream.conf @@ -0,0 +1,13 @@ +{% if certificate and certificate_id > 0 %} +{% if certificate.provider == "letsencrypt" %} + # Let's Encrypt SSL + include conf.d/include/ssl-cache-stream.conf; + include conf.d/include/ssl-ciphers.conf; + ssl_certificate /etc/letsencrypt/live/npm-{{ certificate_id }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/npm-{{ certificate_id }}/privkey.pem; +{%- else %} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{ certificate_id }}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{ certificate_id }}/privkey.pem; +{%- endif -%} +{%- endif -%} diff --git a/backend/templates/_exploits.conf b/backend/templates/_exploits.conf new file mode 100644 index 0000000..002970d --- /dev/null +++ b/backend/templates/_exploits.conf @@ -0,0 +1,4 @@ +{% if block_exploits == 1 or block_exploits == true %} + # Block Exploits + include conf.d/include/block-exploits.conf; +{% endif %} \ No newline at end of file diff --git a/backend/templates/_forced_ssl.conf b/backend/templates/_forced_ssl.conf new file mode 100644 index 0000000..886e866 --- /dev/null +++ b/backend/templates/_forced_ssl.conf @@ -0,0 +1,11 @@ +{% if certificate and certificate_id > 0 -%} +{% if ssl_forced == 1 or ssl_forced == true %} + # Force SSL + {% if trust_forwarded_proto == true %} + set $trust_forwarded_proto "T"; + {% else %} + set $trust_forwarded_proto "F"; + {% endif %} + include conf.d/include/force-ssl.conf; +{% endif %} +{% endif %} \ No newline at end of file diff --git a/backend/templates/_header_comment.conf b/backend/templates/_header_comment.conf new file mode 100644 index 0000000..8f996d3 --- /dev/null +++ b/backend/templates/_header_comment.conf @@ -0,0 +1,3 @@ +# ------------------------------------------------------------ +# {{ domain_names | join: ", " }} +# ------------------------------------------------------------ \ No newline at end of file diff --git a/backend/templates/_hsts.conf b/backend/templates/_hsts.conf new file mode 100644 index 0000000..26c83ee --- /dev/null +++ b/backend/templates/_hsts.conf @@ -0,0 +1,8 @@ +{% if certificate and certificate_id > 0 -%} +{% if ssl_forced == 1 or ssl_forced == true %} +{% if hsts_enabled == 1 or hsts_enabled == true %} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security $hsts_header always; +{% endif %} +{% endif %} +{% endif %} diff --git a/backend/templates/_hsts_map.conf b/backend/templates/_hsts_map.conf new file mode 100644 index 0000000..27dd1f8 --- /dev/null +++ b/backend/templates/_hsts_map.conf @@ -0,0 +1,3 @@ +map $scheme $hsts_header { + https "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload"; +} \ No newline at end of file diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf new file mode 100644 index 0000000..34a808e --- /dev/null +++ b/backend/templates/_listen.conf @@ -0,0 +1,20 @@ + listen 80; +{% if ipv6 -%} + listen [::]:80; +{% else -%} + #listen [::]:80; +{% endif %} +{% if certificate -%} + listen 443 ssl; +{% if ipv6 -%} + listen [::]:443 ssl; +{% else -%} + #listen [::]:443; +{% endif %} +{% endif %} + server_name {{ domain_names | join: " " }}; +{% if http2_support == 1 or http2_support == true %} + http2 on; +{% else -%} + http2 off; +{% endif %} \ No newline at end of file diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf new file mode 100644 index 0000000..a2ecb16 --- /dev/null +++ b/backend/templates/_location.conf @@ -0,0 +1,24 @@ + location {{ path }} { + {{ advanced_config }} + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }}; + + {% include "_access.conf" %} + {% include "_assets.conf" %} + {% include "_exploits.conf" %} + {% include "_forced_ssl.conf" %} + {% include "_hsts.conf" %} + + {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {% endif %} + } + diff --git a/backend/templates/dead_host.conf b/backend/templates/dead_host.conf new file mode 100644 index 0000000..2e7d2a0 --- /dev/null +++ b/backend/templates/dead_host.conf @@ -0,0 +1,28 @@ +{% include "_header_comment.conf" %} + +{% if enabled %} + +{% include "_hsts_map.conf" %} + +server { +{% include "_listen.conf" %} +{% include "_certificates.conf" %} +{% include "_hsts.conf" %} +{% include "_forced_ssl.conf" %} + + access_log /data/logs/dead-host-{{ id }}_access.log standard; + error_log /data/logs/dead-host-{{ id }}_error.log warn; + +{{ advanced_config }} + +{% if use_default_location %} + location / { +{% include "_hsts.conf" %} + return 404; + } +{% endif %} + + # Custom + include /data/nginx/custom/server_dead[.]conf; +} +{% endif %} diff --git a/backend/templates/default.conf b/backend/templates/default.conf new file mode 100644 index 0000000..cc590f9 --- /dev/null +++ b/backend/templates/default.conf @@ -0,0 +1,46 @@ +# ------------------------------------------------------------ +# Default Site +# ------------------------------------------------------------ +{% if value == "congratulations" %} +# Skipping output, congratulations page configration is baked in. +{%- else %} +server { + listen 80 default; +{% if ipv6 -%} + listen [::]:80 default; +{% else -%} + #listen [::]:80 default; +{% endif %} + server_name default-host.localhost; + access_log /data/logs/default-host_access.log combined; + error_log /data/logs/default-host_error.log warn; +{% include "_exploits.conf" %} + + include conf.d/include/letsencrypt-acme-challenge.conf; + +{%- if value == "404" %} + location / { + return 404; + } +{% endif %} + +{%- if value == "444" %} + location / { + return 444; + } +{% endif %} + +{%- if value == "redirect" %} + location / { + return 301 {{ meta.redirect }}; + } +{%- endif %} + +{%- if value == "html" %} + root /data/nginx/default_www; + location / { + try_files $uri /index.html; + } +{%- endif %} +} +{% endif %} diff --git a/backend/templates/ip_ranges.conf b/backend/templates/ip_ranges.conf new file mode 100644 index 0000000..8ede2bd --- /dev/null +++ b/backend/templates/ip_ranges.conf @@ -0,0 +1,3 @@ +{% for range in ip_ranges %} +set_real_ip_from {{ range }}; +{% endfor %} \ No newline at end of file diff --git a/backend/templates/letsencrypt-request.conf b/backend/templates/letsencrypt-request.conf new file mode 100644 index 0000000..676c8a6 --- /dev/null +++ b/backend/templates/letsencrypt-request.conf @@ -0,0 +1,19 @@ +{% include "_header_comment.conf" %} + +server { + listen 80; +{% if ipv6 -%} + listen [::]:80; +{% endif %} + + server_name {{ domain_names | join: " " }}; + + access_log /data/logs/letsencrypt-requests_access.log standard; + error_log /data/logs/letsencrypt-requests_error.log warn; + + include conf.d/include/letsencrypt-acme-challenge.conf; + + location / { + return 404; + } +} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf new file mode 100644 index 0000000..d23ca46 --- /dev/null +++ b/backend/templates/proxy_host.conf @@ -0,0 +1,53 @@ +{% include "_header_comment.conf" %} + +{% if enabled %} + +{% include "_hsts_map.conf" %} + +server { + set $forward_scheme {{ forward_scheme }}; + set $server "{{ forward_host }}"; + set $port {{ forward_port }}; + +{% include "_listen.conf" %} +{% include "_certificates.conf" %} +{% include "_assets.conf" %} +{% include "_exploits.conf" %} +{% include "_hsts.conf" %} +{% include "_forced_ssl.conf" %} + +{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $http_connection; +proxy_http_version 1.1; +{% endif %} + + access_log /data/logs/proxy-host-{{ id }}_access.log proxy; + error_log /data/logs/proxy-host-{{ id }}_error.log warn; + +{{ advanced_config }} + +{{ locations }} + +{% if use_default_location %} + + location / { + +{% include "_access.conf" %} +{% include "_hsts.conf" %} + + {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {% endif %} + + # Proxy! + include conf.d/include/proxy.conf; + } +{% endif %} + + # Custom + include /data/nginx/custom/server_proxy[.]conf; +} +{% endif %} diff --git a/backend/templates/redirection_host.conf b/backend/templates/redirection_host.conf new file mode 100644 index 0000000..7dd3607 --- /dev/null +++ b/backend/templates/redirection_host.conf @@ -0,0 +1,35 @@ +{% include "_header_comment.conf" %} + +{% if enabled %} + +{% include "_hsts_map.conf" %} + +server { +{% include "_listen.conf" %} +{% include "_certificates.conf" %} +{% include "_assets.conf" %} +{% include "_exploits.conf" %} +{% include "_hsts.conf" %} +{% include "_forced_ssl.conf" %} + + access_log /data/logs/redirection-host-{{ id }}_access.log standard; + error_log /data/logs/redirection-host-{{ id }}_error.log warn; + +{{ advanced_config }} + +{% if use_default_location %} + location / { +{% include "_hsts.conf" %} + + {% if preserve_path == 1 or preserve_path == true %} + return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri; + {% else %} + return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}; + {% endif %} + } +{% endif %} + + # Custom + include /data/nginx/custom/server_redirect[.]conf; +} +{% endif %} diff --git a/backend/templates/stream.conf b/backend/templates/stream.conf new file mode 100644 index 0000000..3a10387 --- /dev/null +++ b/backend/templates/stream.conf @@ -0,0 +1,39 @@ +# ------------------------------------------------------------ +# {{ incoming_port }} TCP: {{ tcp_forwarding }} UDP: {{ udp_forwarding }} +# ------------------------------------------------------------ + +{% if enabled %} +{% if tcp_forwarding == 1 or tcp_forwarding == true -%} +server { + listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %}; + {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %}; + + {%- include "_certificates_stream.conf" %} + + proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; + + access_log /data/logs/stream-{{ id }}_access.log stream; + error_log /data/logs/stream-{{ id }}_error.log warn; + + # Custom + include /data/nginx/custom/server_stream[.]conf; + include /data/nginx/custom/server_stream_tcp[.]conf; +} +{% endif %} + +{% if udp_forwarding == 1 or udp_forwarding == true -%} +server { + listen {{ incoming_port }} udp; + {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp; + + proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; + + access_log /data/logs/stream-{{ id }}_access.log stream; + error_log /data/logs/stream-{{ id }}_error.log warn; + + # Custom + include /data/nginx/custom/server_stream[.]conf; + include /data/nginx/custom/server_stream_udp[.]conf; +} +{% endif %} +{% endif %} diff --git a/backend/validate-schema.js b/backend/validate-schema.js new file mode 100644 index 0000000..b187066 --- /dev/null +++ b/backend/validate-schema.js @@ -0,0 +1,19 @@ +#!/usr/bin/node + +import SwaggerParser from "@apidevtools/swagger-parser"; +import chalk from "chalk"; +import { getCompiledSchema } from "./schema/index.js"; + +const log = console.log; + +getCompiledSchema().then(async (swaggerJSON) => { + try { + const api = await SwaggerParser.validate(swaggerJSON); + console.log("API name: %s, Version: %s", api.info.title, api.info.version); + log(chalk.green("❯ Schema is valid")); + } catch (e) { + console.error(e); + log(chalk.red("❯", e.message), "\n"); + process.exit(1); + } +}); diff --git a/backend/yarn.lock b/backend/yarn.lock new file mode 100644 index 0000000..4fbf7ee --- /dev/null +++ b/backend/yarn.lock @@ -0,0 +1,3072 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz#3bc445ed2eddf72bc2f9eb2e295c696bdc5be725" + integrity sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw== + dependencies: + "@types/json-schema" "^7.0.15" + js-yaml "^4.1.0" + +"@apidevtools/json-schema-ref-parser@^15.3.1": + version "15.3.1" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.3.1.tgz#93f70004a085f62319705e32f25af748752ea5a4" + integrity sha512-FIweGOR9zrNuskfDXn8dfsA4eJEe8LmmGsGSDikEZvgYm36SO36yMhasXSOX7/OTGZ3b7I9iPhOxB24D8xL5uQ== + dependencies: + js-yaml "^4.1.1" + +"@apidevtools/openapi-schemas@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@^12.1.0": + version "12.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz#ef73e5f9e32c2becef6d95b90fb4481b0fec8fe4" + integrity sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng== + dependencies: + "@apidevtools/json-schema-ref-parser" "14.0.1" + "@apidevtools/openapi-schemas" "^2.1.0" + "@apidevtools/swagger-methods" "^3.0.2" + ajv "^8.17.1" + ajv-draft-04 "^1.0.0" + call-me-maybe "^1.0.2" + +"@biomejs/biome@^2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.4.5.tgz#90e75c1b6c60eb5bf8e8bfb193a1fbe9dc868f16" + integrity sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "2.4.5" + "@biomejs/cli-darwin-x64" "2.4.5" + "@biomejs/cli-linux-arm64" "2.4.5" + "@biomejs/cli-linux-arm64-musl" "2.4.5" + "@biomejs/cli-linux-x64" "2.4.5" + "@biomejs/cli-linux-x64-musl" "2.4.5" + "@biomejs/cli-win32-arm64" "2.4.5" + "@biomejs/cli-win32-x64" "2.4.5" + +"@biomejs/cli-darwin-arm64@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.5.tgz#a62472ab3529a3905b16e1f3fdbbc74f2e5f0023" + integrity sha512-lGS4Nd5O3KQJ6TeWv10mElnx1phERhBxqGP/IKq0SvZl78kcWDFMaTtVK+w3v3lusRFxJY78n07PbKplirsU5g== + +"@biomejs/cli-darwin-x64@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.5.tgz#e8bb001fcf6a8c751b0971cccf53993e9ba2e6e9" + integrity sha512-6MoH4tyISIBNkZ2Q5T1R7dLd5BsITb2yhhhrU9jHZxnNSNMWl+s2Mxu7NBF8Y3a7JJcqq9nsk8i637z4gqkJxQ== + +"@biomejs/cli-linux-arm64-musl@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.5.tgz#b7ef7902237f16113061659a4c54aff5ad4513d5" + integrity sha512-iqLDgpzobG7gpBF0fwEVS/LT8kmN7+S0E2YKFDtqliJfzNLnAiV2Nnyb+ehCDCJgAZBASkYHR2o60VQWikpqIg== + +"@biomejs/cli-linux-arm64@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.5.tgz#f110af748965cb1b57624dbbbd7acba729da8780" + integrity sha512-U1GAG6FTjhAO04MyH4xn23wRNBkT6H7NentHh+8UxD6ShXKBm5SY4RedKJzkUThANxb9rUKIPc7B8ew9Xo/cWg== + +"@biomejs/cli-linux-x64-musl@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.5.tgz#c3493eba094216e735538c55354dbc8867b51909" + integrity sha512-NlKa7GpbQmNhZf9kakQeddqZyT7itN7jjWdakELeXyTU3pg/83fTysRRDPJD0akTfKDl6vZYNT9Zqn4MYZVBOA== + +"@biomejs/cli-linux-x64@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.5.tgz#15805550db4e45ffbd6c42d140d0cb5c6dbe07af" + integrity sha512-NdODlSugMzTlENPTa4z0xB82dTUlCpsrOxc43///aNkTLblIYH4XpYflBbf5ySlQuP8AA4AZd1qXhV07IdrHdQ== + +"@biomejs/cli-win32-arm64@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.5.tgz#8dae57dc8ffc1e82e00a59e623b5023c09726a4d" + integrity sha512-EBfrTqRIWOFSd7CQb/0ttjHMR88zm3hGravnDwUA9wHAaCAYsULKDebWcN5RmrEo1KBtl/gDVJMrFjNR0pdGUw== + +"@biomejs/cli-win32-x64@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.5.tgz#0b4d3355e0d6856cff8ed722e05ec7b0652e43ab" + integrity sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw== + +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@noble/hashes@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" + integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@otplib/core@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@otplib/core/-/core-13.3.0.tgz#7f037af6cc5907c2cff9cf6092952088a0ce5a6e" + integrity sha512-pnQDOuCmFVeF/XnboJq9TOJgLoo2idNPJKMymOF8vGqJJ+ReKRYM9bUGjNPRWC0tHjMwu1TXbnzyBp494JgRag== + +"@otplib/hotp@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@otplib/hotp/-/hotp-13.3.0.tgz#135e45c6350ae60bbc1170a1801a6100e49794b8" + integrity sha512-XJMZGz2bg4QJwK7ulvl1GUI2VMn/flaIk/E/BTKAejHsX2kUtPF1bRhlZ2+elq8uU5Fs9Z9FHcQK2CPZNQbbUQ== + dependencies: + "@otplib/core" "13.3.0" + "@otplib/uri" "13.3.0" + +"@otplib/plugin-base32-scure@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.3.0.tgz#3362fc5dd568e9cecac913fd0fa169148c469142" + integrity sha512-/jYbL5S6GB0Ie3XGEWtLIr9s5ZICl/BfmNL7+8/W7usZaUU4GiyLd2S+JGsNCslPyqNekSudD864nDAvRI0s8w== + dependencies: + "@otplib/core" "13.3.0" + "@scure/base" "^2.0.0" + +"@otplib/plugin-crypto-noble@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.3.0.tgz#edfd6c8c54730cfdfc52c6a3fcd797e204fdd0e2" + integrity sha512-wmV+jBVncepgwv99G7Plrdzd0tHfbpXk2U+OD7MO7DzpDqOYEgOPi+IIneksJSTL8QvWdfi+uQEuhnER4fKouA== + dependencies: + "@noble/hashes" "^2.0.1" + "@otplib/core" "13.3.0" + +"@otplib/totp@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@otplib/totp/-/totp-13.3.0.tgz#1d1f61cfd1acd773e1051e6b918227ae34545808" + integrity sha512-XfjGNoN8d9S3Ove2j7AwkVV7+QDFsV7Lm7YwSiezNaHffkWtJ60aJYpmf+01dARdPST71U2ptueMsRJso4sq4A== + dependencies: + "@otplib/core" "13.3.0" + "@otplib/hotp" "13.3.0" + "@otplib/uri" "13.3.0" + +"@otplib/uri@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@otplib/uri/-/uri-13.3.0.tgz#7474d6dc0fdf8ab2d0cf2b79e2001446c4e7b6b3" + integrity sha512-3oh6nBXy+cm3UX9cxEAGZiDrfxHU2gfelYFV+XNCx+8dq39VaQVymwlU2yjPZiMAi/3agaUeEftf2RwM5F+Cyg== + dependencies: + "@otplib/core" "13.3.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@scure/base@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.0.0.tgz#ba6371fddf92c2727e88ad6ab485db6e624f9a98" + integrity sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w== + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + +agentkeepalive@^4.1.3: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-draft-04@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" + integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv@^8.0.0, ajv@^8.17.1, ajv@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc" + integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +"aproba@^1.0.3 || ^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.1.0.tgz#75500a190313d95c64e871e7e4284c6ac219f0b1" + integrity sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew== + +archiver-utils@^5.0.0, archiver-utils@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" + integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== + dependencies: + glob "^10.0.0" + graceful-fs "^4.2.0" + is-stream "^2.0.1" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +archiver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" + integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== + dependencies: + archiver-utils "^5.0.2" + async "^3.2.4" + buffer-crc32 "^1.0.0" + readable-stream "^4.0.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^6.0.1" + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +asn1@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +aws-ssl-profiles@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" + integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== + +b4a@^1.6.4: + version "1.8.0" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.8.0.tgz#1ca3ba0edc9469aaabef5647e769a83d50180b1a" + integrity sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.3.tgz#6337a2f23e0604a30481423432f99eac603599f9" + integrity sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g== + +bare-events@^2.7.0: + version "2.8.2" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.8.2.tgz#7b3e10bd8e1fc80daf38bb516921678f566ab89f" + integrity sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +basic-ftp@^5.0.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.2.0.tgz#7c2dff63c918bde60e6bad1f2ff93dcf5137a40a" + integrity sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw== + +batchflow@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/batchflow/-/batchflow-0.4.0.tgz#7d419df79b6b7587b06f9ea34f96ccef6f74e5b5" + integrity sha512-XwQQoCGPUjdLWzmpAvRNZc91wnBYuKLmj52d9LLZ1Ww06ow5RBqBt8kUmU9/3ZvPq88j7Elh3V4cEhgNKXbIlQ== + +bcrypt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-6.0.0.tgz#86643fddde9bcd0ad91400b063003fa4b0312835" + integrity sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg== + dependencies: + node-addon-api "^8.3.0" + node-gyp-build "^4.8.4" + +better-sqlite3@^12.6.2: + version "12.6.2" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-12.6.2.tgz#770649f28a62e543a360f3dfa1afe4cc944b1937" + integrity sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA== + dependencies: + bindings "^1.5.0" + prebuild-install "^7.1.1" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +blueimp-md5@^2.16.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" + integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== + +body-parser@^2.2.1, body-parser@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.2.tgz#1a32cdb966beaf68de50a9dfbe5b58f83cb8890c" + integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.3" + http-errors "^2.0.0" + iconv-lite "^0.7.0" + on-finished "^2.4.1" + qs "^6.14.1" + raw-body "^3.0.1" + type-is "^2.0.1" + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + +brace-expansion@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f" + integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw== + dependencies: + balanced-match "^4.0.2" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +call-me-maybe@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + +chalk@^2.3.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colorette@2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +compress-commons@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" + integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^6.0.0" + is-stream "^2.0.1" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.1.tgz#a8b7bbeb2904befdfb6787e5c0c086959f605f9b" + integrity sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q== + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" + integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== + dependencies: + crc-32 "^1.2.0" + readable-stream "^4.0.0" + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + +db-errors@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/db-errors/-/db-errors-0.2.3.tgz#a6a38952e00b20e790f2695a6446b3c65497ffa2" + integrity sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4, debug@^4.3.3, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +debug@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + +depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +detect-libc@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +email-validator@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" + integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events-universal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/events-universal/-/events-universal-1.0.1.tgz#b56a84fd611b6610e0a2d0f09f80fdf931e2dfe6" + integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw== + dependencies: + bare-events "^2.7.0" + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +express-fileupload@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.5.2.tgz#4da70ba6f2ffd4c736eab0776445865a9dbd9bfa" + integrity sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg== + dependencies: + busboy "^1.6.0" + +express@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.1" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + depd "^2.0.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + +fast-uri@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA== + dependencies: + escape-string-regexp "^1.0.5" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== + dependencies: + locate-path "^2.0.0" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-uri@^6.0.1: + version "6.0.5" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16" + integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + +getopts@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" + integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^10.0.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +gravatar@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/gravatar/-/gravatar-1.8.2.tgz#f298642b1562ed685af2ae938dbe31ec0c542cc1" + integrity sha512-GdRwLM3oYpFQKy47MKuluw9hZ2gaCtiKPbDGdcDEuYDKlc8eNnW27KYL9LVbIDzEsx88WtDWQm2ClBcsgBnj6w== + dependencies: + blueimp-md5 "^2.16.0" + email-validator "^2.0.4" + querystring "0.2.0" + yargs "^15.4.1" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-cache-semantics@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + +http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@^0.7.0, iconv-lite@^0.7.2, iconv-lite@~0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e" + integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ip-address@^10.0.1: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4" + integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + +is-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-stream@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b" + integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-yaml@^4.1.0, js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonwebtoken@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== + dependencies: + jws "^4.0.1" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + +knex@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c" + integrity sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw== + dependencies: + colorette "2.0.19" + commander "^10.0.0" + debug "4.3.4" + escalade "^3.1.1" + esm "^3.2.25" + get-package-type "^0.1.0" + getopts "2.3.0" + interpret "^2.2.0" + lodash "^4.17.21" + pg-connection-string "2.6.2" + rechoir "^0.8.0" + resolve-from "^5.0.0" + tarn "^3.0.2" + tildify "2.0.0" + +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + +liquidjs@10.24.0: + version "10.24.0" + resolved "https://registry.yarnpkg.com/liquidjs/-/liquidjs-10.24.0.tgz#1aa832189b48b4102049dfa7a8eea40281fa9389" + integrity sha512-TAUNAdgwaAXjjcUFuYVJm9kOVH7zc0mTKxsG9t9Lu4qdWjB2BEblyVIYpjWcmJLMGgiYqnGNJjpNMHx0gp/46A== + dependencies: + commander "^10.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw== + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.23: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + +long@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru.min@^1.1.0, lru.min@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.4.tgz#6ea1737a8c1ba2300cc87ad46910a4bdffa0117b" + integrity sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA== + +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^3.0.0, mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@^10.2.1: + version "10.2.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f" + integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw== + dependencies: + brace-expansion "^5.0.2" + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.3: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment@^2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mysql2@^3.18.2: + version "3.18.2" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.18.2.tgz#11d30fbc03a456d076760bd60e6ebf17abc6323d" + integrity sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw== + dependencies: + aws-ssl-profiles "^1.1.2" + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.7.2" + long "^5.3.2" + lru.min "^1.1.4" + named-placeholders "^1.1.6" + sql-escaper "^1.3.3" + +named-placeholders@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.6.tgz#c50c6920b43f258f59c16add1e56654f5cc02bb5" + integrity sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w== + dependencies: + lru.min "^1.1.0" + +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== + +negotiator@^0.6.2, negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + +node-abi@^3.3.0: + version "3.87.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.87.0.tgz#423e28fea5c2f195fddd98acded9938c001ae6dd" + integrity sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ== + dependencies: + semver "^7.3.5" + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-addon-api@^8.3.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.5.0.tgz#c91b2d7682fa457d2e1c388150f0dff9aafb8f3f" + integrity sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A== + +node-gyp-build@^4.8.4: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + +node-rsa@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d" + integrity sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw== + dependencies: + asn1 "^0.2.4" + +nodemon@^3.1.14: + version "3.1.14" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51" + integrity sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^10.2.1" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +objection@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/objection/-/objection-3.1.5.tgz#53c32f6b6cba2958bc28cf723de96c2676da8286" + integrity sha512-Hx/ipAwXSuRBbOMWFKtRsAN0yITafqXtWB4OT4Z9wED7ty1h7bOnBdhLtcNus23GwLJqcMsRWdodL2p5GwlnfQ== + dependencies: + ajv "^8.17.1" + ajv-formats "^2.1.1" + db-errors "^0.2.3" + +on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +otplib@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/otplib/-/otplib-13.3.0.tgz#2ead040ab29d1a829d1d7c510b059a3e4c76b2b0" + integrity sha512-VYMKyyDG8yt2q+z58sz54/EIyTh7+tyMrjeemR44iVh5+dkKtIs57irTqxjH+IkAL1uMmG1JIFhG5CxTpqdU5g== + dependencies: + "@otplib/core" "13.3.0" + "@otplib/hotp" "13.3.0" + "@otplib/plugin-base32-scure" "13.3.0" + "@otplib/plugin-crypto-noble" "13.3.0" + "@otplib/totp" "13.3.0" + "@otplib/uri" "13.3.0" + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== + dependencies: + p-limit "^1.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pac-proxy-agent@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df" + integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.1.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.6" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.5" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== + dependencies: + process "^0.11.1" + util "^0.10.3" + +pg-cloudflare@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz#386035d4bfcf1a7045b026f8b21acf5353f14d65" + integrity sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ== + +pg-connection-string@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475" + integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA== + +pg-connection-string@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.11.0.tgz#5dca53ff595df33ba9db812e181b19909866d10b" + integrity sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.12.0.tgz#798c84ec7d42ba03fff056ebe575daa6e14feab8" + integrity sha512-eIJ0DES8BLaziFHW7VgJEBPi5hg3Nyng5iKpYtj3wbcAUV9A1wLgWiY7ajf/f/oO1wfxt83phXPY8Emztg7ITg== + +pg-protocol@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.12.0.tgz#e9827f3e1dae6cdcb78d009cba5bb699d88ae998" + integrity sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg== + +pg-types@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.19.0: + version "8.19.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.19.0.tgz#2cb45322471c1ed05786ee7ec09bd91abdfe3eeb" + integrity sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ== + dependencies: + pg-connection-string "^2.11.0" + pg-pool "^3.12.0" + pg-protocol "^1.12.0" + pg-types "2.2.0" + pgpass "1.0.5" + optionalDependencies: + pg-cloudflare "^1.3.0" + +pgpass@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + +pkg-conf@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" + integrity sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g== + dependencies: + find-up "^2.0.0" + load-json-file "^4.0.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz#c40b3da0222c500ff1e51c5d7014b60b79697c7a" + integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prebuild-install@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^2.0.0" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.1, process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-agent@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d" + integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.6" + lru-cache "^7.14.1" + pac-proxy-agent "^7.1.0" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.5" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +qs@^6.14.0, qs@^6.14.1: + version "6.15.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3" + integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ== + dependencies: + side-channel "^1.1.0" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.0.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + +serve-static@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +signale@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/signale/-/signale-1.4.0.tgz#c4be58302fb0262ac00fc3d886a7c113759042f1" + integrity sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w== + dependencies: + chalk "^2.3.2" + figures "^2.0.0" + pkg-conf "^2.1.0" + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks-proxy-agent@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" + integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.6.2, socks@^2.8.3: + version "2.8.7" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" + integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + dependencies: + ip-address "^10.0.1" + smart-buffer "^4.2.0" + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sql-escaper@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/sql-escaper/-/sql-escaper-1.3.3.tgz#65faf89f048d26bb9a75566b82b5990ddf8a5b7f" + integrity sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw== + +sqlite3@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.7.tgz#59ca1053c1ab38647396586edad019b1551041b7" + integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== + dependencies: + bindings "^1.5.0" + node-addon-api "^7.0.0" + prebuild-install "^7.1.1" + tar "^6.1.11" + optionalDependencies: + node-gyp "8.x" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +streamx@^2.15.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.23.0.tgz#7d0f3d00d4a6c5de5728aecd6422b4008d66fd0b" + integrity sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg== + dependencies: + events-universal "^1.0.0" + fast-fifo "^1.3.2" + text-decoder "^1.1.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +supports-color@^5.3.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tar-fs@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" + integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.0.0: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tarn@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" + integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== + +temp-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-3.0.0.tgz#7f147b42ee41234cc6ba3138cd8e8aa2302acffa" + integrity sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw== + +temp-write@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-6.0.1.tgz#b1ed81e80e120ba4957db3e01e010669d7c15870" + integrity sha512-6bj9LlNld+knzEOQvnZK6YxiPF+foOUjvG/WoWj1/Mt9c6f2kQCPsh8KZ+NyTk0AejubTQSPpx2alcswE1bF8g== + dependencies: + graceful-fs "^4.2.11" + is-stream "^4.0.1" + temp-dir "^3.0.0" + +text-decoder@^1.1.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.7.tgz#5d073a9a74b9c0a9d28dfadcab96b604af57d8ba" + integrity sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ== + dependencies: + b4a "^1.6.4" + +tildify@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" + integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tslib@^2.0.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + +vary@^1.1.2, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +zip-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" + integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== + dependencies: + archiver-utils "^5.0.0" + compress-commons "^6.0.2" + readable-stream "^4.0.0" diff --git a/docker/.dive-ci b/docker/.dive-ci new file mode 100644 index 0000000..7a408bd --- /dev/null +++ b/docker/.dive-ci @@ -0,0 +1,14 @@ +rules: + # If the efficiency is measured below X%, mark as failed. + # Expressed as a ratio between 0-1. + lowestEfficiency: 0.99 + + # If the amount of wasted space is at least X or larger than X, mark as failed. + # Expressed in B, KB, MB, and GB. + highestWastedBytes: 15MB + + # If the amount of wasted space makes up for X% or more of the image, mark as failed. + # Note: the base image layer is NOT included in the total image size. + # Expressed as a ratio between 0-1; fails if the threshold is met or crossed. + highestUserWastedPercent: 0.02 + diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..18d2971 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,73 @@ +# This is a Dockerfile intended to be built using `docker buildx` +# for multi-arch support. Building with `docker build` may have unexpected results. + +# This file assumes that the frontend has been built using ./scripts/frontend-build + +FROM nginxproxymanager/testca AS testca +FROM nginxproxymanager/nginx-full:certbot-node + +ARG TARGETPLATFORM +ARG BUILD_VERSION +ARG BUILD_COMMIT +ARG BUILD_DATE + +# See: https://github.com/just-containers/s6-overlay/blob/master/README.md +ENV SUPPRESS_NO_CONFIG_WARNING=1 \ + S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ + S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ + S6_FIX_ATTRS_HIDDEN=1 \ + S6_KILL_FINISH_MAXTIME=10000 \ + S6_VERBOSITY=1 \ + NODE_ENV=production \ + NPM_BUILD_VERSION="${BUILD_VERSION}" \ + NPM_BUILD_COMMIT="${BUILD_COMMIT}" \ + NPM_BUILD_DATE="${BUILD_DATE}" \ + NODE_OPTIONS="--openssl-legacy-provider" + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ + && apt-get update \ + && apt-get install -y --no-install-recommends jq logrotate wireguard-tools iptables qrencode \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# s6 overlay +COPY docker/scripts/install-s6 /tmp/install-s6 +RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 + +EXPOSE 80 81 443 51820/udp + +COPY backend /app +COPY frontend/dist /app/frontend + +WORKDIR /app +RUN yarn install \ + && yarn cache clean + +# add late to limit cache-busting by modifications +COPY docker/rootfs / +COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt + +# Remove frontend service not required for prod, dev nginx config as well +RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \ + && chmod 644 /etc/logrotate.d/nginx-proxy-manager + +VOLUME [ "/data", "/etc/wireguard" ] + +# WireGuard environment defaults +ENV WG_ENABLED=true \ + WG_HOST= \ + WG_PORT=51820 \ + WG_DEFAULT_ADDRESS=10.8.0.0/24 \ + WG_DNS=1.1.1.1,8.8.8.8 \ + WG_MTU=1420 \ + WG_PERSISTENT_KEEPALIVE=25 \ + WG_ALLOWED_IPS=0.0.0.0/0,::/0 + +ENTRYPOINT [ "/init" ] + +LABEL org.label-schema.schema-version="1.0" \ + org.label-schema.license="MIT" \ + org.label-schema.name="npm-wg" \ + org.label-schema.description="Nginx Proxy Manager + WireGuard VPN Manager" \ + org.label-schema.url="https://github.com/npm-wg/npm-wg" \ + org.label-schema.cmd="docker run --rm -ti --cap-add=NET_ADMIN --cap-add=SYS_MODULE npm-wg:latest" diff --git a/docker/ci.env b/docker/ci.env new file mode 100644 index 0000000..11e2b2f --- /dev/null +++ b/docker/ci.env @@ -0,0 +1,8 @@ +AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0 +AUTHENTIK_REDIS__HOST=authentik-redis +AUTHENTIK_POSTGRESQL__HOST=pgdb.internal +AUTHENTIK_POSTGRESQL__USER=authentik +AUTHENTIK_POSTGRESQL__NAME=authentik +AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj +AUTHENTIK_BOOTSTRAP_PASSWORD=admin +AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com diff --git a/docker/ci/postgres/authentik.sql.gz b/docker/ci/postgres/authentik.sql.gz new file mode 100644 index 0000000..49665d4 Binary files /dev/null and b/docker/ci/postgres/authentik.sql.gz differ diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 0000000..f0f5ec6 --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,38 @@ +FROM nginxproxymanager/testca AS testca +FROM nginxproxymanager/nginx-full:certbot-node +LABEL maintainer="Jamie Curnow " + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ENV SUPPRESS_NO_CONFIG_WARNING=1 \ + S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ + S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ + S6_FIX_ATTRS_HIDDEN=1 \ + S6_KILL_FINISH_MAXTIME=10000 \ + S6_VERBOSITY=2 \ + NODE_OPTIONS="--openssl-legacy-provider" + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ + && apt-get update \ + && apt-get install -y jq python3-pip logrotate moreutils \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Task +WORKDIR /usr +RUN curl -sL https://taskfile.dev/install.sh | sh +WORKDIR /root + +COPY rootfs / +COPY scripts/install-s6 /tmp/install-s6 +RUN rm -f /etc/nginx/conf.d/production.conf \ + && chmod 644 /etc/logrotate.d/nginx-proxy-manager \ + && /tmp/install-s6 "${TARGETPLATFORM}" \ + && rm -f /tmp/install-s6 \ + && chmod 644 -R /root/.cache + +# Certs for testing purposes +COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt + +EXPOSE 80 81 443 +ENTRYPOINT [ "/init" ] diff --git a/docker/dev/dnsrouter-config.json b/docker/dev/dnsrouter-config.json new file mode 100644 index 0000000..a4e538d --- /dev/null +++ b/docker/dev/dnsrouter-config.json @@ -0,0 +1,28 @@ +{ + "log": { + "format": "nice", + "level": "debug" + }, + "servers": [ + { + "host": "0.0.0.0", + "port": 53, + "upstreams": [ + { + "regex": "website[0-9]+.example\\.com", + "upstream": "127.0.0.11" + }, + { + "regex": ".*\\.example\\.com", + "upstream": "1.1.1.1" + }, + { + "regex": "local", + "nxdomain": true + } + ], + "internal": null, + "default_upstream": "127.0.0.11" + } + ] +} diff --git a/docker/dev/letsencrypt.ini b/docker/dev/letsencrypt.ini new file mode 100644 index 0000000..93647b6 --- /dev/null +++ b/docker/dev/letsencrypt.ini @@ -0,0 +1,7 @@ +text = True +non-interactive = True +webroot-path = /data/letsencrypt-acme-challenge +key-type = ecdsa +elliptic-curve = secp384r1 +preferred-chain = ISRG Root X1 +server = diff --git a/docker/dev/pdns-db.sql b/docker/dev/pdns-db.sql new file mode 100644 index 0000000..c182cf7 --- /dev/null +++ b/docker/dev/pdns-db.sql @@ -0,0 +1,255 @@ +/* + +How this was generated: +1. bring up an empty pdns stack +2. use api to create a zone ... + +curl -X POST \ + 'http://npm.dev:8081/api/v1/servers/localhost/zones' \ + --header 'X-API-Key: npm' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "name": "example.com.", + "kind": "Native", + "masters": [], + "nameservers": [ + "ns1.pdns.", + "ns2.pdns." + ] +}' + +3. Dump sql: + +docker exec -ti npm.pdns.db mysqldump -u pdns -p pdns + +*/ + +---------------------------------------------------------------------- + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `comments` +-- + +DROP TABLE IF EXISTS `comments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `comments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `type` varchar(10) NOT NULL, + `modified_at` int(11) NOT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL, + `comment` text CHARACTER SET utf8mb3 NOT NULL, + PRIMARY KEY (`id`), + KEY `comments_name_type_idx` (`name`,`type`), + KEY `comments_order_idx` (`domain_id`,`modified_at`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `comments` +-- + +LOCK TABLES `comments` WRITE; +/*!40000 ALTER TABLE `comments` DISABLE KEYS */; +/*!40000 ALTER TABLE `comments` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cryptokeys` +-- + +DROP TABLE IF EXISTS `cryptokeys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cryptokeys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `flags` int(11) NOT NULL, + `active` tinyint(1) DEFAULT NULL, + `published` tinyint(1) DEFAULT 1, + `content` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `domainidindex` (`domain_id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cryptokeys` +-- + +LOCK TABLES `cryptokeys` WRITE; +/*!40000 ALTER TABLE `cryptokeys` DISABLE KEYS */; +/*!40000 ALTER TABLE `cryptokeys` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `domainmetadata` +-- + +DROP TABLE IF EXISTS `domainmetadata`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `domainmetadata` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `kind` varchar(32) DEFAULT NULL, + `content` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `domainmetadata_idx` (`domain_id`,`kind`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `domainmetadata` +-- + +LOCK TABLES `domainmetadata` WRITE; +/*!40000 ALTER TABLE `domainmetadata` DISABLE KEYS */; +INSERT INTO `domainmetadata` VALUES +(1,1,'SOA-EDIT-API','DEFAULT'); +/*!40000 ALTER TABLE `domainmetadata` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `domains` +-- + +DROP TABLE IF EXISTS `domains`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `domains` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `master` varchar(128) DEFAULT NULL, + `last_check` int(11) DEFAULT NULL, + `type` varchar(8) NOT NULL, + `notified_serial` int(10) unsigned DEFAULT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL, + `options` varchar(64000) DEFAULT NULL, + `catalog` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name_index` (`name`), + KEY `catalog_idx` (`catalog`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `domains` +-- + +LOCK TABLES `domains` WRITE; +/*!40000 ALTER TABLE `domains` DISABLE KEYS */; +INSERT INTO `domains` VALUES +(1,'example.com','',NULL,'NATIVE',NULL,'',NULL,NULL); +/*!40000 ALTER TABLE `domains` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `records` +-- + +DROP TABLE IF EXISTS `records`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `records` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `type` varchar(10) DEFAULT NULL, + `content` varchar(64000) DEFAULT NULL, + `ttl` int(11) DEFAULT NULL, + `prio` int(11) DEFAULT NULL, + `disabled` tinyint(1) DEFAULT 0, + `ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `auth` tinyint(1) DEFAULT 1, + PRIMARY KEY (`id`), + KEY `nametype_index` (`name`,`type`), + KEY `domain_id` (`domain_id`), + KEY `ordername` (`ordername`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `records` +-- + +LOCK TABLES `records` WRITE; +/*!40000 ALTER TABLE `records` DISABLE KEYS */; +INSERT INTO `records` VALUES +(1,1,'example.com','NS','ns1.pdns',1500,0,0,NULL,1), +(2,1,'example.com','NS','ns2.pdns',1500,0,0,NULL,1), +(3,1,'example.com','SOA','a.misconfigured.dns.server.invalid hostmaster.example.com 2023030501 10800 3600 604800 3600',1500,0,0,NULL,1); +/*!40000 ALTER TABLE `records` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `supermasters` +-- + +DROP TABLE IF EXISTS `supermasters`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `supermasters` ( + `ip` varchar(64) NOT NULL, + `nameserver` varchar(255) NOT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 NOT NULL, + PRIMARY KEY (`ip`,`nameserver`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `supermasters` +-- + +LOCK TABLES `supermasters` WRITE; +/*!40000 ALTER TABLE `supermasters` DISABLE KEYS */; +/*!40000 ALTER TABLE `supermasters` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tsigkeys` +-- + +DROP TABLE IF EXISTS `tsigkeys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tsigkeys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `algorithm` varchar(50) DEFAULT NULL, + `secret` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `namealgoindex` (`name`,`algorithm`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tsigkeys` +-- + +LOCK TABLES `tsigkeys` WRITE; +/*!40000 ALTER TABLE `tsigkeys` DISABLE KEYS */; +/*!40000 ALTER TABLE `tsigkeys` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; diff --git a/docker/dev/squid.conf b/docker/dev/squid.conf new file mode 100644 index 0000000..cdd749f --- /dev/null +++ b/docker/dev/squid.conf @@ -0,0 +1,92 @@ +# WELCOME TO SQUID 6.6 +# ---------------------------- +# +# This is the documentation for the Squid configuration file. +# This documentation can also be found online at: +# http://www.squid-cache.org/Doc/config/ +# +# You may wish to look at the Squid home page and wiki for the +# FAQ and other documentation: +# http://www.squid-cache.org/ +# https://wiki.squid-cache.org/SquidFaq +# https://wiki.squid-cache.org/ConfigExamples +# + +# Example rule allowing access from your local networks. +# Adapt to list your (internal) IP networks from where browsing +# should be allowed +acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) +acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) +acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) +acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines +acl localnet src 172.0.0.0/8 +acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) +acl localnet src fc00::/7 # RFC 4193 local private network range +acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines + +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 81 +acl Safe_ports port 443 # https + +# +# Recommended minimum Access Permission configuration: +# +# Deny requests to certain unsafe ports +http_access deny !Safe_ports + +# Deny CONNECT to other than secure SSL ports +http_access deny CONNECT !SSL_ports + +# Only allow cachemgr access from localhost +http_access allow localhost manager +http_access deny manager + +# This default configuration only allows localhost requests because a more +# permissive Squid installation could introduce new attack vectors into the +# network by proxying external TCP connections to unprotected services. +http_access allow localhost + +# The two deny rules below are unnecessary in this default configuration +# because they are followed by a "deny all" rule. However, they may become +# critically important when you start allowing external requests below them. + +# Protect web applications running on the same server as Squid. They often +# assume that only local users can access them at "localhost" ports. +http_access deny to_localhost + +# Protect cloud servers that provide local users with sensitive info about +# their server via certain well-known link-local (a.k.a. APIPA) addresses. +http_access deny to_linklocal + +# +# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS +# +include /etc/squid/conf.d/*.conf + +# For example, to allow access from your local networks, you may uncomment the +# following rule (and/or add rules that match your definition of "local"): +# http_access allow localnet + +# And finally deny all other access to this proxy +http_access deny all + +# Squid normally listens to port 3128 +http_port 3128 + +# Leave coredumps in the first cache dir +coredump_dir /var/spool/squid + +# +# Add any of your own refresh_pattern entries above these. +# +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims +refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims +refresh_pattern \/InRelease$ 0 0% 0 refresh-ims +refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims +# example pattern for deb packages +#refresh_pattern (\.deb|\.udeb)$ 129600 100% 129600 +refresh_pattern . 0 20% 4320 + diff --git a/docker/docker-compose.ci.mysql.yml b/docker/docker-compose.ci.mysql.yml new file mode 100644 index 0000000..108a1dc --- /dev/null +++ b/docker/docker-compose.ci.mysql.yml @@ -0,0 +1,28 @@ +# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. +services: + + fullstack: + environment: + DB_MYSQL_HOST: 'db-mysql' + DB_MYSQL_PORT: '3306' + DB_MYSQL_USER: 'npm' + DB_MYSQL_PASSWORD: 'npmpass' + DB_MYSQL_NAME: 'npm' + depends_on: + - db-mysql + + db-mysql: + image: jc21/mariadb-aria + environment: + MYSQL_ROOT_PASSWORD: 'npm' + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD: 'npmpass' + MARIADB_AUTO_UPGRADE: '1' + volumes: + - mysql_vol:/var/lib/mysql + networks: + - fulltest + +volumes: + mysql_vol: diff --git a/docker/docker-compose.ci.postgres.yml b/docker/docker-compose.ci.postgres.yml new file mode 100644 index 0000000..b8c4244 --- /dev/null +++ b/docker/docker-compose.ci.postgres.yml @@ -0,0 +1,87 @@ +# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. +services: + cypress: + environment: + CYPRESS_stack: "postgres" + + fullstack: + environment: + DB_POSTGRES_HOST: "pgdb.internal" + DB_POSTGRES_PORT: "5432" + DB_POSTGRES_USER: "npm" + DB_POSTGRES_PASSWORD: "npmpass" + DB_POSTGRES_NAME: "npm" + depends_on: + - db-postgres + - authentik + - authentik-worker + - authentik-ldap + + db-postgres: + image: postgres:17 + environment: + POSTGRES_USER: "npm" + POSTGRES_PASSWORD: "npmpass" + POSTGRES_DB: "npm" + volumes: + - psql_vol:/var/lib/postgresql/data + - ./ci/postgres:/docker-entrypoint-initdb.d + networks: + fulltest: + aliases: + - pgdb.internal + + authentik-redis: + image: "redis:alpine" + command: --save 60 1 --loglevel warning + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis_vol:/data + networks: + - fulltest + + authentik: + image: ghcr.io/goauthentik/server:2024.10.1 + restart: unless-stopped + command: server + env_file: + - ci.env + depends_on: + - authentik-redis + - db-postgres + networks: + - fulltest + + authentik-worker: + image: ghcr.io/goauthentik/server:2024.10.1 + restart: unless-stopped + command: worker + env_file: + - ci.env + depends_on: + - authentik-redis + - db-postgres + networks: + - fulltest + + authentik-ldap: + image: ghcr.io/goauthentik/ldap:2024.10.1 + environment: + AUTHENTIK_HOST: "http://authentik:9000" + AUTHENTIK_INSECURE: "true" + AUTHENTIK_TOKEN: "wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp" + restart: unless-stopped + depends_on: + - authentik + networks: + - fulltest + +volumes: + psql_vol: + redis_vol: diff --git a/docker/docker-compose.ci.sqlite.yml b/docker/docker-compose.ci.sqlite.yml new file mode 100644 index 0000000..1c5be48 --- /dev/null +++ b/docker/docker-compose.ci.sqlite.yml @@ -0,0 +1,9 @@ +# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. +services: + + fullstack: + environment: + DB_SQLITE_FILE: '/data/mydb.sqlite' + PUID: 1000 + PGID: 1000 + DISABLE_IPV6: 'true' diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml new file mode 100644 index 0000000..1bf6dad --- /dev/null +++ b/docker/docker-compose.ci.yml @@ -0,0 +1,133 @@ +# WARNING: This is a CI docker-compose file used for building +# and testing of the entire app, it should not be used for production. +# This is a base compose file, it should be extended with a +# docker-compose.ci.*.yml file +services: + fullstack: + image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" + environment: + TZ: "${TZ:-Australia/Brisbane}" + DEBUG: "true" + CI: "true" + FORCE_COLOR: 1 + # Required for DNS Certificate provisioning in CI + LE_SERVER: "https://ca.internal/acme/acme/directory" + REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt" + volumes: + - "npm_data_ci:/data" + - "npm_le_ci:/etc/letsencrypt" + - "./dev/letsencrypt.ini:/etc/letsencrypt.ini:ro" + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + healthcheck: + test: ["CMD", "/usr/bin/check-health"] + interval: 10s + timeout: 3s + expose: + - "80/tcp" + - "81/tcp" + - "443/tcp" + - "1500/tcp" + - "1501/tcp" + - "1502/tcp" + - "1503/tcp" + networks: + fulltest: + aliases: + - website1.example.com + - website2.example.com + - website3.example.com + + stepca: + image: jc21/testca + volumes: + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + networks: + fulltest: + aliases: + - ca.internal + + pdns: + image: pschiffe/pdns-mysql:4.8 + volumes: + - "/etc/localtime:/etc/localtime:ro" + environment: + PDNS_master: "yes" + PDNS_api: "yes" + PDNS_api_key: "npm" + PDNS_webserver: "yes" + PDNS_webserver_address: "0.0.0.0" + PDNS_webserver_password: "npm" + PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8" + PDNS_version_string: "anonymous" + PDNS_default_ttl: 1500 + PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8" + PDNS_gmysql_host: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - pdns-db + networks: + fulltest: + aliases: + - ns1.pdns + - ns2.pdns + + pdns-db: + image: mariadb + environment: + MYSQL_ROOT_PASSWORD: "pdns" + MYSQL_DATABASE: "pdns" + MYSQL_USER: "pdns" + MYSQL_PASSWORD: "pdns" + volumes: + - "pdns_mysql_vol:/var/lib/mysql" + - "/etc/localtime:/etc/localtime:ro" + - "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro" + networks: + - fulltest + + dnsrouter: + image: jc21/dnsrouter + volumes: + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro + networks: + - fulltest + + cypress: + image: "${IMAGE}-cypress:ci-${BUILD_NUMBER}" + build: + context: ../ + dockerfile: test/cypress/Dockerfile + environment: + HTTP_PROXY: "squid:3128" + HTTPS_PROXY: "squid:3128" + volumes: + - "cypress_logs:/test/results" + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + command: cypress run --browser chrome --config-file=cypress/config/ci.mjs + networks: + - fulltest + + squid: + image: ubuntu/squid + volumes: + - "./dev/squid.conf:/etc/squid/squid.conf:ro" + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + networks: + - fulltest + +volumes: + cypress_logs: + npm_data_ci: + npm_le_ci: + pdns_mysql_vol: + +networks: + fulltest: + name: "npm-${BRANCH_LOWER}-ci-${BUILD_NUMBER}" diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..4d519f8 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,272 @@ +# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. +services: + fullstack: + image: npm2dev:core + container_name: npm2dev.core + build: + context: ./ + dockerfile: ./dev/Dockerfile + ports: + - 3080:80 + - 3081:81 + - 3443:443 + networks: + nginx_proxy_manager: + aliases: + - website1.example.com + - website2.example.com + - website3.example.com + environment: + TZ: "${TZ:-Australia/Brisbane}" + PUID: 1000 + PGID: 1000 + FORCE_COLOR: 1 + # specifically for dev: + DEBUG: "true" + DEVELOPMENT: "true" + LE_STAGING: "true" + # db: + # DB_MYSQL_HOST: 'db' + # DB_MYSQL_PORT: '3306' + # DB_MYSQL_USER: 'npm' + # DB_MYSQL_PASSWORD: 'npm' + # DB_MYSQL_NAME: 'npm' + # db-postgres: + DB_POSTGRES_HOST: "pgdb.internal" + DB_POSTGRES_PORT: "5432" + DB_POSTGRES_USER: "npm" + DB_POSTGRES_PASSWORD: "npmpass" + DB_POSTGRES_NAME: "npm" + # DB_SQLITE_FILE: "/data/database.sqlite" + # DISABLE_IPV6: "true" + # Required for DNS Certificate provisioning testing: + LE_SERVER: "https://ca.internal/acme/acme/directory" + REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt" + volumes: + - npm_data:/data + - le_data:/etc/letsencrypt + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - ../backend:/app + - ../frontend:/frontend + - "/etc/localtime:/etc/localtime:ro" + healthcheck: + test: ["CMD", "/usr/bin/check-health"] + interval: 10s + timeout: 3s + depends_on: + - db + - db-postgres + - authentik + - authentik-worker + - authentik-ldap + working_dir: /app + + db: + image: jc21/mariadb-aria + container_name: npm2dev.db + ports: + - 33306:3306 + networks: + - nginx_proxy_manager + environment: + TZ: "${TZ:-Australia/Brisbane}" + MYSQL_ROOT_PASSWORD: "npm" + MYSQL_DATABASE: "npm" + MYSQL_USER: "npm" + MYSQL_PASSWORD: "npm" + volumes: + - db_data:/var/lib/mysql + - "/etc/localtime:/etc/localtime:ro" + + db-postgres: + image: postgres:17 + container_name: npm2dev.db-postgres + environment: + POSTGRES_USER: "npm" + POSTGRES_PASSWORD: "npmpass" + POSTGRES_DB: "npm" + volumes: + - psql_data:/var/lib/postgresql/data + - ./ci/postgres:/docker-entrypoint-initdb.d + networks: + nginx_proxy_manager: + aliases: + - pgdb.internal + + stepca: + image: jc21/testca + container_name: npm2dev.stepca + volumes: + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + networks: + nginx_proxy_manager: + aliases: + - ca.internal + + dnsrouter: + image: jc21/dnsrouter + container_name: npm2dev.dnsrouter + volumes: + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro + networks: + - nginx_proxy_manager + + swagger: + image: swaggerapi/swagger-ui:latest + container_name: npm2dev.swagger + ports: + - 3082:80 + environment: + URL: "http://npm:81/api/schema" + PORT: "80" + depends_on: + - fullstack + + squid: + image: ubuntu/squid + container_name: npm2dev.squid + volumes: + - "./dev/squid.conf:/etc/squid/squid.conf:ro" + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + networks: + - nginx_proxy_manager + ports: + - 8128:3128 + + pdns: + image: pschiffe/pdns-mysql:4.8 + container_name: npm2dev.pdns + volumes: + - "/etc/localtime:/etc/localtime:ro" + environment: + PDNS_master: "yes" + PDNS_api: "yes" + PDNS_api_key: "npm" + PDNS_webserver: "yes" + PDNS_webserver_address: "0.0.0.0" + PDNS_webserver_password: "npm" + PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8" + PDNS_version_string: "anonymous" + PDNS_default_ttl: 1500 + PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8" + PDNS_gmysql_host: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - pdns-db + networks: + nginx_proxy_manager: + aliases: + - ns1.pdns + - ns2.pdns + + pdns-db: + image: mariadb + container_name: npm2dev.pdns-db + environment: + MYSQL_ROOT_PASSWORD: "pdns" + MYSQL_DATABASE: "pdns" + MYSQL_USER: "pdns" + MYSQL_PASSWORD: "pdns" + volumes: + - "pdns_mysql:/var/lib/mysql" + - "/etc/localtime:/etc/localtime:ro" + - "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro" + networks: + - nginx_proxy_manager + + cypress: + image: npm2dev:cypress + container_name: npm2dev.cypress + build: + context: ../ + dockerfile: test/cypress/Dockerfile + environment: + HTTP_PROXY: "squid:3128" + HTTPS_PROXY: "squid:3128" + volumes: + - "../test/results:/results" + - "./dev/resolv.conf:/etc/resolv.conf:ro" + - "/etc/localtime:/etc/localtime:ro" + command: cypress run --browser chrome --config-file=cypress/config/ci.mjs + networks: + - nginx_proxy_manager + + authentik-redis: + image: "redis:alpine" + container_name: npm2dev.authentik-redis + command: --save 60 1 --loglevel warning + networks: + - nginx_proxy_manager + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis_data:/data + + authentik: + image: ghcr.io/goauthentik/server:2024.10.1 + container_name: npm2dev.authentik + restart: unless-stopped + command: server + networks: + - nginx_proxy_manager + env_file: + - ci.env + ports: + - 9000:9000 + depends_on: + - authentik-redis + - db-postgres + + authentik-worker: + image: ghcr.io/goauthentik/server:2024.10.1 + container_name: npm2dev.authentik-worker + restart: unless-stopped + command: worker + networks: + - nginx_proxy_manager + env_file: + - ci.env + depends_on: + - authentik-redis + - db-postgres + + authentik-ldap: + image: ghcr.io/goauthentik/ldap:2024.10.1 + container_name: npm2dev.authentik-ldap + networks: + - nginx_proxy_manager + environment: + AUTHENTIK_HOST: "http://authentik:9000" + AUTHENTIK_INSECURE: "true" + AUTHENTIK_TOKEN: "wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp" + restart: unless-stopped + depends_on: + - authentik + +volumes: + npm_data: + name: npm2dev_core_data + le_data: + name: npm2dev_le_data + db_data: + name: npm2dev_db_data + pdns_mysql: + name: npnpm2dev_pdns_mysql + psql_data: + name: npm2dev_psql_data + redis_data: + name: npm2dev_redis_data + +networks: + nginx_proxy_manager: + name: npm2dev_network diff --git a/docker/rootfs/etc/letsencrypt.ini b/docker/rootfs/etc/letsencrypt.ini new file mode 100644 index 0000000..aae53b9 --- /dev/null +++ b/docker/rootfs/etc/letsencrypt.ini @@ -0,0 +1,6 @@ +text = True +non-interactive = True +webroot-path = /data/letsencrypt-acme-challenge +key-type = ecdsa +elliptic-curve = secp384r1 +preferred-chain = ISRG Root X1 diff --git a/docker/rootfs/etc/logrotate.d/nginx-proxy-manager b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager new file mode 100644 index 0000000..de97729 --- /dev/null +++ b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager @@ -0,0 +1,27 @@ +/data/logs/*_access.log /data/logs/*/access.log { + su npm npm + create 0644 + weekly + rotate 4 + missingok + notifempty + compress + sharedscripts + postrotate + kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true + endscript +} + +/data/logs/*_error.log /data/logs/*/error.log { + su npm npm + create 0644 + weekly + rotate 10 + missingok + notifempty + compress + sharedscripts + postrotate + kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true + endscript +} diff --git a/docker/rootfs/etc/nginx/conf.d/default.conf b/docker/rootfs/etc/nginx/conf.d/default.conf new file mode 100644 index 0000000..b3f61eb --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/default.conf @@ -0,0 +1,39 @@ +# "You are not configured" page, which is the default if another default doesn't exist +server { + listen 80; + listen [::]:80; + + set $forward_scheme "http"; + set $server "127.0.0.1"; + set $port "80"; + + server_name localhost-nginx-proxy-manager; + access_log /data/logs/fallback_http_access.log standard; + error_log /data/logs/fallback_http_error.log warn; + include conf.d/include/assets.conf; + include conf.d/include/block-exploits.conf; + include conf.d/include/letsencrypt-acme-challenge.conf; + + location / { + index index.html; + root /var/www/html; + } +} + +# First 443 Host, which is the default if another default doesn't exist +server { + listen 443 ssl; + listen [::]:443 ssl; + + set $forward_scheme "https"; + set $server "127.0.0.1"; + set $port "443"; + + server_name localhost; + access_log /data/logs/fallback_http_access.log standard; + error_log /dev/null crit; + include conf.d/include/ssl-ciphers.conf; + ssl_reject_handshake on; + + return 444; +} diff --git a/docker/rootfs/etc/nginx/conf.d/dev.conf b/docker/rootfs/etc/nginx/conf.d/dev.conf new file mode 100644 index 0000000..67efc0f --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/dev.conf @@ -0,0 +1,37 @@ +server { + listen 81 default; + listen [::]:81 default; + + server_name nginxproxymanager-dev; + root /app/frontend/dist; + access_log /dev/null; + + location /api { + return 302 /api/; + } + + location /api/ { + add_header X-Served-By $host; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass http://127.0.0.1:3000/; + + proxy_read_timeout 15m; + proxy_send_timeout 15m; + } + + location / { + add_header X-Served-By $host; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass http://127.0.0.1:5173; + } +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/.gitignore b/docker/rootfs/etc/nginx/conf.d/include/.gitignore new file mode 100644 index 0000000..5291fe1 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/.gitignore @@ -0,0 +1 @@ +resolvers.conf diff --git a/docker/rootfs/etc/nginx/conf.d/include/assets.conf b/docker/rootfs/etc/nginx/conf.d/include/assets.conf new file mode 100644 index 0000000..5a90beb --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/assets.conf @@ -0,0 +1,31 @@ +location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ { + if_modified_since off; + + # use the public cache + proxy_cache public-cache; + proxy_cache_key $host$request_uri; + + # ignore these headers for media + proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires; + + # cache 200s and also 404s (not ideal but there are a few 404 images for some reason) + proxy_cache_valid any 30m; + proxy_cache_valid 404 1m; + + # strip this header to avoid If-Modified-Since requests + proxy_hide_header Last-Modified; + proxy_hide_header Cache-Control; + proxy_hide_header Vary; + + proxy_cache_bypass 0; + proxy_no_cache 0; + + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404; + proxy_connect_timeout 5s; + proxy_read_timeout 45s; + + expires @30m; + access_log off; + + include conf.d/include/proxy.conf; +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf new file mode 100644 index 0000000..093bda2 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf @@ -0,0 +1,136 @@ +## Block SQL injections +set $block_sql_injections 0; + +if ($query_string ~ "union.*select.*\(") { + set $block_sql_injections 1; +} + +if ($query_string ~ "union.*all.*select.*") { + set $block_sql_injections 1; +} + +if ($query_string ~ "concat.*\(") { + set $block_sql_injections 1; +} + +if ($block_sql_injections = 1) { + return 403; +} + +## Block file injections +set $block_file_injections 0; + +if ($query_string ~ "[a-zA-Z0-9_]=http://") { + set $block_file_injections 1; +} + +if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") { + set $block_file_injections 1; +} + +if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { + set $block_file_injections 1; +} + +if ($block_file_injections = 1) { + return 403; +} + +## Block common exploits +set $block_common_exploits 0; + +if ($query_string ~ "(<|%3C).*script.*(>|%3E)") { + set $block_common_exploits 1; +} + +if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") { + set $block_common_exploits 1; +} + +if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") { + set $block_common_exploits 1; +} + +if ($query_string ~ "proc/self/environ") { + set $block_common_exploits 1; +} + +if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") { + set $block_common_exploits 1; +} + +if ($query_string ~ "base64_(en|de)code\(.*\)") { + set $block_common_exploits 1; +} + +if ($block_common_exploits = 1) { + return 403; +} + +## Block spam +set $block_spam 0; + +if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") { + set $block_spam 1; +} + +if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") { + set $block_spam 1; +} + +if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") { + set $block_spam 1; +} + +if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") { + set $block_spam 1; +} + +if ($block_spam = 1) { + return 403; +} + +## Block user agents +set $block_user_agents 0; + +# Disable Akeeba Remote Control 2.5 and earlier +if ($http_user_agent ~ "Indy Library") { + set $block_user_agents 1; +} + +# Common bandwidth hoggers and hacking tools. +if ($http_user_agent ~ "libwww-perl") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "GetRight") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "GetWeb!") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Go!Zilla") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Download Demon") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "Go-Ahead-Got-It") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "TurnitinBot") { + set $block_user_agents 1; +} + +if ($http_user_agent ~ "GrabNet") { + set $block_user_agents 1; +} + +if ($block_user_agents = 1) { + return 403; +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf new file mode 100644 index 0000000..8e58c64 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf @@ -0,0 +1,32 @@ +set $test ""; +if ($scheme = "http") { + set $test "H"; +} +if ($request_uri = /.well-known/acme-challenge/test-challenge) { + set $test "${test}T"; +} + +# Check if the ssl staff has been handled +set $test_ssl_handled ""; +if ($trust_forwarded_proto = "") { + set $trust_forwarded_proto "F"; +} +if ($trust_forwarded_proto = "T") { + set $test_ssl_handled "${test_ssl_handled}T"; +} +if ($http_x_forwarded_proto = "https") { + set $test_ssl_handled "${test_ssl_handled}S"; +} +if ($http_x_forwarded_scheme = "https") { + set $test_ssl_handled "${test_ssl_handled}S"; +} +if ($test_ssl_handled = "TSS") { + set $test_ssl_handled "TS"; +} +if ($test_ssl_handled = "TS") { + set $test "${test}S"; +} + +if ($test = H) { + return 301 https://$host$request_uri; +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf b/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf new file mode 100644 index 0000000..3424932 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf @@ -0,0 +1,2 @@ +# This should be left blank is it is populated programatically +# by the application backend. diff --git a/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf b/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf new file mode 100644 index 0000000..ff2a782 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf @@ -0,0 +1,30 @@ +# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx) +# We use ^~ here, so that we don't check other regexes (for speed-up). We actually MUST cancel +# other regex checks, because in our other config files have regex rule that denies access to files with dotted names. +location ^~ /.well-known/acme-challenge/ { + # Since this is for letsencrypt authentication of a domain and they do not give IP ranges of their infrastructure + # we need to open up access by turning off auth and IP ACL for this location. + auth_basic off; + auth_request off; + allow all; + + # Set correct content type. According to this: + # https://community.letsencrypt.org/t/using-the-webroot-domain-verification-method/1445/29 + # Current specification requires "text/plain" or no content header at all. + # It seems that "text/plain" is a safe option. + default_type "text/plain"; + + # This directory must be the same as in /etc/letsencrypt/cli.ini + # as "webroot-path" parameter. Also don't forget to set "authenticator" parameter + # there to "webroot". + # Do NOT use alias, use root! Target directory is located here: + # /var/www/common/letsencrypt/.well-known/acme-challenge/ + root /data/letsencrypt-acme-challenge; +} + +# Hide /acme-challenge subdirectory and return 404 on all requests. +# It is somewhat more secure than letting Nginx return 403. +# Ending slash is important! +location = /.well-known/acme-challenge/ { + return 404; +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/log-proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/log-proxy.conf new file mode 100644 index 0000000..448b052 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/log-proxy.conf @@ -0,0 +1,4 @@ +log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"'; +log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; + +access_log /data/logs/fallback_http_access.log proxy; diff --git a/docker/rootfs/etc/nginx/conf.d/include/log-stream.conf b/docker/rootfs/etc/nginx/conf.d/include/log-stream.conf new file mode 100644 index 0000000..db3ad31 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/log-stream.conf @@ -0,0 +1,3 @@ +log_format stream '[$time_local] [Client $remote_addr:$remote_port] $protocol $status $bytes_sent $bytes_received $session_time [Sent-to $upstream_addr] [Sent $upstream_bytes_sent] [Received $upstream_bytes_received] [Time $upstream_connect_time] $ssl_protocol $ssl_cipher'; + +access_log /data/logs/fallback_stream_access.log stream; diff --git a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf new file mode 100644 index 0000000..fe2c2f2 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf @@ -0,0 +1,8 @@ +add_header X-Served-By $host; +proxy_set_header Host $host; +proxy_set_header X-Forwarded-Scheme $x_forwarded_scheme; +proxy_set_header X-Forwarded-Proto $x_forwarded_proto; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Real-IP $remote_addr; +proxy_pass $forward_scheme://$server:$port$request_uri; + diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf new file mode 100644 index 0000000..433555d --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf @@ -0,0 +1,2 @@ +ssl_session_timeout 5m; +ssl_session_cache shared:SSL_stream:50m; diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf new file mode 100644 index 0000000..aa7ba2c --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf @@ -0,0 +1,2 @@ +ssl_session_timeout 5m; +ssl_session_cache shared:SSL:50m; diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf new file mode 100644 index 0000000..b5dacfb --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf @@ -0,0 +1,4 @@ +# intermediate configuration. tweak to your needs. +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; +ssl_prefer_server_ciphers off; diff --git a/docker/rootfs/etc/nginx/conf.d/production.conf b/docker/rootfs/etc/nginx/conf.d/production.conf new file mode 100644 index 0000000..877e51d --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/production.conf @@ -0,0 +1,33 @@ +# Admin Interface +server { + listen 81 default; + listen [::]:81 default; + + server_name nginxproxymanager; + root /app/frontend; + access_log /dev/null; + + location /api { + return 302 /api/; + } + + location /api/ { + add_header X-Served-By $host; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass http://127.0.0.1:3000/; + + proxy_read_timeout 15m; + proxy_send_timeout 15m; + } + + location / { + index index.html; + if ($request_uri ~ ^/(.*)\.html$) { + return 302 /$1; + } + try_files $uri $uri.html $uri/ /index.html; + } +} diff --git a/docker/rootfs/etc/nginx/mime.types b/docker/rootfs/etc/nginx/mime.types new file mode 100644 index 0000000..7c7cdef --- /dev/null +++ b/docker/rootfs/etc/nginx/mime.types @@ -0,0 +1,96 @@ +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf new file mode 100644 index 0000000..bdba3b3 --- /dev/null +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -0,0 +1,111 @@ +# run nginx in foreground +daemon off; +pid /run/nginx/nginx.pid; +user npm; + +# Set number of worker processes automatically based on number of CPU cores. +worker_processes auto; + +# Enables the use of JIT for regular expressions to speed-up their processing. +pcre_jit on; + +error_log /data/logs/fallback_error.log warn; + +# Includes files with directives to load dynamic modules. +include /etc/nginx/modules/*.conf; + +# Custom +include /data/nginx/custom/root_top[.]conf; + +events { + include /data/nginx/custom/events[.]conf; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + tcp_nopush on; + tcp_nodelay on; + client_body_temp_path /tmp/nginx/body 1 2; + keepalive_timeout 90s; + proxy_connect_timeout 90s; + proxy_send_timeout 90s; + proxy_read_timeout 90s; + ssl_prefer_server_ciphers on; + gzip on; + proxy_ignore_client_abort off; + client_max_body_size 2000m; + server_names_hash_bucket_size 1024; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Accept-Encoding ""; + proxy_cache off; + proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; + proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; + + # Log format and fallback log file + include /etc/nginx/conf.d/include/log-proxy[.]conf; + + # Dynamically generated resolvers file + include /etc/nginx/conf.d/include/resolvers[.]conf; + + # Default upstream scheme + map $host $forward_scheme { + default http; + } + + # Handle upstream X-Forwarded-Proto and X-Forwarded-Scheme header + map $http_x_forwarded_proto $x_forwarded_proto { + "http" "http"; + "https" "https"; + default $scheme; + } + map $http_x_forwarded_scheme $x_forwarded_scheme { + "http" "http"; + "https" "https"; + default $scheme; + } + + # Real IP Determination + + # Local subnets: + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; # Includes Docker subnet + set_real_ip_from 192.168.0.0/16; + # NPM generated CDN ip ranges: + include conf.d/include/ip_ranges[.]conf; + # always put the following 2 lines after ip subnets: + real_ip_header X-Real-IP; + real_ip_recursive on; + + # Custom + include /data/nginx/custom/http_top[.]conf; + + # Files generated by NPM + include /etc/nginx/conf.d/*.conf; + include /data/nginx/default_host/*.conf; + include /data/nginx/proxy_host/*.conf; + include /data/nginx/redirection_host/*.conf; + include /data/nginx/dead_host/*.conf; + include /data/nginx/temp/*.conf; + + # Custom + include /data/nginx/custom/http[.]conf; +} + +stream { + # Log format and fallback log file + include /etc/nginx/conf.d/include/log-stream[.]conf; + + # Files generated by NPM + include /data/nginx/stream/*.conf; + + # Custom + include /data/nginx/custom/stream[.]conf; +} + +# Custom +include /data/nginx/custom/root[.]conf; diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/dependencies.d/prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run new file mode 100644 index 0000000..37a95f7 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run @@ -0,0 +1,21 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /usr/bin/common.sh + +cd /app || exit 1 + +log_info 'Starting backend ...' + +if [ "${DEVELOPMENT:-}" = 'true' ]; then + s6-setuidgid "$PUID:$PGID" yarn install + exec s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js" +else + while : + do + s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --abort_on_uncaught_exception --max_old_space_size=250 index.js" + sleep 1 + done +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/dependencies.d/prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run new file mode 100644 index 0000000..91ed3fa --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run @@ -0,0 +1,21 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +# This service is DEVELOPMENT only. + +if [ "$DEVELOPMENT" = 'true' ]; then + . /usr/bin/common.sh + cd /frontend || exit 1 + HOME=$NPMHOME + export HOME + mkdir -p /frontend/dist + chown -R "$PUID:$PGID" /frontend/dist + + log_info 'Starting frontend ...' + s6-setuidgid "$PUID:$PGID" yarn install + exec s6-setuidgid "$PUID:$PGID" yarn dev +else + exit 0 +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run new file mode 100644 index 0000000..3e8d8d0 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -0,0 +1,9 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /usr/bin/common.sh + +log_info 'Starting nginx ...' +exec s6-setuidgid "$PUID:$PGID" nginx diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh new file mode 100644 index 0000000..d2e62f3 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh @@ -0,0 +1,22 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /usr/bin/common.sh + +if [ "$(id -u)" != "0" ]; then + log_fatal "This docker container must be run as root, do not specify a user.\nYou can specify PUID and PGID env vars to run processes as that user and group after initialization." +fi + +if [ "$DEBUG" = "true" ]; then + set -x +fi + +. /etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh +. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh +. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh +. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh +. /etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh +. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh +. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh new file mode 100644 index 0000000..ea10019 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh @@ -0,0 +1,40 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info "Configuring $NPMUSER user ..." + +if id -u "$NPMUSER" 2>/dev/null; then + # user already exists + usermod -u "$PUID" "$NPMUSER" +else + # Add user + useradd -o -u "$PUID" -U -d "$NPMHOME" -s /bin/false "$NPMUSER" +fi + +log_info "Configuring $NPMGROUP group ..." +if [ "$(get_group_id "$NPMGROUP")" = '' ]; then + # Add group. This will not set the id properly if it's already taken + groupadd -f -g "$PGID" "$NPMGROUP" +else + groupmod -o -g "$PGID" "$NPMGROUP" +fi + +# Set the group ID and check it +groupmod -o -g "$PGID" "$NPMGROUP" +if [ "$(get_group_id "$NPMGROUP")" != "$PGID" ]; then + echo "ERROR: Unable to set group id properly" + exit 1 +fi + +# Set the group against the user and check it +usermod -G "$PGID" "$NPMGROUP" +if [ "$(id -g "$NPMUSER")" != "$PGID" ] ; then + echo "ERROR: Unable to set group against the user properly" + exit 1 +fi + +# Home for user +mkdir -p "$NPMHOME" +chown -R "$PUID:$PGID" "$NPMHOME" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh new file mode 100644 index 0000000..2f59ef4 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh @@ -0,0 +1,41 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Checking paths ...' + +# Ensure /data is mounted +if [ ! -d '/data' ]; then + log_fatal '/data is not mounted! Check your docker configuration.' +fi +# Ensure /etc/letsencrypt is mounted +if [ ! -d '/etc/letsencrypt' ]; then + log_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.' +fi + +# Create required folders +mkdir -p \ + /data/nginx \ + /data/custom_ssl \ + /data/logs \ + /data/access \ + /data/nginx/default_host \ + /data/nginx/default_www \ + /data/nginx/proxy_host \ + /data/nginx/redirection_host \ + /data/nginx/stream \ + /data/nginx/dead_host \ + /data/nginx/temp \ + /data/letsencrypt-acme-challenge \ + /run/nginx \ + /tmp/nginx/body \ + /var/log/nginx \ + /var/lib/nginx/cache/public \ + /var/lib/nginx/cache/private \ + /var/cache/nginx/proxy_temp + +touch /var/log/nginx/error.log || true +chmod 777 /var/log/nginx/error.log || true +chmod -R 777 /var/cache/nginx || true +chmod 644 /etc/logrotate.d/nginx-proxy-manager diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh new file mode 100644 index 0000000..fa94651 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh @@ -0,0 +1,60 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Setting ownership ...' + +# root +chown root /tmp/nginx + +locations=( + "/data" + "/etc/letsencrypt" + "/run/nginx" + "/tmp/nginx" + "/var/cache/nginx" + "/var/lib/logrotate" + "/var/lib/nginx" + "/var/log/nginx" + "/etc/nginx/nginx" + "/etc/nginx/nginx.conf" + "/etc/nginx/conf.d" +) + +chownit() { + local dir="$1" + local recursive="${2:-true}" + + local have + have="$(stat -c '%u:%g' "$dir")" + echo "- $dir ... " + + if [ "$have" != "$PUID:$PGID" ]; then + if [ "$recursive" = 'true' ] && [ -d "$dir" ]; then + chown -R "$PUID:$PGID" "$dir" + else + chown "$PUID:$PGID" "$dir" + fi + echo " DONE" + else + echo " SKIPPED" + fi +} + +for loc in "${locations[@]}"; do + chownit "$loc" +done + +if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then + log_info 'Skipping ownership change of certbot directories' +else + log_info 'Changing ownership of certbot directories, this may take some time ...' + chownit "/opt/certbot" false + chownit "/opt/certbot/bin" false + + # Handle all site-packages directories efficiently + find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do + chownit "$SITE_PACKAGES_DIR" + done +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh new file mode 100644 index 0000000..6818653 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh @@ -0,0 +1,16 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Dynamic resolvers ...' + +# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` +# thanks @tfmm +if [ "$(is_true "${DISABLE_RESOLVER:-}")" = '0' ]; then + if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then + echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf + else + echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf + fi +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh new file mode 100644 index 0000000..edc9649 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh @@ -0,0 +1,42 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +# This command reads the `DISABLE_IPV6` env var and will either enable +# or disable ipv6 in all nginx configs based on this setting. + +set -e + +log_info 'IPv6 ...' + +process_folder () { + FILES=$(find "$1" -type f -name "*.conf") + SED_REGEX= + + if [ "$(is_true "${DISABLE_IPV6:-}")" = '1' ]; then + # IPV6 is disabled + echo "Disabling IPV6 in hosts in: $1" + SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g' + else + # IPV6 is enabled + echo "Enabling IPV6 in hosts in: $1" + SED_REGEX='s/^(\s*)#listen \[::\]/\1listen [::]/g' + fi + + for FILE in $FILES + do + echo "- ${FILE}" + TMPFILE="${FILE}.tmp" + if sed -E "$SED_REGEX" "$FILE" > "$TMPFILE" && [ -s "$TMPFILE" ]; then + mv "$TMPFILE" "$FILE" + else + echo "WARNING: skipping ${FILE} — sed produced empty output" >&2 + rm -f "$TMPFILE" + fi + done + + # ensure the files are still owned by the npm user + chown -R "$PUID:$PGID" "$1" +} + +process_folder /etc/nginx/conf.d +process_folder /data/nginx diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh new file mode 100644 index 0000000..faa22ac --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +# in s6, environmental variables are written as text files for s6 to monitor +# search through full-path filenames for files ending in "__FILE" +log_info 'Docker secrets ...' + +for FILENAME in $(find /var/run/s6/container_environment/ | grep "__FILE$"); do + echo "[secret-init] Evaluating ${FILENAME##*/} ..." + + # set SECRETFILE to the contents of the full-path textfile + SECRETFILE=$(cat "${FILENAME}") + # if SECRETFILE exists / is not null + if [[ -f "${SECRETFILE}" ]]; then + # strip the appended "__FILE" from environmental variable name ... + STRIPFILE=$(echo "${FILENAME}" | sed "s/__FILE//g") + # echo "[secret-init] Set STRIPFILE to ${STRIPFILE}" # DEBUG - rm for prod! + + # ... and set value to contents of secretfile + # since s6 uses text files, this is effectively "export ..." + printf $(cat "${SECRETFILE}") > "${STRIPFILE}" + # echo "[secret-init] Set ${STRIPFILE##*/} to $(cat ${STRIPFILE})" # DEBUG - rm for prod!" + echo "Success: ${STRIPFILE##*/} set from ${FILENAME##*/}" + + else + echo "Cannot find secret in ${FILENAME}" + fi +done diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh new file mode 100644 index 0000000..48ba639 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh @@ -0,0 +1,18 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e +set +x + +echo " +------------------------------------- + _ _ ____ __ __ +| \ | | _ \| \/ | +| \| | |_) | |\/| | +| |\ | __/| | | | +|_| \_|_| |_| |_| +------------------------------------- +User: $NPMUSER PUID:$PUID ID:$(id -u "$NPMUSER") GROUP:$(id -g "$NPMUSER") +Group: $NPMGROUP PGID:$PGID ID:$(get_group_id "$NPMGROUP") +------------------------------------- +" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up new file mode 100644 index 0000000..896a01b --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up @@ -0,0 +1,2 @@ +# shellcheck shell=bash +/etc/s6-overlay/s6-rc.d/prepare/00-all.sh diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/backend b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/backend new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frontend b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frontend new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/prepare new file mode 100644 index 0000000..e69de29 diff --git a/docker/rootfs/root/.bashrc b/docker/rootfs/root/.bashrc new file mode 100644 index 0000000..1deb975 --- /dev/null +++ b/docker/rootfs/root/.bashrc @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -t 1 ]; then + export PS1="\e[1;34m[\e[1;33m\u@\e[1;32mdocker-\h\e[1;37m:\w\[\e[1;34m]\e[1;36m\\$ \e[0m" +fi + +# Aliases +alias l='ls -lAsh --color' +alias ls='ls -C1 --color' +alias cp='cp -ip' +alias rm='rm -i' +alias mv='mv -i' +alias h='cd ~;clear;' + +. /etc/os-release + +echo -e -n '\E[1;34m' +figlet -w 120 "NginxProxyManager" +echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, ${ID:-debian} \E[1;32m${VERSION:-unknown}\E[1;36m, Certbot \E[1;32m$(certbot --version)\E[0m" +echo -e -n '\E[1;34m' +cat /built-for-arch +echo -e '\E[0m' diff --git a/docker/rootfs/usr/bin/check-health b/docker/rootfs/usr/bin/check-health new file mode 100644 index 0000000..bcf5552 --- /dev/null +++ b/docker/rootfs/usr/bin/check-health @@ -0,0 +1,11 @@ +#!/bin/bash + +OK=$(curl --silent http://127.0.0.1:81/api/ | jq --raw-output '.status') + +if [ "$OK" == "OK" ]; then + echo "OK" + exit 0 +else + echo "NOT OK" + exit 1 +fi diff --git a/docker/rootfs/usr/bin/common.sh b/docker/rootfs/usr/bin/common.sh new file mode 100644 index 0000000..4652987 --- /dev/null +++ b/docker/rootfs/usr/bin/common.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +set -e + +CYAN='\E[1;36m' +BLUE='\E[1;34m' +YELLOW='\E[1;33m' +RED='\E[1;31m' +RESET='\E[0m' +export CYAN BLUE YELLOW RED RESET + +PUID=${PUID:-0} +PGID=${PGID:-0} + +# If changing the username and group name below, +# ensure all references to this user is also changed. +# See docker/rootfs/etc/logrotate.d/nginx-proxy-manager +# and docker/rootfs/etc/nginx/nginx.conf +NPMUSER=npm +NPMGROUP=npm +NPMHOME=/tmp/npmuserhome +export NPMUSER NPMGROUP NPMHOME + +if [[ "$PUID" -ne '0' ]] && [ "$PGID" = '0' ]; then + # set group id to same as user id, + # the user probably forgot to specify the group id and + # it would be rediculous to intentionally use the root group + # for a non-root user + PGID=$PUID +fi + +export PUID PGID + +log_info () { + echo -e "${BLUE}❯ ${CYAN}$1${RESET}" +} + +log_error () { + echo -e "${RED}❯ $1${RESET}" +} + +# The `run` file will only execute 1 line so this helps keep things +# logically separated + +log_fatal () { + echo -e "${RED}--------------------------------------${RESET}" + echo -e "${RED}ERROR: $1${RESET}" + echo -e "${RED}--------------------------------------${RESET}" + /run/s6/basedir/bin/halt + exit 1 +} + +# param $1: group_name +get_group_id () { + if [ "${1:-}" != '' ]; then + getent group "$1" | cut -d: -f3 + fi +} + +# param $1: value +is_true () { + VAL=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') + if [ "$VAL" == 'true' ] || [ "$VAL" == 'on' ] || [ "$VAL" == '1' ] || [ "$VAL" == 'yes' ]; then + echo '1' + else + echo '0' + fi +} diff --git a/docker/rootfs/var/www/html/index.html b/docker/rootfs/var/www/html/index.html new file mode 100644 index 0000000..703db88 --- /dev/null +++ b/docker/rootfs/var/www/html/index.html @@ -0,0 +1,24 @@ + + + + + + + Default Site + + + + +
+
+

Congratulations!

+

You've successfully started the Nginx Proxy Manager.

+

If you're seeing this site then you're trying to access a host that isn't set up yet.

+

Log in to the Admin panel to get started.

+
+

Powered by Nginx Proxy Manager

+
+ + diff --git a/docker/scripts/install-s6 b/docker/scripts/install-s6 new file mode 100644 index 0000000..a06dd78 --- /dev/null +++ b/docker/scripts/install-s6 @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Note: This script is designed to be run inside a Docker Build for a container + +CYAN='\E[1;36m' +YELLOW='\E[1;33m' +BLUE='\E[1;34m' +GREEN='\E[1;32m' +RESET='\E[0m' + +S6_OVERLAY_VERSION=3.2.1.0 +TARGETPLATFORM=${1:-linux/amd64} + +# Determine the correct binary file for the architecture given +case $TARGETPLATFORM in + linux/arm64) + S6_ARCH=aarch64 + ;; + + *) + S6_ARCH=x86_64 + ;; +esac + +echo -e "${BLUE}❯ ${CYAN}Installing S6-overlay v${S6_OVERLAY_VERSION} for ${YELLOW}${TARGETPLATFORM} (${S6_ARCH})${RESET}" + +curl -L -o '/tmp/s6-overlay-noarch.tar.xz' "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" +curl -L -o "/tmp/s6-overlay-${S6_ARCH}.tar.xz" "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" +tar -C / -Jxpf '/tmp/s6-overlay-noarch.tar.xz' +tar -C / -Jxpf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" + +rm -rf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" + +echo -e "${BLUE}❯ ${GREEN}S6-overlay install Complete${RESET}" diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b8acd7b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,14 @@ +dist +node_modules +ts +.temp +.cache +.vitepress/cache + +.yarn/* +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions +*.gz +*.tgz diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..52586bf --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,61 @@ +import { defineConfig, type DefaultTheme } from 'vitepress'; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Nginx Proxy Manager", + description: "Expose your services easily and securely", + head: [ + ["link", { rel: "icon", href: "/icon.png" }], + ["meta", { name: "description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt" }], + ["meta", { property: "og:title", content: "Nginx Proxy Manager" }], + ["meta", { property: "og:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}], + ["meta", { property: "og:type", content: "website" }], + ["meta", { property: "og:url", content: "https://nginxproxymanager.com/" }], + ["meta", { property: "og:image", content: "https://nginxproxymanager.com/icon.png" }], + ["meta", { name: "twitter:card", content: "summary"}], + ["meta", { name: "twitter:title", content: "Nginx Proxy Manager"}], + ["meta", { name: "twitter:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}], + ["meta", { name: "twitter:image", content: "https://nginxproxymanager.com/icon.png"}], + ["meta", { name: "twitter:alt", content: "Nginx Proxy Manager"}], + // GA + ['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}], + ['script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');"], + ], + sitemap: { + hostname: 'https://nginxproxymanager.com' + }, + metaChunk: true, + srcDir: './src', + outDir: './dist', + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + logo: { src: '/logo.svg', width: 24, height: 24 }, + nav: [ + { text: 'Setup', link: '/setup/' }, + ], + sidebar: [ + { + items: [ + // { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/' }, + { text: 'Screenshots', link: '/screenshots/' }, + { text: 'Setup Instructions', link: '/setup/' }, + { text: 'Advanced Configuration', link: '/advanced-config/' }, + { text: 'Upgrading', link: '/upgrading/' }, + { text: 'Frequently Asked Questions', link: '/faq/' }, + { text: 'Third Party', link: '/third-party/' }, + ] + } + ], + socialLinks: [ + { icon: 'github', link: 'https://github.com/NginxProxyManager/nginx-proxy-manager' } + ], + search: { + provider: 'local' + }, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2016-present jc21.com' + } + } +}); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 0000000..2e3cda7 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,28 @@ +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #f15833 30%, #FAA42F); + + --vp-home-hero-image-background-image: linear-gradient(-45deg, #aaaaaa 50%, #777777 50%); + --vp-home-hero-image-filter: blur(44px); + + --vp-c-brand-1: #f15833; + --vp-c-brand-2: #FAA42F; + --vp-c-brand-3: #f15833; +} + + @media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + + @media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +.inline-img img { + display: inline; + margin-right: 8px; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..42fe9a9 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,4 @@ +import DefaultTheme from 'vitepress/theme' +import './custom.css' + +export default DefaultTheme diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..2155a55 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "dev": "vitepress dev --host", + "build": "vitepress build", + "preview": "vitepress preview", + "set-version": "./scripts/set-version.sh" + }, + "devDependencies": { + "vitepress": "^1.6.4" + }, + "dependencies": {} +} diff --git a/docs/scripts/set-version.sh b/docs/scripts/set-version.sh new file mode 100644 index 0000000..b7cd3d7 --- /dev/null +++ b/docs/scripts/set-version.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euf + +# this script accepts a version number as an argument +# and replaces {{VERSION}} in src/*.md with the provided version number. + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$DIR/.." || exit 1 + +VERSION="$1" +# find all .md files in src/ and replace {{VERSION}} with the provided version number +find src/ -type f -name "*.md" -exec sed -i "s/{{VERSION}}/$VERSION/g" {} \; diff --git a/docs/src/advanced-config/index.md b/docs/src/advanced-config/index.md new file mode 100644 index 0000000..3ab04ce --- /dev/null +++ b/docs/src/advanced-config/index.md @@ -0,0 +1,250 @@ +--- +outline: deep +--- + +# Advanced Configuration + +## Running processes as a user/group + +By default, the services (nginx etc) will run as `root` user inside the docker container. +You can change this behaviour by setting the following environment variables. +Not only will they run the services as this user/group, they will change the ownership +on the `data` and `letsencrypt` folders at startup. + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:{{VERSION}}' + environment: + PUID: 1000 + PGID: 1000 + # ... +``` + +This may have the side effect of a failed container start due to permission denied trying +to open port 80 on some systems. The only course to fix that is to remove the variables +and run as the default root user. + +## Best Practice: Use a Docker network + +For those who have a few of their upstream services running in Docker on the same Docker +host as NPM, here's a trick to secure things a bit better. By creating a custom Docker network, +you don't need to publish ports for your upstream services to all of the Docker host's interfaces. + +Create a network, ie "scoobydoo": + +```bash +docker network create scoobydoo +``` + +Then add the following to the `docker-compose.yml` file for both NPM and any other +services running on this Docker host: + +```yml +networks: + default: + external: true + name: scoobydoo +``` + +Let's look at a Portainer example: + +```yml +services: + + portainer: + image: portainer/portainer + privileged: true + volumes: + - './data:/data' + - '/var/run/docker.sock:/var/run/docker.sock' + restart: unless-stopped + +networks: + default: + external: true + name: scoobydoo +``` + +Now in the NPM UI you can create a proxy host with `portainer` as the hostname, +and port `9000` as the port. Even though this port isn't listed in the docker-compose +file, it's "exposed" by the Portainer Docker image for you and not available on +the Docker host outside of this Docker network. The service name is used as the +hostname, so make sure your service names are unique when using the same network. + +## Docker Healthcheck + +The `Dockerfile` that builds this project does not include a `HEALTHCHECK` but you can opt in to this +feature by adding the following to the service in your `docker-compose.yml` file: + +```yml +healthcheck: + test: ["CMD", "/usr/bin/check-health"] + interval: 10s + timeout: 3s +``` + +## Docker File Secrets + +This image supports the use of Docker secrets to import from files and keep sensitive usernames or passwords from being passed or preserved in plaintext. + +You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name. + +```yml +secrets: + # Secrets are single-line text files where the sole content is the secret + # Paths in this example assume that secrets are kept in local folder called ".secrets" + DB_ROOT_PWD: + file: .secrets/db_root_pwd.txt + MYSQL_PWD: + file: .secrets/mysql_pwd.txt + +services: + app: + image: 'jc21/nginx-proxy-manager:{{VERSION}}' + restart: unless-stopped + ports: + # Public HTTP Port: + - '80:80' + # Public HTTPS Port: + - '443:443' + # Admin Web Port: + - '81:81' + environment: + # These are the settings to access your db + DB_MYSQL_HOST: "db" + DB_MYSQL_PORT: 3306 + DB_MYSQL_USER: "npm" + # DB_MYSQL_PASSWORD: "npm" # use secret instead + DB_MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD + DB_MYSQL_NAME: "npm" + # If you would rather use Sqlite, remove all DB_MYSQL_* lines above + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + secrets: + - MYSQL_PWD + depends_on: + - db + + db: + image: 'linuxserver/mariadb' + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD + TZ: 'Australia/Brisbane' + volumes: + - ./mariadb:/config + secrets: + - DB_ROOT_PWD + - MYSQL_PWD +``` + + +## Disabling IPv6 + +On some Docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log: + +> Address family not supported by protocol + +The easy fix is to add a Docker environment variable to the Nginx Proxy Manager stack: + +```yml + environment: + DISABLE_IPV6: 'true' +``` + +## Disabling IP Ranges Fetch + +By default, NPM fetches IP ranges from CloudFront and Cloudflare during application startup. In environments with limited internet access or to speed up container startup, this fetch can be disabled: + +```yml + environment: + IP_RANGES_FETCH_ENABLED: 'false' +``` + +## Custom Nginx Configurations + +If you are a more advanced user, you might be itching for extra Nginx customizability. + +NPM has the ability to include different custom configuration snippets in different places. + +You can add your custom configuration snippet files at `/data/nginx/custom` as follow: + + - `/data/nginx/custom/root_top.conf`: Included at the top of nginx.conf + - `/data/nginx/custom/root.conf`: Included at the very end of nginx.conf + - `/data/nginx/custom/http_top.conf`: Included at the top of the main http block + - `/data/nginx/custom/http.conf`: Included at the end of the main http block + - `/data/nginx/custom/events.conf`: Included at the end of the events block + - `/data/nginx/custom/stream.conf`: Included at the end of the main stream block + - `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block + - `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block + - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block + - `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block + - `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block + - `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block + +Every file is optional. + + +## X-FRAME-OPTIONS Header + +You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header +value by specifying it as a Docker environment variable. The default if not specified is `deny`. + +```yml + ... + environment: + X_FRAME_OPTIONS: "sameorigin" + ... +``` + +## Customising logrotate settings + +By default, NPM rotates the access- and error logs weekly and keeps 4 and 10 log files respectively. +Depending on the usage, this can lead to large log files, especially access logs. +You can customise the logrotate configuration through a mount (if your custom config is `logrotate.custom`): + +```yml + volumes: + ... + - ./logrotate.custom:/etc/logrotate.d/nginx-proxy-manager +``` + +For reference, the default configuration can be found [here](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/rootfs/etc/logrotate.d/nginx-proxy-manager). + +## Enabling the geoip2 module + +To enable the geoip2 module, you can create the custom configuration file `/data/nginx/custom/root_top.conf` and include the following snippet: + +``` +load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so; +load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so; +``` + +## Auto Initial User Creation + +Setting these environment variables will create the default user on startup, skipping the UI first user setup screen: + +```yml + environment: + INITIAL_ADMIN_EMAIL: my@example.com + INITIAL_ADMIN_PASSWORD: mypassword1 +``` + +## Disable Nginx Resolver + +On startup, we generate a resolvers directive for Nginx unless this is defined: + +```yml + environment: + DISABLE_RESOLVER: true +``` + +In this configuration, all DNS queries performed by Nginx will fall to the `/etc/hosts` file +and then the `/etc/resolv.conf`. diff --git a/docs/src/faq/index.md b/docs/src/faq/index.md new file mode 100644 index 0000000..b366856 --- /dev/null +++ b/docs/src/faq/index.md @@ -0,0 +1,32 @@ +--- +outline: deep +--- + +# FAQ + +## Do I have to use Docker? + +Yes, that's how this project is packaged. + +This makes it easier to support the project when we have control over the version of Nginx other packages +use by the project. + +## Can I run it on a Raspberry Pi? + +Yes! The docker image is multi-arch and is built for a variety of architectures. If yours is +[not listed](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) please open a +[GitHub issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). + +## I can't get my service to proxy properly? + +Your best bet is to ask the [Reddit community for support](https://www.reddit.com/r/nginxproxymanager/). There's safety in numbers. + +## When adding username and password access control to a proxy host, I can no longer login into the app. + +Having an Access Control List (ACL) with username and password requires the browser to always send this username +and password in the `Authorization` header on each request. If your proxied app also requires authentication (like +Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information, +as this is the standardized header meant for this kind of information. However having multiples of the same headers +is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps +do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can +only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization. diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md new file mode 100644 index 0000000..6e2d1fe --- /dev/null +++ b/docs/src/guide/index.md @@ -0,0 +1,117 @@ +--- +outline: deep +--- + +# Guide + +::: raw +

+ + + + + + +

+::: + +This project comes as a pre-built docker image that enables you to easily forward to your websites +running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. + +- [Quick Setup](#quick-setup) +- [Full Setup](/setup/) +- [Screenshots](/screenshots/) + +## Project Goal + +I created this project to fill a personal need to provide users with an easy way to accomplish reverse +proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed. +While there might be advanced options they are optional and the project should be as simple as possible +so that the barrier for entry here is low. + +::: raw +Buy Me A Coffee +::: + +## Features + +- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.io/) +- Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx +- Free SSL using Let's Encrypt or provide your own custom SSL certificates +- Access Lists and basic HTTP Authentication for your hosts +- Advanced Nginx configuration available for super users +- User management, permissions and audit log + + +## Hosting your home network + +I won't go in to too much detail here but here are the basics for someone new to this self-hosted world. + +1. Your home router will have a Port Forwarding section somewhere. Log in and find it +2. Add port forwarding for port 80 and 443 to the server hosting this project +3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) +4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services + +## Quick Setup + +1. Install Docker and Docker-Compose + +- [Docker Install documentation](https://docs.docker.com/get-docker/) +- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) + +2. Create a docker-compose.yml file similar to this: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:{{VERSION}}' + restart: unless-stopped + environment: + TZ: "Australia/Brisbane" + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt +``` + +This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more. + +3. Bring up your stack by running + +```bash +docker compose up -d +``` + +4. Log in to the Admin UI + +When your docker container is running, connect to it on port `81` for the admin interface. + +[http://127.0.0.1:81](http://127.0.0.1:81) + +This startup can take a minute depending on your hardware. + + +## Contributing + +All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch. + +CI is used in this project. All PR's must pass before being considered. After passing, +docker builds for PR's are available on dockerhub for manual verifications. + +Documentation within the `develop` branch is available for preview at +[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com) + + +### Contributors + +Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors). + + +## Getting Support + +1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues) +2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions) +3. [Reddit](https://reddit.com/r/nginxproxymanager) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..74a263b --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,32 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Nginx Proxy Manager" + tagline: Expose your services easily and securely + image: + src: /logo.svg + alt: NPM Logo + actions: + - theme: brand + text: Get Started + link: /guide/ + - theme: alt + text: GitHub + link: https://github.com/NginxProxyManager/nginx-proxy-manager + +features: + - title: Get Connected + details: Expose web services on your network · Free SSL with Let's Encrypt · Designed with security in mind · Perfect for home networks + - title: Proxy Hosts + details: Expose your private network Web services and get connected anywhere. + - title: Beautiful UI + details: Based on Tabler, the interface is a pleasure to use. Configuring a server has never been so fun. + - title: Free SSL + details: Built in Let’s Encrypt support allows you to secure your Web services at no cost to you. The certificates even renew themselves! + - title: Docker FTW + details: Built as a Docker Image, Nginx Proxy Manager only requires a database. + - title: Multiple Users + details: Configure other users to either view or manage their own hosts. Full access permissions are available. +--- diff --git a/docs/src/public/github.png b/docs/src/public/github.png new file mode 100644 index 0000000..1480f03 Binary files /dev/null and b/docs/src/public/github.png differ diff --git a/docs/src/public/icon.png b/docs/src/public/icon.png new file mode 100644 index 0000000..7155df7 Binary files /dev/null and b/docs/src/public/icon.png differ diff --git a/docs/src/public/logo.svg b/docs/src/public/logo.svg new file mode 100644 index 0000000..23fa245 --- /dev/null +++ b/docs/src/public/logo.svg @@ -0,0 +1 @@ +logo \ No newline at end of file diff --git a/docs/src/public/robots.txt b/docs/src/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/docs/src/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/docs/src/public/screenshots/dark/01_first-user.png b/docs/src/public/screenshots/dark/01_first-user.png new file mode 100644 index 0000000..f585c85 Binary files /dev/null and b/docs/src/public/screenshots/dark/01_first-user.png differ diff --git a/docs/src/public/screenshots/dark/02_login.png b/docs/src/public/screenshots/dark/02_login.png new file mode 100644 index 0000000..0ce7fd4 Binary files /dev/null and b/docs/src/public/screenshots/dark/02_login.png differ diff --git a/docs/src/public/screenshots/dark/03_dashboard.png b/docs/src/public/screenshots/dark/03_dashboard.png new file mode 100644 index 0000000..22aedb3 Binary files /dev/null and b/docs/src/public/screenshots/dark/03_dashboard.png differ diff --git a/docs/src/public/screenshots/dark/04_proxy-hosts.png b/docs/src/public/screenshots/dark/04_proxy-hosts.png new file mode 100644 index 0000000..363c708 Binary files /dev/null and b/docs/src/public/screenshots/dark/04_proxy-hosts.png differ diff --git a/docs/src/public/screenshots/dark/05_redirection_hosts.png b/docs/src/public/screenshots/dark/05_redirection_hosts.png new file mode 100644 index 0000000..dd9eebb Binary files /dev/null and b/docs/src/public/screenshots/dark/05_redirection_hosts.png differ diff --git a/docs/src/public/screenshots/dark/06_streams.png b/docs/src/public/screenshots/dark/06_streams.png new file mode 100644 index 0000000..feb4fa4 Binary files /dev/null and b/docs/src/public/screenshots/dark/06_streams.png differ diff --git a/docs/src/public/screenshots/dark/07_404_hosts.png b/docs/src/public/screenshots/dark/07_404_hosts.png new file mode 100644 index 0000000..431c69d Binary files /dev/null and b/docs/src/public/screenshots/dark/07_404_hosts.png differ diff --git a/docs/src/public/screenshots/dark/08_access-lists.png b/docs/src/public/screenshots/dark/08_access-lists.png new file mode 100644 index 0000000..33a19bc Binary files /dev/null and b/docs/src/public/screenshots/dark/08_access-lists.png differ diff --git a/docs/src/public/screenshots/dark/09_certificates.png b/docs/src/public/screenshots/dark/09_certificates.png new file mode 100644 index 0000000..e870ae5 Binary files /dev/null and b/docs/src/public/screenshots/dark/09_certificates.png differ diff --git a/docs/src/public/screenshots/dark/10_users.png b/docs/src/public/screenshots/dark/10_users.png new file mode 100644 index 0000000..34ed0b5 Binary files /dev/null and b/docs/src/public/screenshots/dark/10_users.png differ diff --git a/docs/src/public/screenshots/dark/11_audit-logs.png b/docs/src/public/screenshots/dark/11_audit-logs.png new file mode 100644 index 0000000..bce75b1 Binary files /dev/null and b/docs/src/public/screenshots/dark/11_audit-logs.png differ diff --git a/docs/src/public/screenshots/dark/12_settings.png b/docs/src/public/screenshots/dark/12_settings.png new file mode 100644 index 0000000..adc518f Binary files /dev/null and b/docs/src/public/screenshots/dark/12_settings.png differ diff --git a/docs/src/public/screenshots/dark/13_add-proxy_host.png b/docs/src/public/screenshots/dark/13_add-proxy_host.png new file mode 100644 index 0000000..4b5522c Binary files /dev/null and b/docs/src/public/screenshots/dark/13_add-proxy_host.png differ diff --git a/docs/src/public/screenshots/dark/14_add_proxy_host_dns.png b/docs/src/public/screenshots/dark/14_add_proxy_host_dns.png new file mode 100644 index 0000000..199708d Binary files /dev/null and b/docs/src/public/screenshots/dark/14_add_proxy_host_dns.png differ diff --git a/docs/src/public/screenshots/light/01_first-user.png b/docs/src/public/screenshots/light/01_first-user.png new file mode 100644 index 0000000..c0a48d6 Binary files /dev/null and b/docs/src/public/screenshots/light/01_first-user.png differ diff --git a/docs/src/public/screenshots/light/02_login.png b/docs/src/public/screenshots/light/02_login.png new file mode 100644 index 0000000..86574aa Binary files /dev/null and b/docs/src/public/screenshots/light/02_login.png differ diff --git a/docs/src/public/screenshots/light/03_dashboard.png b/docs/src/public/screenshots/light/03_dashboard.png new file mode 100644 index 0000000..38057e8 Binary files /dev/null and b/docs/src/public/screenshots/light/03_dashboard.png differ diff --git a/docs/src/public/screenshots/light/04_proxy-hosts.png b/docs/src/public/screenshots/light/04_proxy-hosts.png new file mode 100644 index 0000000..e1a45d9 Binary files /dev/null and b/docs/src/public/screenshots/light/04_proxy-hosts.png differ diff --git a/docs/src/public/screenshots/light/05_redirection_hosts.png b/docs/src/public/screenshots/light/05_redirection_hosts.png new file mode 100644 index 0000000..e90a884 Binary files /dev/null and b/docs/src/public/screenshots/light/05_redirection_hosts.png differ diff --git a/docs/src/public/screenshots/light/06_streams.png b/docs/src/public/screenshots/light/06_streams.png new file mode 100644 index 0000000..79657aa Binary files /dev/null and b/docs/src/public/screenshots/light/06_streams.png differ diff --git a/docs/src/public/screenshots/light/07_404_hosts.png b/docs/src/public/screenshots/light/07_404_hosts.png new file mode 100644 index 0000000..cca39f1 Binary files /dev/null and b/docs/src/public/screenshots/light/07_404_hosts.png differ diff --git a/docs/src/public/screenshots/light/08_access-lists.png b/docs/src/public/screenshots/light/08_access-lists.png new file mode 100644 index 0000000..84ee910 Binary files /dev/null and b/docs/src/public/screenshots/light/08_access-lists.png differ diff --git a/docs/src/public/screenshots/light/09_certificates.png b/docs/src/public/screenshots/light/09_certificates.png new file mode 100644 index 0000000..0ed2426 Binary files /dev/null and b/docs/src/public/screenshots/light/09_certificates.png differ diff --git a/docs/src/public/screenshots/light/10_users.png b/docs/src/public/screenshots/light/10_users.png new file mode 100644 index 0000000..7e94980 Binary files /dev/null and b/docs/src/public/screenshots/light/10_users.png differ diff --git a/docs/src/public/screenshots/light/11_audit-logs.png b/docs/src/public/screenshots/light/11_audit-logs.png new file mode 100644 index 0000000..19de897 Binary files /dev/null and b/docs/src/public/screenshots/light/11_audit-logs.png differ diff --git a/docs/src/public/screenshots/light/12_settings.png b/docs/src/public/screenshots/light/12_settings.png new file mode 100644 index 0000000..2afa273 Binary files /dev/null and b/docs/src/public/screenshots/light/12_settings.png differ diff --git a/docs/src/public/screenshots/light/13_add-proxy_host.png b/docs/src/public/screenshots/light/13_add-proxy_host.png new file mode 100644 index 0000000..eeeb1fc Binary files /dev/null and b/docs/src/public/screenshots/light/13_add-proxy_host.png differ diff --git a/docs/src/public/screenshots/light/14_add_proxy_host_dns.png b/docs/src/public/screenshots/light/14_add_proxy_host_dns.png new file mode 100644 index 0000000..50b1088 Binary files /dev/null and b/docs/src/public/screenshots/light/14_add_proxy_host_dns.png differ diff --git a/docs/src/screenshots/index.md b/docs/src/screenshots/index.md new file mode 100644 index 0000000..2e15fef --- /dev/null +++ b/docs/src/screenshots/index.md @@ -0,0 +1,47 @@ +--- +outline: deep +--- + +# Screenshots + +### Light Mode + +::: raw +
+ Setup + Login + Dashboard + Proxy Hosts + Redirection Hosts + Streams + 404 Hosts + Access Lists + Certificates + Users + Audit Logs + Settings + Add Proxy Host + Add Proxy Host with DNS +
+::: + +### Dark Mode + +::: raw +
+ Setup + Login + Dashboard + Proxy Hosts + Redirection Hosts + Streams + 404 Hosts + Access Lists + Certificates + Users + Audit Logs + Settings + Add Proxy Host + Add Proxy Host with DNS +
+::: diff --git a/docs/src/setup/index.md b/docs/src/setup/index.md new file mode 100644 index 0000000..286a2e3 --- /dev/null +++ b/docs/src/setup/index.md @@ -0,0 +1,191 @@ +--- +outline: deep +--- + +# Full Setup Instructions + +## Running the App + +Create a `docker-compose.yml` file: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:{{VERSION}}' + restart: unless-stopped + + ports: + # These ports are in format : + - '80:80' # Public HTTP Port + - '443:443' # Public HTTPS Port + - '81:81' # Admin Web Port + # Add any other Stream port you want to expose + # - '21:21' # FTP + + environment: + TZ: "Australia/Brisbane" + + # Uncomment this if you want to change the location of + # the SQLite DB file within the container + # DB_SQLITE_FILE: "/data/database.sqlite" + + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt +``` + +Then: + +```bash +docker compose up -d +``` + +## Using MySQL / MariaDB Database + +If you opt for the MySQL configuration you will have to provide the database server yourself. + +It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples +are going to use. + +Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:{{VERSION}}' + restart: unless-stopped + ports: + # These ports are in format : + - '80:80' # Public HTTP Port + - '443:443' # Public HTTPS Port + - '81:81' # Admin Web Port + # Add any other Stream port you want to expose + # - '21:21' # FTP + environment: + TZ: "Australia/Brisbane" + # Mysql/Maria connection parameters: + DB_MYSQL_HOST: "db" + DB_MYSQL_PORT: 3306 + DB_MYSQL_USER: "npm" + DB_MYSQL_PASSWORD: "npm" + DB_MYSQL_NAME: "npm" + # Optional SSL (see section below) + # DB_MYSQL_SSL: 'true' + # DB_MYSQL_SSL_REJECT_UNAUTHORIZED: 'true' + # DB_MYSQL_SSL_VERIFY_IDENTITY: 'true' + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + depends_on: + - db + + db: + image: 'linuxserver/mariadb' + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: 'npm' + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD: 'npm' + TZ: 'Australia/Brisbane' + volumes: + - ./mariadb:/config +``` + +::: warning +Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use SQLite. +::: + +### Optional: MySQL / MariaDB SSL + +You can enable TLS for the MySQL/MariaDB connection with these environment variables: + +- `DB_MYSQL_SSL`: Enable SSL when set to true. If unset or false, SSL disabled (previous default behaviour). +- `DB_MYSQL_SSL_REJECT_UNAUTHORIZED`: (default: true) Validate the server certificate chain. Set to false to allow self‑signed/unknown CA. +- `DB_MYSQL_SSL_VERIFY_IDENTITY`: (default: true) Performs host name / identity verification. + +Enabling SSL using a self-signed cert (not recommended for production). + +## Using Postgres database + +Similar to the MySQL server setup: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:{{VERSION}}' + restart: unless-stopped + ports: + # These ports are in format : + - '80:80' # Public HTTP Port + - '443:443' # Public HTTPS Port + - '81:81' # Admin Web Port + # Add any other Stream port you want to expose + # - '21:21' # FTP + environment: + TZ: "Australia/Brisbane" + # Postgres parameters: + DB_POSTGRES_HOST: 'db' + DB_POSTGRES_PORT: '5432' + DB_POSTGRES_USER: 'npm' + DB_POSTGRES_PASSWORD: 'npmpass' + DB_POSTGRES_NAME: 'npm' + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + depends_on: + - db + + db: + image: postgres:17 + environment: + POSTGRES_USER: 'npm' + POSTGRES_PASSWORD: 'npmpass' + POSTGRES_DB: 'npm' + volumes: + - ./postgresql:/var/lib/postgresql +``` + +::: warning + +Custom Postgres schema is not supported, as such `public` will be used. + +::: + +## Running on Raspberry PI / ARM devices + +The docker images support the following architectures: +- amd64 +- arm64 + +::: warning +`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please +use the `2.13.7` image tag if this applies to you. +::: + +The docker images are a manifest of all the architecture docker builds supported, so this means +you don't have to worry about doing anything special and you can follow the common instructions above. + +Check out the [dockerhub tags](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) +for a list of supported architectures and if you want one that doesn't exist, +[create a feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). + +Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/) +on Raspbian. + +## Initial Run + +After the app is running for the first time, the following will happen: + +1. JWT keys will be generated and saved in the data folder +2. The database will initialize with table structures +3. A default admin user will be created + +This process can take a couple of minutes depending on your machine. diff --git a/docs/src/third-party/index.md b/docs/src/third-party/index.md new file mode 100644 index 0000000..cd54b45 --- /dev/null +++ b/docs/src/third-party/index.md @@ -0,0 +1,20 @@ +--- +outline: deep +--- + +# Third Party + +As this software gains popularity it's common to see it integrated with other platforms. Please be aware that unless specifically mentioned in the documentation of those +integrations, they are *not supported* by me. + +Known integrations: + +- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager) +- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager) +- [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) +- [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager) +- [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) + + +If you would like your integration of NPM listed, please open a +[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) diff --git a/docs/src/upgrading/index.md b/docs/src/upgrading/index.md new file mode 100644 index 0000000..21076d1 --- /dev/null +++ b/docs/src/upgrading/index.md @@ -0,0 +1,16 @@ +--- +outline: deep +--- + +# Upgrading + +```bash +docker compose pull +docker compose up -d +``` + +This project will automatically update any databases or other requirements so you don't have to follow +any crazy instructions. These steps above will pull the latest updates and recreate the docker +containers. + +See the [list of releases](https://github.com/NginxProxyManager/nginx-proxy-manager/releases) for any upgrade steps specific to each release. diff --git a/docs/yarn.lock b/docs/yarn.lock new file mode 100644 index 0000000..78a286a --- /dev/null +++ b/docs/yarn.lock @@ -0,0 +1,1232 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@algolia/abtesting@1.12.2": + version "1.12.2" + resolved "https://registry.yarnpkg.com/@algolia/abtesting/-/abtesting-1.12.2.tgz#1cba5e3c654d02c6d435822a0a0070a5c435daa6" + integrity sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/autocomplete-core@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz#2c410baa94a47c5c5f56ed712bb4a00ebe24088b" + integrity sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.17.7" + "@algolia/autocomplete-shared" "1.17.7" + +"@algolia/autocomplete-plugin-algolia-insights@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz#7d2b105f84e7dd8f0370aa4c4ab3b704e6760d82" + integrity sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A== + dependencies: + "@algolia/autocomplete-shared" "1.17.7" + +"@algolia/autocomplete-preset-algolia@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz#c9badc0d73d62db5bf565d839d94ec0034680ae9" + integrity sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA== + dependencies: + "@algolia/autocomplete-shared" "1.17.7" + +"@algolia/autocomplete-shared@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz#105e84ad9d1a31d3fb86ba20dc890eefe1a313a0" + integrity sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg== + +"@algolia/client-abtesting@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz#264a72f0e9d2fe0d0dc5c3d2d16bbb9cfe2ce9e8" + integrity sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/client-analytics@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.46.2.tgz#3f00a237508aa0c46c9c02dea9c855e0a78e241f" + integrity sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/client-common@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.46.2.tgz#7f282fd8f721b0d96958445df2170f4c7dce6aac" + integrity sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg== + +"@algolia/client-insights@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.46.2.tgz#194b7b529ee8a4ffd5d70037745082996c3b9aa0" + integrity sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/client-personalization@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.46.2.tgz#d604da7f0a3df1b3e2a9fe338d368e48fb781f8e" + integrity sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/client-query-suggestions@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz#f13bc5897bfbdc19509d430a26e9bbe2402e00c9" + integrity sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/client-search@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.46.2.tgz#771367916aaa3fb7a19d5944f8375504b0568ba6" + integrity sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/ingestion@1.46.2": + version "1.46.2" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.46.2.tgz#2a5d8a592d9f864dfb438722506382af56f8554f" + integrity sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/monitoring@1.46.2": + version "1.46.2" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.46.2.tgz#bd199368a49cb799cf12cfe76c49de6dd3021148" + integrity sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/recommend@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.46.2.tgz#e74bade1254046ed9be8ccd37f2a116ab9799508" + integrity sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q== + dependencies: + "@algolia/client-common" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +"@algolia/requester-browser-xhr@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz#7662480143405e815e1eed99136b4b2acd838ee7" + integrity sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg== + dependencies: + "@algolia/client-common" "5.46.2" + +"@algolia/requester-fetch@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz#dee07f0131b75f30d083bafd6fb878afe7402eb9" + integrity sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw== + dependencies: + "@algolia/client-common" "5.46.2" + +"@algolia/requester-node-http@5.46.2": + version "5.46.2" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz#7869d67cb2926bbdbfbfed2b4757e547c2e227eb" + integrity sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg== + dependencies: + "@algolia/client-common" "5.46.2" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/parser@^7.28.5": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + +"@babel/types@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@docsearch/css@3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.8.2.tgz#7973ceb6892c30f154ba254cd05c562257a44977" + integrity sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ== + +"@docsearch/js@3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.8.2.tgz#bdcfc9837700eb38453b88e211ab5cc5a3813cc6" + integrity sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ== + dependencies: + "@docsearch/react" "3.8.2" + preact "^10.0.0" + +"@docsearch/react@3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.8.2.tgz#7b11d39b61c976c0aa9fbde66e6b73b30f3acd42" + integrity sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg== + dependencies: + "@algolia/autocomplete-core" "1.17.7" + "@algolia/autocomplete-preset-algolia" "1.17.7" + "@docsearch/css" "3.8.2" + algoliasearch "^5.14.2" + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@iconify-json/simple-icons@^1.2.21": + version "1.2.66" + resolved "https://registry.yarnpkg.com/@iconify-json/simple-icons/-/simple-icons-1.2.66.tgz#d0576ca65f69616b49491129e17132316cf3f309" + integrity sha512-D1OnnXwiQXFkVMw5M+Bt8mPsXeMkQyGmMdrmN7lsQlKMUkfLOp6JWhnUJ92po51WXT046aF/zzqSmkKqg08p4Q== + dependencies: + "@iconify/types" "*" + +"@iconify/types@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@rollup/rollup-android-arm-eabi@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz#a6742c74c7d9d6d604ef8a48f99326b4ecda3d82" + integrity sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg== + +"@rollup/rollup-android-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz#97247be098de4df0c11971089fd2edf80a5da8cf" + integrity sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q== + +"@rollup/rollup-darwin-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz#674852cf14cf11b8056e0b1a2f4e872b523576cf" + integrity sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg== + +"@rollup/rollup-darwin-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz#36dfd7ed0aaf4d9d89d9ef983af72632455b0246" + integrity sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w== + +"@rollup/rollup-freebsd-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz#2f87c2074b4220260fdb52a9996246edfc633c22" + integrity sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA== + +"@rollup/rollup-freebsd-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz#9b5a26522a38a95dc06616d1939d4d9a76937803" + integrity sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg== + +"@rollup/rollup-linux-arm-gnueabihf@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz#86aa4859385a8734235b5e40a48e52d770758c3a" + integrity sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw== + +"@rollup/rollup-linux-arm-musleabihf@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz#cbe70e56e6ece8dac83eb773b624fc9e5a460976" + integrity sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA== + +"@rollup/rollup-linux-arm64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz#d14992a2e653bc3263d284bc6579b7a2890e1c45" + integrity sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA== + +"@rollup/rollup-linux-arm64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz#2fdd1ddc434ea90aeaa0851d2044789b4d07f6da" + integrity sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA== + +"@rollup/rollup-linux-loong64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz#8a181e6f89f969f21666a743cd411416c80099e7" + integrity sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg== + +"@rollup/rollup-linux-loong64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz#904125af2babc395f8061daa27b5af1f4e3f2f78" + integrity sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q== + +"@rollup/rollup-linux-ppc64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz#a57970ac6864c9a3447411a658224bdcf948be22" + integrity sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA== + +"@rollup/rollup-linux-ppc64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz#bb84de5b26870567a4267666e08891e80bb56a63" + integrity sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA== + +"@rollup/rollup-linux-riscv64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz#72d00d2c7fb375ce3564e759db33f17a35bffab9" + integrity sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg== + +"@rollup/rollup-linux-riscv64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz#4c166ef58e718f9245bd31873384ba15a5c1a883" + integrity sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg== + +"@rollup/rollup-linux-s390x-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz#bb5025cde9a61db478c2ca7215808ad3bce73a09" + integrity sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w== + +"@rollup/rollup-linux-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz#9b66b1f9cd95c6624c788f021c756269ffed1552" + integrity sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg== + +"@rollup/rollup-linux-x64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz#b007ca255dc7166017d57d7d2451963f0bd23fd9" + integrity sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg== + +"@rollup/rollup-openbsd-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz#e8b357b2d1aa2c8d76a98f5f0d889eabe93f4ef9" + integrity sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ== + +"@rollup/rollup-openharmony-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz#96c2e3f4aacd3d921981329831ff8dde492204dc" + integrity sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA== + +"@rollup/rollup-win32-arm64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz#2d865149d706d938df8b4b8f117e69a77646d581" + integrity sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A== + +"@rollup/rollup-win32-ia32-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz#abe1593be0fa92325e9971c8da429c5e05b92c36" + integrity sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA== + +"@rollup/rollup-win32-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz#c4af3e9518c9a5cd4b1c163dc81d0ad4d82e7eab" + integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA== + +"@rollup/rollup-win32-x64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c" + integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== + +"@shikijs/core@2.5.0", "@shikijs/core@^2.1.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-2.5.0.tgz#e14d33961dfa3141393d4a76fc8923d0d1c4b62f" + integrity sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg== + dependencies: + "@shikijs/engine-javascript" "2.5.0" + "@shikijs/engine-oniguruma" "2.5.0" + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.4" + +"@shikijs/engine-javascript@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz#e045c6ecfbda6c99137547b0a482e0b87f1053fc" + integrity sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w== + dependencies: + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + oniguruma-to-es "^3.1.0" + +"@shikijs/engine-oniguruma@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz#230de5693cc1da6c9d59c7ad83593c2027274817" + integrity sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw== + dependencies: + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + +"@shikijs/langs@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-2.5.0.tgz#97ab50c495922cc1ca06e192985b28dc73de5d50" + integrity sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w== + dependencies: + "@shikijs/types" "2.5.0" + +"@shikijs/themes@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-2.5.0.tgz#8c6aecf73f5455681c8bec15797cf678162896cb" + integrity sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw== + dependencies: + "@shikijs/types" "2.5.0" + +"@shikijs/transformers@^2.1.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-2.5.0.tgz#190c84786ff06c417580ab79177338a947168c55" + integrity sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg== + dependencies: + "@shikijs/core" "2.5.0" + "@shikijs/types" "2.5.0" + +"@shikijs/types@2.5.0", "@shikijs/types@^2.1.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-2.5.0.tgz#e949c7384802703a48b9d6425dd41673c164df69" + integrity sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== + +"@types/estree@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/hast@^3.0.0", "@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + +"@types/markdown-it@^14.1.2": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/web-bluetooth@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63" + integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA== + +"@ungap/structured-clone@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vitejs/plugin-vue@^5.2.1": + version "5.2.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8" + integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA== + +"@vue/compiler-core@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz#1a91ea90980528bedff7b1c292690bfb30612485" + integrity sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w== + dependencies: + "@babel/parser" "^7.28.5" + "@vue/shared" "3.5.26" + entities "^7.0.0" + estree-walker "^2.0.2" + source-map-js "^1.2.1" + +"@vue/compiler-dom@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz#66c36b6ed8bdf43236d7188ea332bc9d078eb286" + integrity sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A== + dependencies: + "@vue/compiler-core" "3.5.26" + "@vue/shared" "3.5.26" + +"@vue/compiler-sfc@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz#fb1c6c4bf9a9e22bb169e039e19437cb6995917a" + integrity sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA== + dependencies: + "@babel/parser" "^7.28.5" + "@vue/compiler-core" "3.5.26" + "@vue/compiler-dom" "3.5.26" + "@vue/compiler-ssr" "3.5.26" + "@vue/shared" "3.5.26" + estree-walker "^2.0.2" + magic-string "^0.30.21" + postcss "^8.5.6" + source-map-js "^1.2.1" + +"@vue/compiler-ssr@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz#f6e94bccbb5339180779036ddfb614f998a197ea" + integrity sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw== + dependencies: + "@vue/compiler-dom" "3.5.26" + "@vue/shared" "3.5.26" + +"@vue/devtools-api@^7.7.0": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063" + integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g== + dependencies: + "@vue/devtools-kit" "^7.7.9" + +"@vue/devtools-kit@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58" + integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA== + dependencies: + "@vue/devtools-shared" "^7.7.9" + birpc "^2.3.0" + hookable "^5.5.3" + mitt "^3.0.1" + perfect-debounce "^1.0.0" + speakingurl "^14.0.1" + superjson "^2.2.2" + +"@vue/devtools-shared@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14" + integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA== + dependencies: + rfdc "^1.4.1" + +"@vue/reactivity@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.26.tgz#59a1edf566dc80133c1c26c93711c877e8602c48" + integrity sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ== + dependencies: + "@vue/shared" "3.5.26" + +"@vue/runtime-core@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz#3f2c040bcf8018c03a1ab5adb0d788c13c986f0e" + integrity sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q== + dependencies: + "@vue/reactivity" "3.5.26" + "@vue/shared" "3.5.26" + +"@vue/runtime-dom@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz#5954848614883948ecc1f631a67b32cc32f81936" + integrity sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ== + dependencies: + "@vue/reactivity" "3.5.26" + "@vue/runtime-core" "3.5.26" + "@vue/shared" "3.5.26" + csstype "^3.2.3" + +"@vue/server-renderer@3.5.26": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz#269055497fcc75b3984063f866f17c748b565ef4" + integrity sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA== + dependencies: + "@vue/compiler-ssr" "3.5.26" + "@vue/shared" "3.5.26" + +"@vue/shared@3.5.26", "@vue/shared@^3.5.13": + version "3.5.26" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f" + integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A== + +"@vueuse/core@12.8.2", "@vueuse/core@^12.4.0": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-12.8.2.tgz#007c6dd29a7d1f6933e916e7a2f8ef3c3f968eaa" + integrity sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ== + dependencies: + "@types/web-bluetooth" "^0.0.21" + "@vueuse/metadata" "12.8.2" + "@vueuse/shared" "12.8.2" + vue "^3.5.13" + +"@vueuse/integrations@^12.4.0": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-12.8.2.tgz#d04f33d86fe985c9a27c98addcfde9f30f2db1df" + integrity sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g== + dependencies: + "@vueuse/core" "12.8.2" + "@vueuse/shared" "12.8.2" + vue "^3.5.13" + +"@vueuse/metadata@12.8.2": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3" + integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A== + +"@vueuse/shared@12.8.2": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930" + integrity sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w== + dependencies: + vue "^3.5.13" + +algoliasearch@^5.14.2: + version "5.46.2" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.46.2.tgz#3afba0e53f3293e39cfde9b2ef27c583d44bf2a5" + integrity sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q== + dependencies: + "@algolia/abtesting" "1.12.2" + "@algolia/client-abtesting" "5.46.2" + "@algolia/client-analytics" "5.46.2" + "@algolia/client-common" "5.46.2" + "@algolia/client-insights" "5.46.2" + "@algolia/client-personalization" "5.46.2" + "@algolia/client-query-suggestions" "5.46.2" + "@algolia/client-search" "5.46.2" + "@algolia/ingestion" "1.46.2" + "@algolia/monitoring" "1.46.2" + "@algolia/recommend" "5.46.2" + "@algolia/requester-browser-xhr" "5.46.2" + "@algolia/requester-fetch" "5.46.2" + "@algolia/requester-node-http" "5.46.2" + +birpc@^2.3.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145" + integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +copy-anything@^4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea" + integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA== + dependencies: + is-what "^5.2.0" + +csstype@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +emoji-regex-xs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz#e8af22e5d9dbd7f7f22d280af3d19d2aab5b0724" + integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== + +entities@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a" + integrity sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +focus-trap@^7.6.4: + version "7.8.0" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.8.0.tgz#b1d9463fa42b93ad7a5223d750493a6c09b672a8" + integrity sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA== + dependencies: + tabbable "^6.4.0" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +hast-util-to-html@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005" + integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hookable@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" + integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +is-what@^5.2.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4" + integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw== + +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +mark.js@8.11.1: + version "8.11.1" + resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" + integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== + +mdast-util-to-hast@^13.0.0: + version "13.2.1" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz#d7ff84ca499a57e2c060ae67548ad950e689a053" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +micromark-util-character@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + +minisearch@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.2.0.tgz#3dc30e41e9464b3836553b6d969b656614f8f359" + integrity sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== + +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +oniguruma-to-es@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz#480e4bac4d3bc9439ac0d2124f0725e7a0d76d17" + integrity sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ== + dependencies: + emoji-regex-xs "^1.0.0" + regex "^6.0.1" + regex-recursion "^6.0.2" + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +postcss@^8.4.43: + version "8.4.47" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" + +postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +preact@^10.0.0: + version "10.21.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.21.0.tgz#5b0335c873a1724deb66e517830db4fd310c24f6" + integrity sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg== + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + +regex-recursion@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33" + integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== + dependencies: + regex-utilities "^2.3.0" + +regex-utilities@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280" + integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + +regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/regex/-/regex-6.1.0.tgz#d7ce98f8ee32da7497c13f6601fca2bc4a6a7803" + integrity sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== + dependencies: + regex-utilities "^2.3.0" + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rollup@^4.20.0: + version "4.59.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f" + integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.59.0" + "@rollup/rollup-android-arm64" "4.59.0" + "@rollup/rollup-darwin-arm64" "4.59.0" + "@rollup/rollup-darwin-x64" "4.59.0" + "@rollup/rollup-freebsd-arm64" "4.59.0" + "@rollup/rollup-freebsd-x64" "4.59.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" + "@rollup/rollup-linux-arm-musleabihf" "4.59.0" + "@rollup/rollup-linux-arm64-gnu" "4.59.0" + "@rollup/rollup-linux-arm64-musl" "4.59.0" + "@rollup/rollup-linux-loong64-gnu" "4.59.0" + "@rollup/rollup-linux-loong64-musl" "4.59.0" + "@rollup/rollup-linux-ppc64-gnu" "4.59.0" + "@rollup/rollup-linux-ppc64-musl" "4.59.0" + "@rollup/rollup-linux-riscv64-gnu" "4.59.0" + "@rollup/rollup-linux-riscv64-musl" "4.59.0" + "@rollup/rollup-linux-s390x-gnu" "4.59.0" + "@rollup/rollup-linux-x64-gnu" "4.59.0" + "@rollup/rollup-linux-x64-musl" "4.59.0" + "@rollup/rollup-openbsd-x64" "4.59.0" + "@rollup/rollup-openharmony-arm64" "4.59.0" + "@rollup/rollup-win32-arm64-msvc" "4.59.0" + "@rollup/rollup-win32-ia32-msvc" "4.59.0" + "@rollup/rollup-win32-x64-gnu" "4.59.0" + "@rollup/rollup-win32-x64-msvc" "4.59.0" + fsevents "~2.3.2" + +shiki@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-2.5.0.tgz#09d01ebf3b0b06580431ce3ddc023320442cf223" + integrity sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ== + dependencies: + "@shikijs/core" "2.5.0" + "@shikijs/engine-javascript" "2.5.0" + "@shikijs/engine-oniguruma" "2.5.0" + "@shikijs/langs" "2.5.0" + "@shikijs/themes" "2.5.0" + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +superjson@^2.2.2: + version "2.2.6" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099" + integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA== + dependencies: + copy-anything "^4" + +tabbable@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581" + integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +vite@^5.4.14: + version "5.4.21" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027" + integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vitepress@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.6.4.tgz#1b6c68fede541a3f401a66263dce0c985e2d8d92" + integrity sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg== + dependencies: + "@docsearch/css" "3.8.2" + "@docsearch/js" "3.8.2" + "@iconify-json/simple-icons" "^1.2.21" + "@shikijs/core" "^2.1.0" + "@shikijs/transformers" "^2.1.0" + "@shikijs/types" "^2.1.0" + "@types/markdown-it" "^14.1.2" + "@vitejs/plugin-vue" "^5.2.1" + "@vue/devtools-api" "^7.7.0" + "@vue/shared" "^3.5.13" + "@vueuse/core" "^12.4.0" + "@vueuse/integrations" "^12.4.0" + focus-trap "^7.6.4" + mark.js "8.11.1" + minisearch "^7.1.1" + shiki "^2.1.0" + vite "^5.4.14" + vue "^3.5.13" + +vue@^3.5.13: + version "3.5.26" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.26.tgz#03a0b17311e0e593d34b9358fa249b85e3a6d9fb" + integrity sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA== + dependencies: + "@vue/compiler-dom" "3.5.26" + "@vue/compiler-sfc" "3.5.26" + "@vue/runtime-dom" "3.5.26" + "@vue/server-renderer" "3.5.26" + "@vue/shared" "3.5.26" + +zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a9b91bc --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +src/locale/lang + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000..ccc3cd3 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "!**/dist/**/*" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 4, + "lineWidth": 120, + "formatWithErrors": true + }, + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "groups": [ + ":BUN:", + ":NODE:", + [ + "npm:*", + "npm:*/**" + ], + ":PACKAGE_WITH_PROTOCOL:", + ":URL:", + ":PACKAGE:", + [ + "/src/*", + "/src/**" + ], + [ + "/**" + ], + [ + "#*", + "#*/**" + ], + ":PATH:" + ] + } + } + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "useUniqueElementIds": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noArrayIndexKey": "off" + }, + "performance": { + "noDelete": "off" + }, + "nursery": "off", + "a11y": { + "useSemanticElements": "off", + "useValidAnchor": "off" + }, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + } +} diff --git a/frontend/check-locales.cjs b/frontend/check-locales.cjs new file mode 100644 index 0000000..75d2d8f --- /dev/null +++ b/frontend/check-locales.cjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +// This file does a few things to ensure that the Locales are present and valid: +// - Ensures that the name of the locale exists in the language list +// - Ensures that each locale contains the translations used in the application +// - Ensures that there are no unused translations in the locale files +// - Also checks the error messages returned by the backend + +const allLocales = [ + ["en", "en-US"], + ["de", "de-DE"], + ["pt", "pt-PT"], + ["es", "es-ES"], + ["et", "et-EE"], + ["fr", "fr-FR"], + ["it", "it-IT"], + ["ja", "ja-JP"], + ["nl", "nl-NL"], + ["pl", "pl-PL"], + ["ru", "ru-RU"], + ["sk", "sk-SK"], + ["cs", "cs-CZ"], + ["vi", "vi-VN"], + ["zh", "zh-CN"], + ["ko", "ko-KR"], + ["bg", "bg-BG"], + ["id", "id-ID"], + ["tr", "tr-TR"], + ["hu", "hu-HU"], + ["no", "no-NO"], +]; + +const ignoreUnused = [/^.*$/]; + +const { spawnSync } = require("child_process"); +const fs = require("fs"); + +const tmp = require("tmp"); + +// Parse backend errors +const BACKEND_ERRORS_FILE = "../backend/internal/errors/errors.go"; +const BACKEND_ERRORS = []; +/* +try { + const backendErrorsContent = fs.readFileSync(BACKEND_ERRORS_FILE, "utf8"); + const backendErrorsContentRes = [ + ...backendErrorsContent.matchAll(/(?:errors|eris)\.New\("([^"]+)"\)/g), + ]; + backendErrorsContentRes.map((item) => { + BACKEND_ERRORS.push("error." + item[1]); + return null; + }); +} catch (err) { + console.log("\x1b[31m%s\x1b[0m", err); + process.exit(1); +} +*/ + +// get all translations used in frontend code +const tmpobj = tmp.fileSync({ postfix: ".json" }); +spawnSync("yarn", ["locale-extract", "--out-file", tmpobj.name]); + +const allLocalesInProject = require(tmpobj.name); + +// get list og language names and locales +const langList = require("./src/locale/src/lang-list.json"); + +// store a list of all validation errors +const allErrors = []; +const allWarnings = []; +const allKeys = []; + +const checkLangList = (fullCode) => { + const key = "locale-" + fullCode; + if (typeof langList[key] === "undefined") { + allErrors.push("ERROR: `" + key + "` language does not exist in lang-list.json"); + } +}; + +const compareLocale = (locale) => { + const projectLocaleKeys = Object.keys(allLocalesInProject); + // Check that locale contains the items used in the codebase + projectLocaleKeys.map((key) => { + if (typeof locale.data[key] === "undefined") { + allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`"); + } + return null; + }); + // Check that locale contains all error.* items + BACKEND_ERRORS.forEach((key) => { + if (typeof locale.data[key] === "undefined") { + allErrors.push("ERROR: `" + locale[0] + "` does not contain item: `" + key + "`"); + } + return null; + }); + + // Check that locale does not contain items not used in the codebase + const localeKeys = Object.keys(locale.data); + localeKeys.map((key) => { + let ignored = false; + ignoreUnused.map((regex) => { + if (key.match(regex)) { + ignored = true; + } + return null; + }); + + if (!ignored && typeof allLocalesInProject[key] === "undefined") { + // ensure this key doesn't exist in the backend errors either + if (!BACKEND_ERRORS.includes(key)) { + allErrors.push("ERROR: `" + locale[0] + "` contains unused item: `" + key + "`"); + } + } + + // Add this key to allKeys + if (allKeys.indexOf(key) === -1) { + allKeys.push(key); + } + return null; + }); +}; + +// Checks for any keys missing from this locale, that +// have been defined in any other locales +const checkForMissing = (locale) => { + allKeys.forEach((key) => { + if (typeof locale.data[key] === "undefined") { + allWarnings.push("WARN: `" + locale[0] + "` does not contain item: `" + key + "`"); + } + return null; + }); +}; + +// Local all locale data +allLocales.map((locale, idx) => { + checkLangList(locale[1]); + allLocales[idx].data = require("./src/locale/src/" + locale[0] + ".json"); + return null; +}); + +// Verify all locale data +allLocales.map((locale) => { + compareLocale(locale); + checkForMissing(locale); + return null; +}); + +if (allErrors.length) { + allErrors.map((err) => { + console.log("\x1b[31m%s\x1b[0m", err); + return null; + }); +} +if (allWarnings.length) { + allWarnings.map((err) => { + console.log("\x1b[33m%s\x1b[0m", err); + return null; + }); +} + +if (allErrors.length) { + process.exit(1); +} + +console.log("\x1b[32m%s\x1b[0m", "Locale check passed"); +process.exit(0); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..08c7609 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,45 @@ + + + + + + Nginx Proxy Manager + + + + + + + + + + + + + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..a488939 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6240 @@ +{ + "name": "nginx-proxy-manager", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nginx-proxy-manager", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@tabler/core": "^1.4.0", + "@tabler/icons-react": "^3.38.0", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-table": "^8.21.3", + "@uiw/react-textarea-code-editor": "^3.1.1", + "classnames": "^2.5.1", + "country-flag-icons": "^1.6.15", + "date-fns": "^4.1.0", + "ez-modal-react": "^1.0.5", + "formik": "^2.4.9", + "generate-password-browser": "^1.1.0", + "humps": "^2.0.1", + "query-string": "^9.3.1", + "react": "^19.2.4", + "react-bootstrap": "^2.10.10", + "react-dom": "^19.2.4", + "react-intl": "^8.1.3", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", + "react-select": "^5.10.2", + "react-toastify": "^11.0.5", + "rooks": "^9.5.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.5", + "@formatjs/cli": "^6.13.0", + "@tanstack/react-query-devtools": "^5.91.3", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/country-flag-icons": "^1.2.2", + "@types/humps": "^2.0.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/react-table": "^7.7.20", + "@vitejs/plugin-react": "^5.1.4", + "happy-dom": "^20.8.3", + "postcss": "^8.5.8", + "postcss-simple-vars": "^7.0.1", + "sass": "^1.97.3", + "tmp": "^0.2.5", + "typescript": "5.9.3", + "vite": "^7.3.1", + "vite-plugin-checker": "^0.12.0", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.0.18" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.5.tgz", + "integrity": "sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.5", + "@biomejs/cli-darwin-x64": "2.4.5", + "@biomejs/cli-linux-arm64": "2.4.5", + "@biomejs/cli-linux-arm64-musl": "2.4.5", + "@biomejs/cli-linux-x64": "2.4.5", + "@biomejs/cli-linux-x64-musl": "2.4.5", + "@biomejs/cli-win32-arm64": "2.4.5", + "@biomejs/cli-win32-x64": "2.4.5" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.5.tgz", + "integrity": "sha512-lGS4Nd5O3KQJ6TeWv10mElnx1phERhBxqGP/IKq0SvZl78kcWDFMaTtVK+w3v3lusRFxJY78n07PbKplirsU5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.5.tgz", + "integrity": "sha512-6MoH4tyISIBNkZ2Q5T1R7dLd5BsITb2yhhhrU9jHZxnNSNMWl+s2Mxu7NBF8Y3a7JJcqq9nsk8i637z4gqkJxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.5.tgz", + "integrity": "sha512-U1GAG6FTjhAO04MyH4xn23wRNBkT6H7NentHh+8UxD6ShXKBm5SY4RedKJzkUThANxb9rUKIPc7B8ew9Xo/cWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.5.tgz", + "integrity": "sha512-iqLDgpzobG7gpBF0fwEVS/LT8kmN7+S0E2YKFDtqliJfzNLnAiV2Nnyb+ehCDCJgAZBASkYHR2o60VQWikpqIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.5.tgz", + "integrity": "sha512-NdODlSugMzTlENPTa4z0xB82dTUlCpsrOxc43///aNkTLblIYH4XpYflBbf5ySlQuP8AA4AZd1qXhV07IdrHdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.5.tgz", + "integrity": "sha512-NlKa7GpbQmNhZf9kakQeddqZyT7itN7jjWdakELeXyTU3pg/83fTysRRDPJD0akTfKDl6vZYNT9Zqn4MYZVBOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.5.tgz", + "integrity": "sha512-EBfrTqRIWOFSd7CQb/0ttjHMR88zm3hGravnDwUA9wHAaCAYsULKDebWcN5RmrEo1KBtl/gDVJMrFjNR0pdGUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.5.tgz", + "integrity": "sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@formatjs/cli": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.13.0.tgz", + "integrity": "sha512-bl4+FNg7S6RPNa9cSAE8HqdXu84n7LpzDdkDAPqS0sk58XNbY/1Le6GdWqCKzELWX+FhI58gyZtZecmWsZ+Bhg==", + "dev": true, + "license": "MIT", + "bin": { + "formatjs": "bin/formatjs" + }, + "engines": { + "node": ">= 20.12.0" + }, + "peerDependencies": { + "@glimmer/syntax": "^0.84.3 || ^0.95.0", + "@vue/compiler-core": "3.5.27", + "content-tag": "^4.1.0", + "vue": "3.5.27" + }, + "peerDependenciesMeta": { + "@glimmer/env": { + "optional": true + }, + "@glimmer/reference": { + "optional": true + }, + "@glimmer/syntax": { + "optional": true + }, + "@glimmer/validator": { + "optional": true + }, + "@vue/compiler-core": { + "optional": true + }, + "content-tag": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz", + "integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/intl-localematcher": "0.8.1", + "decimal.js": "^10.6.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", + "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz", + "integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/icu-skeleton-parser": "2.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz", + "integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/intl": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-4.1.2.tgz", + "integrity": "sha512-V60fNY/X/7zqmRffr7qPwscGmVGYDmlKF069mSQ2a/7fE22q602NtIfOQY8vzRA63Gr/O/U6vjRVBHMabrnA9A==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/icu-messageformat-parser": "3.5.1", + "intl-messageformat": "11.1.2", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "typescript": "^5.6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", + "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tabler/core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.4.0.tgz", + "integrity": "sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.11.8", + "bootstrap": "5.3.7" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons": { + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.38.0.tgz", + "integrity": "sha512-FdETQSpQ3lN7BEjEUzjKhsfTDCamrvMDops4HEMphTm3DmkIFpThoODn8XXZ8Q9MhjshIvphIYVHHB7zpq167w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.38.0.tgz", + "integrity": "sha512-kR5wv+m4+GgmnSszg3rQd6SrTFAQ/XnQC/yTwIfuRJSfqB12KoIC7fPbIijFgOHTFlBN5DARnN0IVrR7KYG6/A==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.38.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz", + "integrity": "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.91.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz", + "integrity": "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.93.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.20", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/country-flag-icons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/country-flag-icons/-/country-flag-icons-1.2.2.tgz", + "integrity": "sha512-CefEn/J336TBDp7NX8JqzlDtCBOsm8M3r1Li0gEOt0HOMHF1XemNyrx9lSHjsafcb1yYWybU0N8ZAXuyCaND0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/humps": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/humps/-/humps-2.0.6.tgz", + "integrity": "sha512-Fagm1/a/1J9gDKzGdtlPmmTN5eSw/aaTzHtj740oSfo+MODsSY2WglxMmhTdOglC8nxqUhGGQ+5HfVtBvxo3Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-table": { + "version": "7.7.20", + "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.20.tgz", + "integrity": "sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@uiw/react-textarea-code-editor": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@uiw/react-textarea-code-editor/-/react-textarea-code-editor-3.1.1.tgz", + "integrity": "sha512-AERRbp/d85vWR+UPgsB5hEgerNXuyszdmhWl2fV2H2jN63jgOobwEnjIpb76Vwy8SaGa/AdehaoJX2XZgNXtJA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "rehype": "~13.0.0", + "rehype-prism-plus": "2.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.10.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bootstrap": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", + "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/country-flag-icons": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.15.tgz", + "integrity": "sha512-92HoA8l6DluEidku8tKBftjuFRj4Rv3zDW1lXxCuNnqAxhUSkvso9gM/Afj4F5BnK+wneHIe3ydI+s+4NA29/Q==", + "license": "MIT" + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/ez-modal-react": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ez-modal-react/-/ez-modal-react-1.0.5.tgz", + "integrity": "sha512-/A8yLK54tpmWCMkW8Pwqc2xxspmimGOOw/m+1Y+tNtUIheuDHhLynHP1Q0utciJEGDAK849aQcd+6DrJ88hggQ==", + "license": "MIT", + "peerDependencies": { + "react": ">16.8.0", + "react-dom": ">16.8.0", + "typescript": ">4.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/formik": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz", + "integrity": "sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-password-browser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/generate-password-browser/-/generate-password-browser-1.1.0.tgz", + "integrity": "sha512-qsQve0rVbCqGqAfKgZwjxKUfI1d1nyd22dz+kE8gn1iw1LxGkR+Slsl79XXfm2wxuK27IkopTs5KXcOEQnhg0w==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "randombytes": "^2.0.5" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/happy-dom": { + "version": "20.8.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.3.tgz", + "integrity": "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/intl-messageformat": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz", + "integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/icu-messageformat-parser": "3.5.1", + "tslib": "^2.8.1" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/query-string": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz", + "integrity": "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" + }, + "node_modules/react-intl": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz", + "integrity": "sha512-eL1/d+uQdnapirynOGAriW0K9uAoyarjRGL3V9LaTRuohNSvPgCfJX06EZl5M52h/Hu7Gz7A1sD7dNHcos1lNg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/icu-messageformat-parser": "3.5.1", + "@formatjs/intl": "4.1.2", + "@types/hoist-non-react-statics": "^3.3.1", + "hoist-non-react-statics": "^3.3.2", + "intl-messageformat": "11.1.2", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@types/react": "19", + "react": "19", + "typescript": "^5.6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/refractor": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.9.0.tgz", + "integrity": "sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/refractor/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/refractor/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-prism-plus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz", + "integrity": "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==", + "license": "MIT", + "dependencies": { + "hast-util-to-string": "^3.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.8.0", + "rehype-parse": "^9.0.0", + "unist-util-filter": "^5.0.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rooks": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rooks/-/rooks-9.5.0.tgz", + "integrity": "sha512-AtmaX8yjQkJAW7EXW+UU481bpGwuk455hjD/aEUuy7N7VjvXlNmO8BErQ+jEUQp1DRA/PTWonv+Dq1nEkJdgkw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash.debounce": "^4.0.8", + "raf": "^3.4.1", + "use-sync-external-store": "^1.4.0" + }, + "engines": { + "node": ">=v10.24.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "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/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-filter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", + "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.12.0.tgz", + "integrity": "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "chokidar": "^4.0.3", + "npm-run-path": "^6.0.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.3", + "tiny-invariant": "^1.3.3", + "tinyglobby": "^0.2.15", + "vscode-uri": "^3.1.0" + }, + "engines": { + "node": ">=16.11" + }, + "peerDependencies": { + "@biomejs/biome": ">=1.7", + "eslint": ">=9.39.1", + "meow": "^13.2.0", + "optionator": "^0.9.4", + "oxlint": ">=1", + "stylelint": ">=16", + "typescript": "*", + "vite": ">=5.4.21", + "vls": "*", + "vti": "*", + "vue-tsc": "~2.2.10 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "oxlint": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", + "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "extraneous": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4223c67 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,66 @@ +{ + "name": "nginx-proxy-manager", + "version": "2.0.0", + "type": "module", + "author": "Jamie Curnow ", + "license": "MIT", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "biome lint", + "preview": "vite preview", + "prettier": "biome format --write ./src", + "locale-extract": "formatjs extract 'src/**/*.tsx'", + "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang", + "locale-sort": "./src/locale/scripts/locale-sort.sh", + "test": "vitest" + }, + "dependencies": { + "@tabler/core": "^1.4.0", + "@tabler/icons-react": "^3.38.0", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-table": "^8.21.3", + "@uiw/react-textarea-code-editor": "^3.1.1", + "classnames": "^2.5.1", + "country-flag-icons": "^1.6.15", + "date-fns": "^4.1.0", + "ez-modal-react": "^1.0.5", + "formik": "^2.4.9", + "generate-password-browser": "^1.1.0", + "humps": "^2.0.1", + "query-string": "^9.3.1", + "react": "^19.2.4", + "react-bootstrap": "^2.10.10", + "react-dom": "^19.2.4", + "react-intl": "^8.1.3", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", + "react-select": "^5.10.2", + "react-toastify": "^11.0.5", + "rooks": "^9.5.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.5", + "@formatjs/cli": "^6.13.0", + "@tanstack/react-query-devtools": "^5.91.3", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/country-flag-icons": "^1.2.2", + "@types/humps": "^2.0.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/react-table": "^7.7.20", + "@vitejs/plugin-react": "^5.1.4", + "happy-dom": "^20.8.3", + "postcss": "^8.5.8", + "postcss-simple-vars": "^7.0.1", + "sass": "^1.97.3", + "tmp": "^0.2.5", + "typescript": "5.9.3", + "vite": "^7.3.1", + "vite-plugin-checker": "^0.12.0", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.0.18" + } +} diff --git a/frontend/public/images/default-avatar.jpg b/frontend/public/images/default-avatar.jpg new file mode 100644 index 0000000..1a0e507 Binary files /dev/null and b/frontend/public/images/default-avatar.jpg differ diff --git a/frontend/public/images/favicon/android-chrome-192x192.png b/frontend/public/images/favicon/android-chrome-192x192.png new file mode 100644 index 0000000..7155df7 Binary files /dev/null and b/frontend/public/images/favicon/android-chrome-192x192.png differ diff --git a/frontend/public/images/favicon/android-chrome-512x512.png b/frontend/public/images/favicon/android-chrome-512x512.png new file mode 100644 index 0000000..8f95e00 Binary files /dev/null and b/frontend/public/images/favicon/android-chrome-512x512.png differ diff --git a/frontend/public/images/favicon/apple-touch-icon.png b/frontend/public/images/favicon/apple-touch-icon.png new file mode 100644 index 0000000..e8f243a Binary files /dev/null and b/frontend/public/images/favicon/apple-touch-icon.png differ diff --git a/frontend/public/images/favicon/browserconfig.xml b/frontend/public/images/favicon/browserconfig.xml new file mode 100644 index 0000000..87c8dbd --- /dev/null +++ b/frontend/public/images/favicon/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #333333 + + + diff --git a/frontend/public/images/favicon/favicon-16x16.png b/frontend/public/images/favicon/favicon-16x16.png new file mode 100644 index 0000000..e8a183e Binary files /dev/null and b/frontend/public/images/favicon/favicon-16x16.png differ diff --git a/frontend/public/images/favicon/favicon-32x32.png b/frontend/public/images/favicon/favicon-32x32.png new file mode 100644 index 0000000..4fa0cf0 Binary files /dev/null and b/frontend/public/images/favicon/favicon-32x32.png differ diff --git a/frontend/public/images/favicon/favicon.ico b/frontend/public/images/favicon/favicon.ico new file mode 100644 index 0000000..9eae8b5 Binary files /dev/null and b/frontend/public/images/favicon/favicon.ico differ diff --git a/frontend/public/images/favicon/mstile-150x150.png b/frontend/public/images/favicon/mstile-150x150.png new file mode 100644 index 0000000..4a3fd8c Binary files /dev/null and b/frontend/public/images/favicon/mstile-150x150.png differ diff --git a/frontend/public/images/favicon/safari-pinned-tab.svg b/frontend/public/images/favicon/safari-pinned-tab.svg new file mode 100644 index 0000000..5f9ea3f --- /dev/null +++ b/frontend/public/images/favicon/safari-pinned-tab.svg @@ -0,0 +1,85 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + diff --git a/frontend/public/images/favicon/site.webmanifest b/frontend/public/images/favicon/site.webmanifest new file mode 100644 index 0000000..99d1016 --- /dev/null +++ b/frontend/public/images/favicon/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/images/favicons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/images/favicons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/frontend/public/images/logo-256.png b/frontend/public/images/logo-256.png new file mode 100644 index 0000000..2bfb661 Binary files /dev/null and b/frontend/public/images/logo-256.png differ diff --git a/frontend/public/images/logo-bold-horizontal-grey.svg b/frontend/public/images/logo-bold-horizontal-grey.svg new file mode 100644 index 0000000..c87396f --- /dev/null +++ b/frontend/public/images/logo-bold-horizontal-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/images/logo-no-text.svg b/frontend/public/images/logo-no-text.svg new file mode 100644 index 0000000..dc3c116 --- /dev/null +++ b/frontend/public/images/logo-no-text.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/logo-text-horizontal-grey.png b/frontend/public/images/logo-text-horizontal-grey.png new file mode 100644 index 0000000..057a83d Binary files /dev/null and b/frontend/public/images/logo-text-horizontal-grey.png differ diff --git a/frontend/public/images/logo-text-vertical-grey.png b/frontend/public/images/logo-text-vertical-grey.png new file mode 100644 index 0000000..7027317 Binary files /dev/null and b/frontend/public/images/logo-text-vertical-grey.png differ diff --git a/frontend/public/images/unhealthy.svg b/frontend/public/images/unhealthy.svg new file mode 100644 index 0000000..6c39eeb --- /dev/null +++ b/frontend/public/images/unhealthy.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..ad335ca --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,118 @@ +:root { + color-scheme: light dark; +} + +.light { + color-scheme: light; +} +.dark { + color-scheme: dark; +} + +.modal-backdrop { + --tblr-backdrop-opacity: 0.8 !important; +} + +[data-bs-theme="dark"] .modal-content { + --tblr-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +[data-bs-theme="dark"] .modal-backdrop { + --tblr-backdrop-bg: #000 !important; + --tblr-backdrop-opacity: 0.65 !important; +} + +.domain-name { + font-family: monospace; +} + +.mr-1 { + margin-right: 0.25rem; +} +.ml-1 { + margin-left: 0.25rem; +} + +.react-select-container { + .react-select__control { + color: var(--tblr-body-color); + background-color: var(--tblr-bg-forms); + border: var(--tblr-border-width) solid var(--tblr-border-color); + + .react-select__input { + color: var(--tblr-body-color) !important; + } + + .react-select__single-value { + color: var(--tblr-body-color); + } + + .react-select__multi-value { + border: 1px solid var(--tblr-border-color); + background-color: var(--tblr-bg-surface-tertiary); + color: var(--tblr-secondary) !important; + + .react-select__multi-value__label { + color: var(--tblr-secondary) !important; + } + } + } + + .react-select__menu { + background-color: var(--tblr-bg-forms); + + .react-select__option { + background: rgba(var(--tblr-primary-rgb), .04); + color: inherit !important; + &.react-select__option--is-focused { + background: rgba(var(--tblr-primary-rgb), .1); + } + + &.react-select__option--is-focused.react-select__option--is-selected { + background: rgba(var(--tblr-primary-rgb), .2); + } + } + } +} + +.textareaMono { + font-family: 'Courier New', Courier, monospace !important; + resize: vertical; +} + +label.row { + cursor: pointer; +} + +.input-group-select { + display: flex; + align-items: center; + padding: 0; + font-size: .875rem; + font-weight: 400; + line-height: 1.25rem; + color: var(--tblr-gray-500); + text-align: center; + white-space: nowrap; + background-color: var(--tblr-bg-surface-secondary); + border: var(--tblr-border-width) solid var(--tblr-border-color); + border-radius: var(--tblr-border-radius); + + .form-select { + border: none; + background-color: var(--tblr-bg-surface-secondary); + border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius); + } +} + +/* Fix for dropdown menus being clipped by table-responsive containers. */ +.table-responsive .dropdown { + position: static; +} + +/* Fix for Tabler scrollbar compensation */ +@media (min-width: 992px) { + :host, :root { + margin-left: 0; + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..b6f0bba --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,41 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import EasyModal from "ez-modal-react"; +import { RawIntlProvider } from "react-intl"; +import { ToastContainer } from "react-toastify"; +import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context"; +import { intl } from "src/locale"; +import Router from "src/Router.tsx"; + +// Create a client +const queryClient = new QueryClient(); + +function App() { + return ( + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx new file mode 100644 index 0000000..9b5152a --- /dev/null +++ b/frontend/src/Router.tsx @@ -0,0 +1,85 @@ +import { lazy, Suspense } from "react"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { + ErrorNotFound, + LoadingPage, + Page, + SiteContainer, + SiteFooter, + SiteHeader, + SiteMenu, + Unhealthy, +} from "src/components"; +import { useAuthState } from "src/context"; +import { useHealth } from "src/hooks"; + +const Setup = lazy(() => import("src/pages/Setup")); +const Login = lazy(() => import("src/pages/Login")); +const Dashboard = lazy(() => import("src/pages/Dashboard")); +const Settings = lazy(() => import("src/pages/Settings")); +const Certificates = lazy(() => import("src/pages/Certificates")); +const Access = lazy(() => import("src/pages/Access")); +const AuditLog = lazy(() => import("src/pages/AuditLog")); +const Users = lazy(() => import("src/pages/Users")); +const ProxyHosts = lazy(() => import("src/pages/Nginx/ProxyHosts")); +const RedirectionHosts = lazy(() => import("src/pages/Nginx/RedirectionHosts")); +const DeadHosts = lazy(() => import("src/pages/Nginx/DeadHosts")); +const Streams = lazy(() => import("src/pages/Nginx/Streams")); +const WireGuard = lazy(() => import("src/pages/WireGuard")); + +function Router() { + const health = useHealth(); + const { authenticated } = useAuthState(); + + if (health.isLoading) { + return ; + } + + if (health.isError || health.data?.status !== "OK") { + return ; + } + + if (!health.data?.setup) { + return ; + } + + if (!authenticated) { + return ( + }> + + + ); + } + + return ( + + +
+ + +
+ + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + +
+
+ ); +} + +export default Router; diff --git a/frontend/src/api/backend/base.ts b/frontend/src/api/backend/base.ts new file mode 100644 index 0000000..d56f120 --- /dev/null +++ b/frontend/src/api/backend/base.ts @@ -0,0 +1,163 @@ +import { QueryClient } from "@tanstack/react-query"; +import { camelizeKeys, decamelize, decamelizeKeys } from "humps"; +import queryString, { type StringifiableRecord } from "query-string"; +import AuthStore from "src/modules/AuthStore"; + +const queryClient = new QueryClient(); +const contentTypeHeader = "Content-Type"; + +interface BuildUrlArgs { + url: string; + params?: StringifiableRecord; +} + +function decamelizeParams(params?: StringifiableRecord): StringifiableRecord | undefined { + if (!params) { + return undefined; + } + const result: StringifiableRecord = {}; + for (const [key, value] of Object.entries(params)) { + result[decamelize(key)] = value; + } + + return result; +} + +function buildUrl({ url, params }: BuildUrlArgs) { + const endpoint = url.replace(/^\/|\/$/g, ""); + const baseUrl = `/api/${endpoint}`; + const apiUrl = queryString.stringifyUrl({ + url: baseUrl, + query: decamelizeParams(params), + }); + return apiUrl; +} + +function buildAuthHeader(): Record | undefined { + if (AuthStore.token) { + return { Authorization: `Bearer ${AuthStore.token.token}` }; + } + return {}; +} + +function buildBody(data?: Record): string | undefined { + if (data) { + return JSON.stringify(decamelizeKeys(data)); + } +} + +async function processResponse(response: Response) { + const payload = await response.json(); + if (!response.ok) { + if (response.status === 401) { + // Force logout user and reload the page if Unauthorized + AuthStore.clear(); + queryClient.clear(); + window.location.reload(); + } + throw new Error( + typeof payload.error.messageI18n !== "undefined" ? payload.error.messageI18n : payload.error.message, + ); + } + return camelizeKeys(payload) as any; +} + +interface GetArgs { + url: string; + params?: queryString.StringifiableRecord; +} + +async function baseGet({ url, params }: GetArgs, abortController?: AbortController) { + const apiUrl = buildUrl({ url, params }); + const method = "GET"; + const headers = buildAuthHeader(); + const signal = abortController?.signal; + const response = await fetch(apiUrl, { method, headers, signal }); + return response; +} + +export async function get(args: GetArgs, abortController?: AbortController) { + return processResponse(await baseGet(args, abortController)); +} + +export async function download({ url, params }: GetArgs, filename = "download.file") { + const headers = buildAuthHeader(); + const res = await fetch(buildUrl({ url, params }), { headers }); + const bl = await res.blob(); + const u = window.URL.createObjectURL(bl); + const a = document.createElement("a"); + a.href = u; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); +} + +interface PostArgs { + url: string; + params?: queryString.StringifiableRecord; + data?: any; + noAuth?: boolean; +} + +export async function post({ url, params, data, noAuth }: PostArgs, abortController?: AbortController) { + const apiUrl = buildUrl({ url, params }); + const method = "POST"; + + let headers: Record = {}; + if (!noAuth) { + headers = { + ...buildAuthHeader(), + }; + } + + let body: string | FormData | undefined; + // Check if the data is an instance of FormData + // If data is FormData, let the browser set the Content-Type header + if (data instanceof FormData) { + body = data; + } else { + // If data is JSON, set the Content-Type header to 'application/json' + headers = { + ...headers, + [contentTypeHeader]: "application/json", + }; + body = buildBody(data); + } + + const signal = abortController?.signal; + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response); +} + +interface PutArgs { + url: string; + params?: queryString.StringifiableRecord; + data?: Record; +} +export async function put({ url, params, data }: PutArgs, abortController?: AbortController) { + const apiUrl = buildUrl({ url, params }); + const method = "PUT"; + const headers = { + ...buildAuthHeader(), + [contentTypeHeader]: "application/json", + }; + const signal = abortController?.signal; + const body = buildBody(data); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response); +} + +interface DeleteArgs { + url: string; + params?: queryString.StringifiableRecord; +} +export async function del({ url, params }: DeleteArgs, abortController?: AbortController) { + const apiUrl = buildUrl({ url, params }); + const method = "DELETE"; + const headers = { + ...buildAuthHeader(), + }; + const signal = abortController?.signal; + const response = await fetch(apiUrl, { method, headers, signal }); + return processResponse(response); +} diff --git a/frontend/src/api/backend/checkVersion.ts b/frontend/src/api/backend/checkVersion.ts new file mode 100644 index 0000000..5095526 --- /dev/null +++ b/frontend/src/api/backend/checkVersion.ts @@ -0,0 +1,8 @@ +import * as api from "./base"; +import type { VersionCheckResponse } from "./responseTypes"; + +export async function checkVersion(): Promise { + return await api.get({ + url: "/version/check", + }); +} diff --git a/frontend/src/api/backend/createAccessList.ts b/frontend/src/api/backend/createAccessList.ts new file mode 100644 index 0000000..4a17f67 --- /dev/null +++ b/frontend/src/api/backend/createAccessList.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { AccessList } from "./models"; + +export async function createAccessList(item: AccessList): Promise { + return await api.post({ + url: "/nginx/access-lists", + data: item, + }); +} diff --git a/frontend/src/api/backend/createCertificate.ts b/frontend/src/api/backend/createCertificate.ts new file mode 100644 index 0000000..ea81c44 --- /dev/null +++ b/frontend/src/api/backend/createCertificate.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { Certificate } from "./models"; + +export async function createCertificate(item: Certificate): Promise { + return await api.post({ + url: "/nginx/certificates", + data: item, + }); +} diff --git a/frontend/src/api/backend/createDeadHost.ts b/frontend/src/api/backend/createDeadHost.ts new file mode 100644 index 0000000..f2a9f45 --- /dev/null +++ b/frontend/src/api/backend/createDeadHost.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { DeadHost } from "./models"; + +export async function createDeadHost(item: DeadHost): Promise { + return await api.post({ + url: "/nginx/dead-hosts", + data: item, + }); +} diff --git a/frontend/src/api/backend/createProxyHost.ts b/frontend/src/api/backend/createProxyHost.ts new file mode 100644 index 0000000..fcde7cd --- /dev/null +++ b/frontend/src/api/backend/createProxyHost.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { ProxyHost } from "./models"; + +export async function createProxyHost(item: ProxyHost): Promise { + return await api.post({ + url: "/nginx/proxy-hosts", + data: item, + }); +} diff --git a/frontend/src/api/backend/createRedirectionHost.ts b/frontend/src/api/backend/createRedirectionHost.ts new file mode 100644 index 0000000..a797f81 --- /dev/null +++ b/frontend/src/api/backend/createRedirectionHost.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { RedirectionHost } from "./models"; + +export async function createRedirectionHost(item: RedirectionHost): Promise { + return await api.post({ + url: "/nginx/redirection-hosts", + data: item, + }); +} diff --git a/frontend/src/api/backend/createStream.ts b/frontend/src/api/backend/createStream.ts new file mode 100644 index 0000000..adad2a1 --- /dev/null +++ b/frontend/src/api/backend/createStream.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { Stream } from "./models"; + +export async function createStream(item: Stream): Promise { + return await api.post({ + url: "/nginx/streams", + data: item, + }); +} diff --git a/frontend/src/api/backend/createUser.ts b/frontend/src/api/backend/createUser.ts new file mode 100644 index 0000000..c5ea306 --- /dev/null +++ b/frontend/src/api/backend/createUser.ts @@ -0,0 +1,24 @@ +import * as api from "./base"; +import type { User } from "./models"; + +export interface AuthOptions { + type: string; + secret: string; +} + +export interface NewUser { + name: string; + nickname: string; + email: string; + isDisabled?: boolean; + auth?: AuthOptions; + roles?: string[]; +} + +export async function createUser(item: NewUser, noAuth?: boolean): Promise { + return await api.post({ + url: "/users", + data: item, + noAuth, + }); +} diff --git a/frontend/src/api/backend/deleteAccessList.ts b/frontend/src/api/backend/deleteAccessList.ts new file mode 100644 index 0000000..838c940 --- /dev/null +++ b/frontend/src/api/backend/deleteAccessList.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteAccessList(id: number): Promise { + return await api.del({ + url: `/nginx/access-lists/${id}`, + }); +} diff --git a/frontend/src/api/backend/deleteCertificate.ts b/frontend/src/api/backend/deleteCertificate.ts new file mode 100644 index 0000000..2257316 --- /dev/null +++ b/frontend/src/api/backend/deleteCertificate.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteCertificate(id: number): Promise { + return await api.del({ + url: `/nginx/certificates/${id}`, + }); +} diff --git a/frontend/src/api/backend/deleteDeadHost.ts b/frontend/src/api/backend/deleteDeadHost.ts new file mode 100644 index 0000000..8e72ca8 --- /dev/null +++ b/frontend/src/api/backend/deleteDeadHost.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteDeadHost(id: number): Promise { + return await api.del({ + url: `/nginx/dead-hosts/${id}`, + }); +} diff --git a/frontend/src/api/backend/deleteProxyHost.ts b/frontend/src/api/backend/deleteProxyHost.ts new file mode 100644 index 0000000..7b7f2d8 --- /dev/null +++ b/frontend/src/api/backend/deleteProxyHost.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteProxyHost(id: number): Promise { + return await api.del({ + url: `/nginx/proxy-hosts/${id}`, + }); +} diff --git a/frontend/src/api/backend/deleteRedirectionHost.ts b/frontend/src/api/backend/deleteRedirectionHost.ts new file mode 100644 index 0000000..7c594b6 --- /dev/null +++ b/frontend/src/api/backend/deleteRedirectionHost.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteRedirectionHost(id: number): Promise { + return await api.del({ + url: `/nginx/redirection-hosts/${id}`, + }); +} diff --git a/frontend/src/api/backend/deleteStream.ts b/frontend/src/api/backend/deleteStream.ts new file mode 100644 index 0000000..db9e11f --- /dev/null +++ b/frontend/src/api/backend/deleteStream.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteStream(id: number): Promise { + return await api.del({ + url: `/nginx/streams/${id}`, + }); +} diff --git a/frontend/src/api/backend/deleteUser.ts b/frontend/src/api/backend/deleteUser.ts new file mode 100644 index 0000000..2a04a6e --- /dev/null +++ b/frontend/src/api/backend/deleteUser.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function deleteUser(id: number): Promise { + return await api.del({ + url: `/users/${id}`, + }); +} diff --git a/frontend/src/api/backend/downloadCertificate.ts b/frontend/src/api/backend/downloadCertificate.ts new file mode 100644 index 0000000..079f335 --- /dev/null +++ b/frontend/src/api/backend/downloadCertificate.ts @@ -0,0 +1,10 @@ +import * as api from "./base"; + +export async function downloadCertificate(id: number): Promise { + await api.download( + { + url: `/nginx/certificates/${id}/download`, + }, + `certificate-${id}.zip`, + ); +} diff --git a/frontend/src/api/backend/expansions.ts b/frontend/src/api/backend/expansions.ts new file mode 100644 index 0000000..e098a49 --- /dev/null +++ b/frontend/src/api/backend/expansions.ts @@ -0,0 +1,6 @@ +export type AccessListExpansion = "owner" | "items" | "clients"; +export type AuditLogExpansion = "user"; +export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts" | "streams"; +export type HostExpansion = "owner" | "certificate"; +export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; +export type UserExpansion = "permissions"; diff --git a/frontend/src/api/backend/getAccessList.ts b/frontend/src/api/backend/getAccessList.ts new file mode 100644 index 0000000..a1afd35 --- /dev/null +++ b/frontend/src/api/backend/getAccessList.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { AccessListExpansion } from "./expansions"; +import type { AccessList } from "./models"; + +export async function getAccessList(id: number, expand?: AccessListExpansion[], params = {}): Promise { + return await api.get({ + url: `/nginx/access-lists/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getAccessLists.ts b/frontend/src/api/backend/getAccessLists.ts new file mode 100644 index 0000000..515f1e2 --- /dev/null +++ b/frontend/src/api/backend/getAccessLists.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { AccessListExpansion } from "./expansions"; +import type { AccessList } from "./models"; + +export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise { + return await api.get({ + url: "/nginx/access-lists", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getAuditLog.ts b/frontend/src/api/backend/getAuditLog.ts new file mode 100644 index 0000000..d2d01f7 --- /dev/null +++ b/frontend/src/api/backend/getAuditLog.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { AuditLogExpansion } from "./expansions"; +import type { AuditLog } from "./models"; + +export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise { + return await api.get({ + url: `/audit-log/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getAuditLogs.ts b/frontend/src/api/backend/getAuditLogs.ts new file mode 100644 index 0000000..bbdb2f1 --- /dev/null +++ b/frontend/src/api/backend/getAuditLogs.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { AuditLogExpansion } from "./expansions"; +import type { AuditLog } from "./models"; + +export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise { + return await api.get({ + url: "/audit-log", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getCertificate.ts b/frontend/src/api/backend/getCertificate.ts new file mode 100644 index 0000000..13de898 --- /dev/null +++ b/frontend/src/api/backend/getCertificate.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { CertificateExpansion } from "./expansions"; +import type { Certificate } from "./models"; + +export async function getCertificate(id: number, expand?: CertificateExpansion[], params = {}): Promise { + return await api.get({ + url: `/nginx/certificates/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getCertificateDNSProviders.ts b/frontend/src/api/backend/getCertificateDNSProviders.ts new file mode 100644 index 0000000..03e3afa --- /dev/null +++ b/frontend/src/api/backend/getCertificateDNSProviders.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { DNSProvider } from "./models"; + +export async function getCertificateDNSProviders(params = {}): Promise { + return await api.get({ + url: "/nginx/certificates/dns-providers", + params, + }); +} diff --git a/frontend/src/api/backend/getCertificates.ts b/frontend/src/api/backend/getCertificates.ts new file mode 100644 index 0000000..d6d215b --- /dev/null +++ b/frontend/src/api/backend/getCertificates.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { CertificateExpansion } from "./expansions"; +import type { Certificate } from "./models"; + +export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise { + return await api.get({ + url: "/nginx/certificates", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getDeadHost.ts b/frontend/src/api/backend/getDeadHost.ts new file mode 100644 index 0000000..d6c062b --- /dev/null +++ b/frontend/src/api/backend/getDeadHost.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { HostExpansion } from "./expansions"; +import type { DeadHost } from "./models"; + +export async function getDeadHost(id: number, expand?: HostExpansion[], params = {}): Promise { + return await api.get({ + url: `/nginx/dead-hosts/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getDeadHosts.ts b/frontend/src/api/backend/getDeadHosts.ts new file mode 100644 index 0000000..1ca410e --- /dev/null +++ b/frontend/src/api/backend/getDeadHosts.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { HostExpansion } from "./expansions"; +import type { DeadHost } from "./models"; + +export async function getDeadHosts(expand?: HostExpansion[], params = {}): Promise { + return await api.get({ + url: "/nginx/dead-hosts", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getHealth.ts b/frontend/src/api/backend/getHealth.ts new file mode 100644 index 0000000..055681c --- /dev/null +++ b/frontend/src/api/backend/getHealth.ts @@ -0,0 +1,8 @@ +import * as api from "./base"; +import type { HealthResponse } from "./responseTypes"; + +export async function getHealth(): Promise { + return await api.get({ + url: "/", + }); +} diff --git a/frontend/src/api/backend/getHostsReport.ts b/frontend/src/api/backend/getHostsReport.ts new file mode 100644 index 0000000..938d3a7 --- /dev/null +++ b/frontend/src/api/backend/getHostsReport.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function getHostsReport(): Promise> { + return await api.get({ + url: "/reports/hosts", + }); +} diff --git a/frontend/src/api/backend/getProxyHost.ts b/frontend/src/api/backend/getProxyHost.ts new file mode 100644 index 0000000..911b89c --- /dev/null +++ b/frontend/src/api/backend/getProxyHost.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { ProxyHostExpansion } from "./expansions"; +import type { ProxyHost } from "./models"; + +export async function getProxyHost(id: number, expand?: ProxyHostExpansion[], params = {}): Promise { + return await api.get({ + url: `/nginx/proxy-hosts/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getProxyHosts.ts b/frontend/src/api/backend/getProxyHosts.ts new file mode 100644 index 0000000..ba9c864 --- /dev/null +++ b/frontend/src/api/backend/getProxyHosts.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { ProxyHostExpansion } from "./expansions"; +import type { ProxyHost } from "./models"; + +export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise { + return await api.get({ + url: "/nginx/proxy-hosts", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getRedirectionHost.ts b/frontend/src/api/backend/getRedirectionHost.ts new file mode 100644 index 0000000..01df988 --- /dev/null +++ b/frontend/src/api/backend/getRedirectionHost.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { HostExpansion } from "./expansions"; +import type { RedirectionHost } from "./models"; + +export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise { + return await api.get({ + url: `/nginx/redirection-hosts/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getRedirectionHosts.ts b/frontend/src/api/backend/getRedirectionHosts.ts new file mode 100644 index 0000000..63e292f --- /dev/null +++ b/frontend/src/api/backend/getRedirectionHosts.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { HostExpansion } from "./expansions"; +import type { RedirectionHost } from "./models"; + +export async function getRedirectionHosts(expand?: HostExpansion[], params = {}): Promise { + return await api.get({ + url: "/nginx/redirection-hosts", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getSetting.ts b/frontend/src/api/backend/getSetting.ts new file mode 100644 index 0000000..daa54fe --- /dev/null +++ b/frontend/src/api/backend/getSetting.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { Setting } from "./models"; + +export async function getSetting(id: string, expand?: string[], params = {}): Promise { + return await api.get({ + url: `/settings/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getSettings.ts b/frontend/src/api/backend/getSettings.ts new file mode 100644 index 0000000..b8e992c --- /dev/null +++ b/frontend/src/api/backend/getSettings.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { Setting } from "./models"; + +export async function getSettings(expand?: string[], params = {}): Promise { + return await api.get({ + url: "/settings", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getStream.ts b/frontend/src/api/backend/getStream.ts new file mode 100644 index 0000000..82e10a0 --- /dev/null +++ b/frontend/src/api/backend/getStream.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { HostExpansion } from "./expansions"; +import type { Stream } from "./models"; + +export async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise { + return await api.get({ + url: `/nginx/streams/${id}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getStreams.ts b/frontend/src/api/backend/getStreams.ts new file mode 100644 index 0000000..b5e9379 --- /dev/null +++ b/frontend/src/api/backend/getStreams.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { HostExpansion } from "./expansions"; +import type { Stream } from "./models"; + +export async function getStreams(expand?: HostExpansion[], params = {}): Promise { + return await api.get({ + url: "/nginx/streams", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getToken.ts b/frontend/src/api/backend/getToken.ts new file mode 100644 index 0000000..7f62a0e --- /dev/null +++ b/frontend/src/api/backend/getToken.ts @@ -0,0 +1,22 @@ +import * as api from "./base"; +import type { TokenResponse, TwoFactorChallengeResponse } from "./responseTypes"; + +export type LoginResponse = TokenResponse | TwoFactorChallengeResponse; + +export function isTwoFactorChallenge(response: LoginResponse): response is TwoFactorChallengeResponse { + return "requires2fa" in response && response.requires2fa === true; +} + +export async function getToken(identity: string, secret: string): Promise { + return await api.post({ + url: "/tokens", + data: { identity, secret }, + }); +} + +export async function verify2FA(challengeToken: string, code: string): Promise { + return await api.post({ + url: "/tokens/2fa", + data: { challengeToken, code }, + }); +} diff --git a/frontend/src/api/backend/getUser.ts b/frontend/src/api/backend/getUser.ts new file mode 100644 index 0000000..a006782 --- /dev/null +++ b/frontend/src/api/backend/getUser.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import type { UserExpansion } from "./expansions"; +import type { User } from "./models"; + +export async function getUser(id: number | string = "me", expand?: UserExpansion[], params = {}): Promise { + const userId = id ? id : "me"; + return await api.get({ + url: `/users/${userId}`, + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/getUsers.ts b/frontend/src/api/backend/getUsers.ts new file mode 100644 index 0000000..dab584f --- /dev/null +++ b/frontend/src/api/backend/getUsers.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import type { UserExpansion } from "./expansions"; +import type { User } from "./models"; + +export async function getUsers(expand?: UserExpansion[], params = {}): Promise { + return await api.get({ + url: "/users", + params: { + expand: expand?.join(","), + ...params, + }, + }); +} diff --git a/frontend/src/api/backend/helpers.ts b/frontend/src/api/backend/helpers.ts new file mode 100644 index 0000000..8d97731 --- /dev/null +++ b/frontend/src/api/backend/helpers.ts @@ -0,0 +1,54 @@ +import { decamelize } from "humps"; + +/** + * This will convert a react-table sort object into + * a string that the backend api likes: + * name.asc,id.desc + */ +export function tableSortToAPI(sortBy: any): string | undefined { + if (sortBy?.length > 0) { + const strs: string[] = []; + sortBy.map((item: any) => { + strs.push(`${decamelize(item.id)}.${item.desc ? "desc" : "asc"}`); + return undefined; + }); + return strs.join(","); + } + return; +} + +/** + * This will convert a react-table filters object into + * a string that the backend api likes: + * name:contains=jam + */ +export function tableFiltersToAPI(filters: any[]): { [key: string]: string } { + const items: { [key: string]: string } = {}; + if (filters?.length > 0) { + filters.map((item: any) => { + items[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value; + return undefined; + }); + } + return items; +} + +/** + * Builds a filters object by removing entries with undefined, null, or empty string values. + * + */ +export function buildFilters(filters?: Record) { + if (!filters) { + return filters; + } + const result: Record = {}; + for (const key in filters) { + const value = filters[key]; + // If the value is undefined, null, or an empty string, skip it + if (value === undefined || value === null || value === "") { + continue; + } + result[key] = value.toString(); + } + return result; +} diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts new file mode 100644 index 0000000..8c58687 --- /dev/null +++ b/frontend/src/api/backend/index.ts @@ -0,0 +1,64 @@ +export * from "./checkVersion"; +export * from "./createAccessList"; +export * from "./createCertificate"; +export * from "./createDeadHost"; +export * from "./createProxyHost"; +export * from "./createRedirectionHost"; +export * from "./createStream"; +export * from "./createUser"; +export * from "./deleteAccessList"; +export * from "./deleteCertificate"; +export * from "./deleteDeadHost"; +export * from "./deleteProxyHost"; +export * from "./deleteRedirectionHost"; +export * from "./deleteStream"; +export * from "./deleteUser"; +export * from "./downloadCertificate"; +export * from "./expansions"; +export * from "./getAccessList"; +export * from "./getAccessLists"; +export * from "./getAuditLog"; +export * from "./getAuditLogs"; +export * from "./getCertificate"; +export * from "./getCertificateDNSProviders"; +export * from "./getCertificates"; +export * from "./getDeadHost"; +export * from "./getDeadHosts"; +export * from "./getHealth"; +export * from "./getHostsReport"; +export * from "./getProxyHost"; +export * from "./getProxyHosts"; +export * from "./getRedirectionHost"; +export * from "./getRedirectionHosts"; +export * from "./getSetting"; +export * from "./getSettings"; +export * from "./getStream"; +export * from "./getStreams"; +export * from "./getToken"; +export * from "./getUser"; +export * from "./getUsers"; +export * from "./helpers"; +export * from "./loginAsUser"; +export * from "./models"; +export * from "./refreshToken"; +export * from "./renewCertificate"; +export * from "./responseTypes"; +export * from "./setPermissions"; +export * from "./testHttpCertificate"; +export * from "./toggleDeadHost"; +export * from "./toggleProxyHost"; +export * from "./toggleRedirectionHost"; +export * from "./toggleStream"; +export * from "./toggleUser"; +export * from "./updateAccessList"; +export * from "./updateAuth"; +export * from "./updateDeadHost"; +export * from "./updateProxyHost"; +export * from "./updateRedirectionHost"; +export * from "./updateSetting"; +export * from "./updateStream"; +export * from "./updateUser"; +export * from "./uploadCertificate"; +export * from "./validateCertificate"; +export * from "./twoFactor"; +export * from "./wireguard"; diff --git a/frontend/src/api/backend/loginAsUser.ts b/frontend/src/api/backend/loginAsUser.ts new file mode 100644 index 0000000..2ade379 --- /dev/null +++ b/frontend/src/api/backend/loginAsUser.ts @@ -0,0 +1,8 @@ +import * as api from "./base"; +import type { LoginAsTokenResponse } from "./responseTypes"; + +export async function loginAsUser(id: number): Promise { + return await api.post({ + url: `/users/${id}/login`, + }); +} diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts new file mode 100644 index 0000000..2ae0b08 --- /dev/null +++ b/frontend/src/api/backend/models.ts @@ -0,0 +1,210 @@ +export interface AppVersion { + major: number; + minor: number; + revision: number; +} + +export interface UserPermissions { + id?: number; + createdOn?: string; + modifiedOn?: string; + userId?: number; + visibility: string; + proxyHosts: string; + redirectionHosts: string; + deadHosts: string; + streams: string; + accessLists: string; + certificates: string; +} + +export interface User { + id: number; + createdOn: string; + modifiedOn: string; + isDisabled: boolean; + email: string; + name: string; + nickname: string; + avatar: string; + roles: string[]; + permissions?: UserPermissions; +} + +export interface AuditLog { + id: number; + createdOn: string; + modifiedOn: string; + userId: number; + objectType: string; + objectId: number; + action: string; + meta: Record; + // Expansions: + user?: User; +} + +export interface AccessList { + id?: number; + createdOn?: string; + modifiedOn?: string; + ownerUserId: number; + name: string; + meta: Record; + satisfyAny: boolean; + passAuth: boolean; + proxyHostCount?: number; + // Expansions: + owner?: User; + items?: AccessListItem[]; + clients?: AccessListClient[]; +} + +export interface AccessListItem { + id?: number; + createdOn?: string; + modifiedOn?: string; + accessListId?: number; + username: string; + password: string; + meta?: Record; + hint?: string; +} + +export type AccessListClient = { + id?: number; + createdOn?: string; + modifiedOn?: string; + accessListId?: number; + address: string; + directive: "allow" | "deny"; + meta?: Record; +}; + +export interface Certificate { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + provider: string; + niceName: string; + domainNames: string[]; + expiresOn: string; + meta: Record; + owner?: User; + proxyHosts?: ProxyHost[]; + deadHosts?: DeadHost[]; + redirectionHosts?: RedirectionHost[]; +} + +export interface ProxyLocation { + path: string; + advancedConfig: string; + forwardScheme: string; + forwardHost: string; + forwardPort: number; +} + +export interface ProxyHost { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + domainNames: string[]; + forwardScheme: string; + forwardHost: string; + forwardPort: number; + accessListId: number; + certificateId: number; + sslForced: boolean; + cachingEnabled: boolean; + blockExploits: boolean; + advancedConfig: string; + meta: Record; + allowWebsocketUpgrade: boolean; + http2Support: boolean; + enabled: boolean; + locations?: ProxyLocation[]; + hstsEnabled: boolean; + hstsSubdomains: boolean; + trustForwardedProto: boolean; + // Expansions: + owner?: User; + accessList?: AccessList; + certificate?: Certificate; +} + +export interface DeadHost { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + domainNames: string[]; + certificateId: number; + sslForced: boolean; + advancedConfig: string; + meta: Record; + http2Support: boolean; + enabled: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + // Expansions: + owner?: User; + certificate?: Certificate; +} + +export interface RedirectionHost { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + domainNames: string[]; + forwardDomainName: string; + preservePath: boolean; + certificateId: number; + sslForced: boolean; + blockExploits: boolean; + advancedConfig: string; + meta: Record; + http2Support: boolean; + forwardScheme: string; + forwardHttpCode: number; + enabled: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + // Expansions: + owner?: User; + certificate?: Certificate; +} + +export interface Stream { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + incomingPort: number; + forwardingHost: string; + forwardingPort: number; + tcpForwarding: boolean; + udpForwarding: boolean; + meta: Record; + enabled: boolean; + certificateId: number; + // Expansions: + owner?: User; + certificate?: Certificate; +} + +export interface Setting { + id: string; + name?: string; + description?: string; + value: string; + meta?: Record; +} + +export interface DNSProvider { + id: string; + name: string; + credentials: string; +} diff --git a/frontend/src/api/backend/refreshToken.ts b/frontend/src/api/backend/refreshToken.ts new file mode 100644 index 0000000..de1848c --- /dev/null +++ b/frontend/src/api/backend/refreshToken.ts @@ -0,0 +1,8 @@ +import * as api from "./base"; +import type { TokenResponse } from "./responseTypes"; + +export async function refreshToken(): Promise { + return await api.get({ + url: "/tokens", + }); +} diff --git a/frontend/src/api/backend/renewCertificate.ts b/frontend/src/api/backend/renewCertificate.ts new file mode 100644 index 0000000..0f3d082 --- /dev/null +++ b/frontend/src/api/backend/renewCertificate.ts @@ -0,0 +1,8 @@ +import * as api from "./base"; +import type { Certificate } from "./models"; + +export async function renewCertificate(id: number): Promise { + return await api.post({ + url: `/nginx/certificates/${id}/renew`, + }); +} diff --git a/frontend/src/api/backend/responseTypes.ts b/frontend/src/api/backend/responseTypes.ts new file mode 100644 index 0000000..2f88ede --- /dev/null +++ b/frontend/src/api/backend/responseTypes.ts @@ -0,0 +1,46 @@ +import type { AppVersion, User } from "./models"; + +export interface HealthResponse { + status: string; + version: AppVersion; + setup: boolean; +} + +export interface TokenResponse { + expires: number; + token: string; +} + +export interface ValidatedCertificateResponse { + certificate: Record; + certificateKey: boolean; +} + +export interface LoginAsTokenResponse extends TokenResponse { + user: User; +} + +export interface VersionCheckResponse { + current: string | null; + latest: string | null; + updateAvailable: boolean; +} + +export interface TwoFactorChallengeResponse { + requires2fa: boolean; + challengeToken: string; +} + +export interface TwoFactorStatusResponse { + enabled: boolean; + backupCodesRemaining: number; +} + +export interface TwoFactorSetupResponse { + secret: string; + otpauthUrl: string; +} + +export interface TwoFactorEnableResponse { + backupCodes: string[]; +} diff --git a/frontend/src/api/backend/setPermissions.ts b/frontend/src/api/backend/setPermissions.ts new file mode 100644 index 0000000..47fa630 --- /dev/null +++ b/frontend/src/api/backend/setPermissions.ts @@ -0,0 +1,10 @@ +import * as api from "./base"; +import type { UserPermissions } from "./models"; + +export async function setPermissions(userId: number, data: UserPermissions): Promise { + // Remove readonly fields + return await api.put({ + url: `/users/${userId}/permissions`, + data, + }); +} diff --git a/frontend/src/api/backend/testHttpCertificate.ts b/frontend/src/api/backend/testHttpCertificate.ts new file mode 100644 index 0000000..23dfa4b --- /dev/null +++ b/frontend/src/api/backend/testHttpCertificate.ts @@ -0,0 +1,10 @@ +import * as api from "./base"; + +export async function testHttpCertificate(domains: string[]): Promise> { + return await api.post({ + url: "/nginx/certificates/test-http", + data: { + domains, + }, + }); +} diff --git a/frontend/src/api/backend/toggleDeadHost.ts b/frontend/src/api/backend/toggleDeadHost.ts new file mode 100644 index 0000000..71a780e --- /dev/null +++ b/frontend/src/api/backend/toggleDeadHost.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function toggleDeadHost(id: number, enabled: boolean): Promise { + return await api.post({ + url: `/nginx/dead-hosts/${id}/${enabled ? "enable" : "disable"}`, + }); +} diff --git a/frontend/src/api/backend/toggleProxyHost.ts b/frontend/src/api/backend/toggleProxyHost.ts new file mode 100644 index 0000000..376e788 --- /dev/null +++ b/frontend/src/api/backend/toggleProxyHost.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function toggleProxyHost(id: number, enabled: boolean): Promise { + return await api.post({ + url: `/nginx/proxy-hosts/${id}/${enabled ? "enable" : "disable"}`, + }); +} diff --git a/frontend/src/api/backend/toggleRedirectionHost.ts b/frontend/src/api/backend/toggleRedirectionHost.ts new file mode 100644 index 0000000..0cfa573 --- /dev/null +++ b/frontend/src/api/backend/toggleRedirectionHost.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function toggleRedirectionHost(id: number, enabled: boolean): Promise { + return await api.post({ + url: `/nginx/redirection-hosts/${id}/${enabled ? "enable" : "disable"}`, + }); +} diff --git a/frontend/src/api/backend/toggleStream.ts b/frontend/src/api/backend/toggleStream.ts new file mode 100644 index 0000000..2b71f72 --- /dev/null +++ b/frontend/src/api/backend/toggleStream.ts @@ -0,0 +1,7 @@ +import * as api from "./base"; + +export async function toggleStream(id: number, enabled: boolean): Promise { + return await api.post({ + url: `/nginx/streams/${id}/${enabled ? "enable" : "disable"}`, + }); +} diff --git a/frontend/src/api/backend/toggleUser.ts b/frontend/src/api/backend/toggleUser.ts new file mode 100644 index 0000000..d2a24c9 --- /dev/null +++ b/frontend/src/api/backend/toggleUser.ts @@ -0,0 +1,10 @@ +import type { User } from "./models"; +import { updateUser } from "./updateUser"; + +export async function toggleUser(id: number, enabled: boolean): Promise { + await updateUser({ + id, + isDisabled: !enabled, + } as User); + return true; +} diff --git a/frontend/src/api/backend/twoFactor.ts b/frontend/src/api/backend/twoFactor.ts new file mode 100644 index 0000000..855e9cc --- /dev/null +++ b/frontend/src/api/backend/twoFactor.ts @@ -0,0 +1,37 @@ +import * as api from "./base"; +import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes"; + +export async function get2FAStatus(userId: number | "me"): Promise { + return await api.get({ + url: `/users/${userId}/2fa`, + }); +} + +export async function start2FASetup(userId: number | "me"): Promise { + return await api.post({ + url: `/users/${userId}/2fa`, + }); +} + +export async function enable2FA(userId: number | "me", code: string): Promise { + return await api.post({ + url: `/users/${userId}/2fa/enable`, + data: { code }, + }); +} + +export async function disable2FA(userId: number | "me", code: string): Promise { + return await api.del({ + url: `/users/${userId}/2fa`, + params: { + code, + }, + }); +} + +export async function regenerateBackupCodes(userId: number | "me", code: string): Promise { + return await api.post({ + url: `/users/${userId}/2fa/backup-codes`, + data: { code }, + }); +} diff --git a/frontend/src/api/backend/updateAccessList.ts b/frontend/src/api/backend/updateAccessList.ts new file mode 100644 index 0000000..7a23566 --- /dev/null +++ b/frontend/src/api/backend/updateAccessList.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { AccessList } from "./models"; + +export async function updateAccessList(item: AccessList): Promise { + // Remove readonly fields + const { id, createdOn: _, modifiedOn: __, ...data } = item; + + return await api.put({ + url: `/nginx/access-lists/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/updateAuth.ts b/frontend/src/api/backend/updateAuth.ts new file mode 100644 index 0000000..5b14b07 --- /dev/null +++ b/frontend/src/api/backend/updateAuth.ts @@ -0,0 +1,18 @@ +import * as api from "./base"; +import type { User } from "./models"; + +export async function updateAuth(userId: number | "me", newPassword: string, current?: string): Promise { + const data = { + type: "password", + current: current, + secret: newPassword, + }; + if (userId === "me") { + data.current = current; + } + + return await api.put({ + url: `/users/${userId}/auth`, + data, + }); +} diff --git a/frontend/src/api/backend/updateDeadHost.ts b/frontend/src/api/backend/updateDeadHost.ts new file mode 100644 index 0000000..1eb5631 --- /dev/null +++ b/frontend/src/api/backend/updateDeadHost.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { DeadHost } from "./models"; + +export async function updateDeadHost(item: DeadHost): Promise { + // Remove readonly fields + const { id, createdOn: _, modifiedOn: __, ...data } = item; + + return await api.put({ + url: `/nginx/dead-hosts/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/updateProxyHost.ts b/frontend/src/api/backend/updateProxyHost.ts new file mode 100644 index 0000000..e7ee3d9 --- /dev/null +++ b/frontend/src/api/backend/updateProxyHost.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { ProxyHost } from "./models"; + +export async function updateProxyHost(item: ProxyHost): Promise { + // Remove readonly fields + const { id, createdOn: _, modifiedOn: __, ...data } = item; + + return await api.put({ + url: `/nginx/proxy-hosts/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/updateRedirectionHost.ts b/frontend/src/api/backend/updateRedirectionHost.ts new file mode 100644 index 0000000..4cc36f1 --- /dev/null +++ b/frontend/src/api/backend/updateRedirectionHost.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { RedirectionHost } from "./models"; + +export async function updateRedirectionHost(item: RedirectionHost): Promise { + // Remove readonly fields + const { id, createdOn: _, modifiedOn: __, ...data } = item; + + return await api.put({ + url: `/nginx/redirection-hosts/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/updateSetting.ts b/frontend/src/api/backend/updateSetting.ts new file mode 100644 index 0000000..bcb9405 --- /dev/null +++ b/frontend/src/api/backend/updateSetting.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { Setting } from "./models"; + +export async function updateSetting(item: Setting): Promise { + // Remove readonly fields + const { id, ...data } = item; + + return await api.put({ + url: `/settings/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/updateStream.ts b/frontend/src/api/backend/updateStream.ts new file mode 100644 index 0000000..508bcec --- /dev/null +++ b/frontend/src/api/backend/updateStream.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { Stream } from "./models"; + +export async function updateStream(item: Stream): Promise { + // Remove readonly fields + const { id, createdOn: _, modifiedOn: __, ...data } = item; + + return await api.put({ + url: `/nginx/streams/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/updateUser.ts b/frontend/src/api/backend/updateUser.ts new file mode 100644 index 0000000..d063e12 --- /dev/null +++ b/frontend/src/api/backend/updateUser.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; +import type { User } from "./models"; + +export async function updateUser(item: User): Promise { + // Remove readonly fields + const { id, createdOn: _, modifiedOn: __, ...data } = item; + + return await api.put({ + url: `/users/${id}`, + data: data, + }); +} diff --git a/frontend/src/api/backend/uploadCertificate.ts b/frontend/src/api/backend/uploadCertificate.ts new file mode 100644 index 0000000..b28c4e0 --- /dev/null +++ b/frontend/src/api/backend/uploadCertificate.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { Certificate } from "./models"; + +export async function uploadCertificate(id: number, data: FormData): Promise { + return await api.post({ + url: `/nginx/certificates/${id}/upload`, + data, + }); +} diff --git a/frontend/src/api/backend/validateCertificate.ts b/frontend/src/api/backend/validateCertificate.ts new file mode 100644 index 0000000..d404e51 --- /dev/null +++ b/frontend/src/api/backend/validateCertificate.ts @@ -0,0 +1,9 @@ +import * as api from "./base"; +import type { ValidatedCertificateResponse } from "./responseTypes"; + +export async function validateCertificate(data: FormData): Promise { + return await api.post({ + url: "/nginx/certificates/validate", + data, + }); +} diff --git a/frontend/src/api/backend/wireguard.ts b/frontend/src/api/backend/wireguard.ts new file mode 100644 index 0000000..9dfa844 --- /dev/null +++ b/frontend/src/api/backend/wireguard.ts @@ -0,0 +1,61 @@ +import * as api from "./base"; + +export interface WgClient { + id: number; + name: string; + enabled: boolean; + ipv4Address: string; + publicKey: string; + allowedIps: string; + persistentKeepalive: number; + createdOn: string; + updatedOn: string; + expiresAt: string | null; + latestHandshakeAt: string | null; + endpoint: string | null; + transferRx: number; + transferTx: number; +} + +export interface WgInterface { + id: number; + name: string; + publicKey: string; + ipv4Cidr: string; + listenPort: number; + mtu: number; + dns: string; + host: string; +} + +export async function getWgClients(): Promise { + return await api.get({ url: "/wireguard/client" }); +} + +export async function getWgInterface(): Promise { + return await api.get({ url: "/wireguard" }); +} + +export async function createWgClient(data: { name: string }): Promise { + return await api.post({ url: "/wireguard/client", data }); +} + +export async function deleteWgClient(id: number): Promise { + return await api.del({ url: `/wireguard/client/${id}` }); +} + +export async function enableWgClient(id: number): Promise { + return await api.post({ url: `/wireguard/client/${id}/enable` }); +} + +export async function disableWgClient(id: number): Promise { + return await api.post({ url: `/wireguard/client/${id}/disable` }); +} + +export async function getWgClientConfig(id: number): Promise { + return await api.get({ url: `/wireguard/client/${id}/configuration` }); +} + +export function downloadWgConfig(id: number, name: string) { + return api.download({ url: `/wireguard/client/${id}/configuration` }, `${name}.conf`); +} diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx new file mode 100644 index 0000000..6d02f93 --- /dev/null +++ b/frontend/src/components/Button.tsx @@ -0,0 +1,64 @@ +import cn from "classnames"; +import type { ReactNode } from "react"; + +interface Props { + children: ReactNode; + className?: string; + type?: "button" | "submit"; + actionType?: "primary" | "secondary" | "success" | "warning" | "danger" | "info" | "light" | "dark"; + variant?: "ghost" | "outline" | "pill" | "square" | "action"; + size?: "sm" | "md" | "lg" | "xl"; + fullWidth?: boolean; + isLoading?: boolean; + disabled?: boolean; + color?: + | "blue" + | "azure" + | "indigo" + | "purple" + | "pink" + | "red" + | "orange" + | "yellow" + | "lime" + | "green" + | "teal" + | "cyan"; + onClick?: () => void; +} +function Button({ + children, + className, + onClick, + type, + actionType, + variant, + size, + color, + fullWidth, + isLoading, + disabled, +}: Props) { + const myOnClick = () => { + !isLoading && onClick && onClick(); + }; + + const cns = cn( + "btn", + className, + actionType && `btn-${actionType}`, + variant && `btn-${variant}`, + size && `btn-${size}`, + color && `btn-${color}`, + fullWidth && "w-100", + isLoading && "btn-loading", + ); + + return ( + + ); +} + +export { Button }; diff --git a/frontend/src/components/EmptyData.tsx b/frontend/src/components/EmptyData.tsx new file mode 100644 index 0000000..46a9664 --- /dev/null +++ b/frontend/src/components/EmptyData.tsx @@ -0,0 +1,63 @@ +import type { Table as ReactTable } from "@tanstack/react-table"; +import cn from "classnames"; +import type { ReactNode } from "react"; +import { Button, HasPermission } from "src/components"; +import { T } from "src/locale"; +import { type ADMIN, MANAGE, type Permission, type Section } from "src/modules/Permissions"; + +interface Props { + tableInstance: ReactTable; + onNew?: () => void; + isFiltered?: boolean; + object: string; + objects: string; + color?: string; + customAddBtn?: ReactNode; + permissionSection?: Section | typeof ADMIN; + permission?: Permission; +} +function EmptyData({ + tableInstance, + onNew, + isFiltered, + object, + objects, + color = "primary", + customAddBtn, + permissionSection, + permission, +}: Props) { + return ( + + +
+ {isFiltered ? ( +

+ +

+ ) : ( + <> +

+ +

+ +

+ +

+ {customAddBtn ? ( + customAddBtn + ) : ( + + )} +
+ + )} +
+ + + ); +} + +export { EmptyData }; diff --git a/frontend/src/components/ErrorNotFound.tsx b/frontend/src/components/ErrorNotFound.tsx new file mode 100644 index 0000000..a162d1c --- /dev/null +++ b/frontend/src/components/ErrorNotFound.tsx @@ -0,0 +1,25 @@ +import { useNavigate } from "react-router-dom"; +import { Button } from "src/components"; +import { T } from "src/locale"; + +export function ErrorNotFound() { + const navigate = useNavigate(); + + return ( +
+
+

+ +

+

+ +

+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/Flag.tsx b/frontend/src/components/Flag.tsx new file mode 100644 index 0000000..420f6c8 --- /dev/null +++ b/frontend/src/components/Flag.tsx @@ -0,0 +1,24 @@ +import { IconWorld } from "@tabler/icons-react"; +import { hasFlag } from "country-flag-icons"; +// @ts-expect-error Creating a typing for a subfolder is not easily possible +import Flags from "country-flag-icons/react/3x2"; + +interface FlagProps { + className?: string; + countryCode: string; +} +function Flag({ className, countryCode }: FlagProps) { + countryCode = countryCode.toUpperCase(); + if (countryCode === "EN") { + return ; + } + + if (hasFlag(countryCode)) { + const FlagElement = Flags[countryCode] as any; + return ; + } + console.error(`No flag for country ${countryCode} found!`); + return null; +} + +export { Flag }; diff --git a/frontend/src/components/Form/AccessClientFields.tsx b/frontend/src/components/Form/AccessClientFields.tsx new file mode 100644 index 0000000..9dda8c3 --- /dev/null +++ b/frontend/src/components/Form/AccessClientFields.tsx @@ -0,0 +1,131 @@ +import { IconX } from "@tabler/icons-react"; +import cn from "classnames"; +import { useFormikContext } from "formik"; +import { useState } from "react"; +import type { AccessListClient } from "src/api/backend"; +import { intl, T } from "src/locale"; + +interface Props { + initialValues: AccessListClient[]; + name?: string; +} +export function AccessClientFields({ initialValues, name = "clients" }: Props) { + const [values, setValues] = useState(initialValues || []); + const { setFieldValue } = useFormikContext(); + + const blankClient: AccessListClient = { directive: "allow", address: "" }; + + if (values?.length === 0) { + setValues([blankClient]); + } + + const handleAdd = () => { + setValues([...values, blankClient]); + }; + + const handleRemove = (idx: number) => { + const newValues = values.filter((_: AccessListClient, i: number) => i !== idx); + if (newValues.length === 0) { + newValues.push(blankClient); + } + setValues(newValues); + setFormField(newValues); + }; + + const handleChange = (idx: number, field: string, fieldValue: string) => { + const newValues = values.map((v: AccessListClient, i: number) => + i === idx ? { ...v, [field]: fieldValue } : v, + ); + setValues(newValues); + setFormField(newValues); + }; + + const setFormField = (newValues: AccessListClient[]) => { + const filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== ""); + setFieldValue(name, filtered); + }; + + return ( + <> +

+ +

+ {values.map((client: AccessListClient, idx: number) => ( +
+
+
+ + + + handleChange(idx, "address", e.target.value)} + placeholder={intl.formatMessage({ id: "access-list.rule-source.placeholder" })} + /> +
+
+ +
+ ))} +
+ +
+
+

+ +

+
+
+ + + + +
+
+
+ + ); +} diff --git a/frontend/src/components/Form/AccessField.tsx b/frontend/src/components/Form/AccessField.tsx new file mode 100644 index 0000000..afcbd0c --- /dev/null +++ b/frontend/src/components/Form/AccessField.tsx @@ -0,0 +1,101 @@ +import { IconLock, IconLockOpen2 } from "@tabler/icons-react"; +import { Field, useFormikContext } from "formik"; +import type { ReactNode } from "react"; +import Select, { type ActionMeta, components, type OptionProps } from "react-select"; +import type { AccessList } from "src/api/backend"; +import { useLocaleState } from "src/context"; +import { useAccessLists } from "src/hooks"; +import { formatDateTime, intl, T } from "src/locale"; + +interface AccessOption { + readonly value: number; + readonly label: string; + readonly subLabel: string; + readonly icon: ReactNode; +} + +const Option = (props: OptionProps) => { + return ( + +
+
+ {props.data.icon} {props.data.label} +
+
{props.data.subLabel}
+
+
+ ); +}; + +interface Props { + id?: string; + name?: string; + label?: string; +} +export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) { + const { locale } = useLocaleState(); + const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); + const { setFieldValue } = useFormikContext(); + + const handleChange = (newValue: any, _actionMeta: ActionMeta) => { + setFieldValue(name, newValue?.value); + }; + + const options: AccessOption[] = + data?.map((item: AccessList) => ({ + value: item.id || 0, + label: item.name, + subLabel: intl.formatMessage( + { id: "access-list.subtitle" }, + { + users: item?.items?.length, + rules: item?.clients?.length, + date: item?.createdOn ? formatDateTime(item?.createdOn, locale) : "N/A", + }, + ), + icon: , + })) || []; + + // Public option + options?.unshift({ + value: 0, + label: intl.formatMessage({ id: "access-list.public" }), + subLabel: intl.formatMessage({ id: "access-list.public.subtitle" }), + icon: , + }); + + return ( + + {({ field, form }: any) => ( +
+ + {isLoading ?
: null} + {isError ?
{`${error}`}
: null} + {!isLoading && !isError ? ( + handleChange(idx, "username", e.target.value)} + /> +
+
+ iv.username === item.username).length > 0 + ? "••••••••" + : "" + } + onChange={(e) => handleChange(idx, "password", e.target.value)} + /> +
+ +
+ ))} +
+ +
+ + ); +} diff --git a/frontend/src/components/Form/DNSProviderFields.module.css b/frontend/src/components/Form/DNSProviderFields.module.css new file mode 100644 index 0000000..fdf04b0 --- /dev/null +++ b/frontend/src/components/Form/DNSProviderFields.module.css @@ -0,0 +1,8 @@ +.dnsChallengeWarning { + border: 1px solid var(--tblr-orange-lt); + padding: 1rem; + border-radius: 0.375rem; + margin-top: 1rem; + background-color: var(--tblr-cyan-lt); +} + diff --git a/frontend/src/components/Form/DNSProviderFields.tsx b/frontend/src/components/Form/DNSProviderFields.tsx new file mode 100644 index 0000000..1826548 --- /dev/null +++ b/frontend/src/components/Form/DNSProviderFields.tsx @@ -0,0 +1,132 @@ +import { IconAlertTriangle } from "@tabler/icons-react"; +import CodeEditor from "@uiw/react-textarea-code-editor"; +import { Field, useFormikContext } from "formik"; +import { useState } from "react"; +import Select, { type ActionMeta } from "react-select"; +import type { DNSProvider } from "src/api/backend"; +import { useDnsProviders } from "src/hooks"; +import { intl, T } from "src/locale"; +import styles from "./DNSProviderFields.module.css"; + +interface DNSProviderOption { + readonly value: string; + readonly label: string; + readonly credentials: string; +} + +interface Props { + showBoundaryBox?: boolean; +} +export function DNSProviderFields({ showBoundaryBox = false }: Props) { + const { values, setFieldValue } = useFormikContext(); + const { data: dnsProviders, isLoading } = useDnsProviders(); + const [dnsProviderId, setDnsProviderId] = useState(null); + + const v: any = values || {}; + + const handleChange = (newValue: any, _actionMeta: ActionMeta) => { + setFieldValue("meta.dnsProvider", newValue?.value); + setFieldValue("meta.dnsProviderCredentials", newValue?.credentials); + setDnsProviderId(newValue?.value); + }; + + const options: DNSProviderOption[] = + dnsProviders?.map((p: DNSProvider) => ({ + value: p.id, + label: p.name, + credentials: p.credentials, + })) || []; + + return ( +
+

+ + +

+ + + {({ field }: any) => ( +
+ + + + + +
+ )} +
+ + ) : null} +
+ ); +} diff --git a/frontend/src/components/Form/DomainNamesField.tsx b/frontend/src/components/Form/DomainNamesField.tsx new file mode 100644 index 0000000..bdba2f2 --- /dev/null +++ b/frontend/src/components/Form/DomainNamesField.tsx @@ -0,0 +1,85 @@ +import { Field, useFormikContext } from "formik"; +import type { ReactNode } from "react"; +import type { ActionMeta, MultiValue } from "react-select"; +import CreatableSelect from "react-select/creatable"; +import { intl, T } from "src/locale"; +import { validateDomain, validateDomains } from "src/modules/Validations"; + +type SelectOption = { + label: string; + value: string; + color?: string; +}; + +interface Props { + id?: string; + maxDomains?: number; + isWildcardPermitted?: boolean; + dnsProviderWildcardSupported?: boolean; + name?: string; + label?: string; + onChange?: (domains: string[]) => void; +} +export function DomainNamesField({ + name = "domainNames", + label = "domain-names", + id = "domainNames", + maxDomains, + isWildcardPermitted = false, + dnsProviderWildcardSupported = false, + onChange, +}: Props) { + const { setFieldValue } = useFormikContext(); + + const handleChange = (v: MultiValue, _actionMeta: ActionMeta) => { + const doms = v?.map((i: SelectOption) => { + return i.value; + }); + setFieldValue(name, doms); + onChange?.(doms); + }; + + const helperTexts: ReactNode[] = []; + if (maxDomains) { + helperTexts.push(); + } + if (!isWildcardPermitted) { + helperTexts.push(); + } else if (!dnsProviderWildcardSupported) { + helperTexts.push(); + } + + return ( + + {({ field, form }: any) => ( +
+ + ({ label: d, value: d }))} + /> + {form.errors[field.name] && form.touched[field.name] ? ( + {form.errors[field.name]} + ) : helperTexts.length ? ( + helperTexts.map((i, idx) => ( + + {i} + + )) + ) : null} +
+ )} +
+ ); +} diff --git a/frontend/src/components/Form/LocationsFields.module.css b/frontend/src/components/Form/LocationsFields.module.css new file mode 100644 index 0000000..4b48ef3 --- /dev/null +++ b/frontend/src/components/Form/LocationsFields.module.css @@ -0,0 +1,3 @@ +.locationCard { + border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important; +} diff --git a/frontend/src/components/Form/LocationsFields.tsx b/frontend/src/components/Form/LocationsFields.tsx new file mode 100644 index 0000000..4240b1f --- /dev/null +++ b/frontend/src/components/Form/LocationsFields.tsx @@ -0,0 +1,185 @@ +import { IconSettings } from "@tabler/icons-react"; +import CodeEditor from "@uiw/react-textarea-code-editor"; +import cn from "classnames"; +import { useFormikContext } from "formik"; +import { useState } from "react"; +import type { ProxyLocation } from "src/api/backend"; +import { intl, T } from "src/locale"; +import styles from "./LocationsFields.module.css"; + +interface Props { + initialValues: ProxyLocation[]; + name?: string; +} +export function LocationsFields({ initialValues, name = "locations" }: Props) { + const [values, setValues] = useState(initialValues || []); + const { setFieldValue } = useFormikContext(); + const [advVisible, setAdvVisible] = useState([]); + + const blankItem: ProxyLocation = { + path: "", + advancedConfig: "", + forwardScheme: "http", + forwardHost: "", + forwardPort: 80, + }; + + const toggleAdvVisible = (idx: number) => { + setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]); + }; + + const handleAdd = () => { + setValues([...values, blankItem]); + }; + + const handleRemove = (idx: number) => { + const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx); + setValues(newValues); + setFormField(newValues); + }; + + const handleChange = (idx: number, field: string, fieldValue: string) => { + const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v)); + setValues(newValues); + setFormField(newValues); + }; + + const setFormField = (newValues: ProxyLocation[]) => { + const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== ""); + setFieldValue(name, filtered); + }; + + if (values.length === 0) { + return ( +
+ +
+ ); + } + + return ( + <> + {values.map((item: ProxyLocation, idx: number) => ( +
+
+
+
+
+ Location + handleChange(idx, "path", e.target.value)} + /> +
+
+
+ +
+
+
+
+
+ + +
+
+
+
+ + handleChange(idx, "forwardHost", e.target.value)} + /> +
+
+
+
+ + handleChange(idx, "forwardPort", e.target.value)} + /> +
+
+
+ {advVisible.includes(idx) && ( +
+ handleChange(idx, "advancedConfig", e.target.value)} + style={{ + fontFamily: + "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + borderRadius: "0.3rem", + minHeight: "170px", + }} + /> +
+ )} + +
+
+ ))} +
+ +
+ + ); +} diff --git a/frontend/src/components/Form/NginxConfigField.tsx b/frontend/src/components/Form/NginxConfigField.tsx new file mode 100644 index 0000000..208a384 --- /dev/null +++ b/frontend/src/components/Form/NginxConfigField.tsx @@ -0,0 +1,41 @@ +import CodeEditor from "@uiw/react-textarea-code-editor"; +import { Field } from "formik"; +import { intl, T } from "src/locale"; + +interface Props { + id?: string; + name?: string; + label?: string; +} +export function NginxConfigField({ + name = "advancedConfig", + label = "nginx-config.label", + id = "advancedConfig", +}: Props) { + return ( + + {({ field }: any) => ( +
+ + +
+ )} +
+ ); +} diff --git a/frontend/src/components/Form/SSLCertificateField.tsx b/frontend/src/components/Form/SSLCertificateField.tsx new file mode 100644 index 0000000..6ab3ea9 --- /dev/null +++ b/frontend/src/components/Form/SSLCertificateField.tsx @@ -0,0 +1,140 @@ +import { IconShield } from "@tabler/icons-react"; +import { Field, useFormikContext } from "formik"; +import Select, { type ActionMeta, components, type OptionProps } from "react-select"; +import type { Certificate } from "src/api/backend"; +import { useLocaleState } from "src/context"; +import { useCertificates } from "src/hooks"; +import { formatDateTime, intl, T } from "src/locale"; + +interface CertOption { + readonly value: number | "new"; + readonly label: string; + readonly subLabel: string; + readonly icon: React.ReactNode; +} + +const Option = (props: OptionProps) => { + return ( + +
+
+ {props.data.icon} {props.data.label} +
+
{props.data.subLabel}
+
+
+ ); +}; + +interface Props { + id?: string; + name?: string; + label?: string; + required?: boolean; + allowNew?: boolean; + forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields +} +export function SSLCertificateField({ + name = "certificateId", + label = "ssl-certificate", + id = "certificateId", + required, + allowNew, + forHttp = true, +}: Props) { + const { locale } = useLocaleState(); + const { isLoading, isError, error, data } = useCertificates(); + const { values, setFieldValue } = useFormikContext(); + const v: any = values || {}; + + const handleChange = (newValue: any, _actionMeta: ActionMeta) => { + setFieldValue(name, newValue?.value); + const { + sslForced, + http2Support, + hstsEnabled, + hstsSubdomains, + dnsChallenge, + dnsProvider, + dnsProviderCredentials, + propagationSeconds, + } = v; + if (forHttp && !newValue?.value) { + sslForced && setFieldValue("sslForced", false); + http2Support && setFieldValue("http2Support", false); + hstsEnabled && setFieldValue("hstsEnabled", false); + hstsSubdomains && setFieldValue("hstsSubdomains", false); + } + if (newValue?.value !== "new") { + dnsChallenge && setFieldValue("dnsChallenge", undefined); + dnsProvider && setFieldValue("dnsProvider", undefined); + dnsProviderCredentials && setFieldValue("dnsProviderCredentials", undefined); + propagationSeconds && setFieldValue("propagationSeconds", undefined); + } + }; + + const options: CertOption[] = + data?.map((cert: Certificate) => ({ + value: cert.id, + label: cert.niceName, + subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn, locale) : "N/A" })}`, + icon: , + })) || []; + + // Prepend the Add New option + if (allowNew) { + options?.unshift({ + value: "new", + label: intl.formatMessage({ id: "certificates.request.title" }), + subLabel: intl.formatMessage({ id: "certificates.request.subtitle" }), + icon: , + }); + } + + // Prepend the None option + if (!required) { + options?.unshift({ + value: 0, + label: intl.formatMessage({ id: "certificate.none.title" }), + subLabel: forHttp + ? intl.formatMessage({ id: "certificate.none.subtitle.for-http" }) + : intl.formatMessage({ id: "certificate.none.subtitle" }), + icon: , + }); + } + + return ( + + {({ field, form }: any) => ( +
+ + {isLoading ?
: null} + {isError ?
{`${error}`}
: null} + {!isLoading && !isError ? ( + handleToggleChange(e, field.name)} + disabled={!hasCertificate} + /> + + + + + )} + +
+
+ + {({ field }: any) => ( + + )} + +
+
+
+
+ + {({ field }: any) => ( + + )} + +
+
+ + {({ field }: any) => ( + + )} + +
+
+ + ); + + const getHttpAdvancedOptions = () =>( +
+
+ +
+
+ + {({ field }: any) => ( + + )} + +
+
+
+
+ ); + + return ( +
+ {forHttp ? getHttpOptions() : null} + {newCertificate ? ( + <> + + {({ field }: any) => ( + + )} + + {requireDomainNames ? : null} + {dnsChallenge ? : null} + + ) : null} + {forProxyHost && forHttp ? getHttpAdvancedOptions() : null} +
+ ); +} diff --git a/frontend/src/components/Form/index.ts b/frontend/src/components/Form/index.ts new file mode 100644 index 0000000..f218b2e --- /dev/null +++ b/frontend/src/components/Form/index.ts @@ -0,0 +1,9 @@ +export * from "./AccessClientFields"; +export * from "./AccessField"; +export * from "./BasicAuthFields"; +export * from "./DNSProviderFields"; +export * from "./DomainNamesField"; +export * from "./LocationsFields"; +export * from "./NginxConfigField"; +export * from "./SSLCertificateField"; +export * from "./SSLOptionsFields"; diff --git a/frontend/src/components/HasPermission.tsx b/frontend/src/components/HasPermission.tsx new file mode 100644 index 0000000..c038066 --- /dev/null +++ b/frontend/src/components/HasPermission.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from "react"; +import Alert from "react-bootstrap/Alert"; +import { Loading, LoadingPage } from "src/components"; +import { useUser } from "src/hooks"; +import { T } from "src/locale"; +import { type ADMIN, hasPermission, type Permission, type Section } from "src/modules/Permissions"; + +interface Props { + section?: Section | typeof ADMIN; + permission: Permission; + hideError?: boolean; + children?: ReactNode; + pageLoading?: boolean; + loadingNoLogo?: boolean; +} +function HasPermission({ + section, + permission, + children, + hideError = false, + pageLoading = false, + loadingNoLogo = false, +}: Props) { + const { data, isLoading } = useUser("me"); + + if (!section) { + return <>{children}; + } + + if (isLoading) { + if (hideError) { + return null; + } + if (pageLoading) { + return ; + } + return ; + } + + const allowed = hasPermission(section, permission, data?.permissions, data?.roles); + if (allowed) { + return <>{children}; + } + + return !hideError ? ( + + + + ) : null; +} + +export { HasPermission }; diff --git a/frontend/src/components/Loading.module.css b/frontend/src/components/Loading.module.css new file mode 100644 index 0000000..7a13ded --- /dev/null +++ b/frontend/src/components/Loading.module.css @@ -0,0 +1,3 @@ +.logo { + max-height: 100px; +} diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx new file mode 100644 index 0000000..14d96e6 --- /dev/null +++ b/frontend/src/components/Loading.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from "react"; +import { T } from "src/locale"; +import styles from "./Loading.module.css"; + +interface Props { + label?: string | ReactNode; + noLogo?: boolean; +} +export function Loading({ label, noLogo }: Props) { + return ( +
+ {noLogo ? null : ( +
+ +
+ )} +
{label || }
+
+
+
+
+ ); +} diff --git a/frontend/src/components/LoadingPage.tsx b/frontend/src/components/LoadingPage.tsx new file mode 100644 index 0000000..7d3bec1 --- /dev/null +++ b/frontend/src/components/LoadingPage.tsx @@ -0,0 +1,15 @@ +import { Loading, Page } from "src/components"; + +interface Props { + label?: string; + noLogo?: boolean; +} +export function LoadingPage({ label, noLogo }: Props) { + return ( + +
+ +
+
+ ); +} diff --git a/frontend/src/components/LocalePicker.module.css b/frontend/src/components/LocalePicker.module.css new file mode 100644 index 0000000..6f39e7f --- /dev/null +++ b/frontend/src/components/LocalePicker.module.css @@ -0,0 +1,8 @@ +.btn { + color: light-dark(var(--tblr-dark), var(--tblr-light)) !important; + + &:hover { + border: var(--tblr-btn-border-width) solid transparent !important; + background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; + } +} diff --git a/frontend/src/components/LocalePicker.tsx b/frontend/src/components/LocalePicker.tsx new file mode 100644 index 0000000..2d7dbb8 --- /dev/null +++ b/frontend/src/components/LocalePicker.tsx @@ -0,0 +1,53 @@ +import cn from "classnames"; +import { Flag } from "src/components"; +import { useLocaleState } from "src/context"; +import { useTheme } from "src/hooks"; +import { changeLocale, getFlagCodeForLocale, localeOptions, T } from "src/locale"; +import styles from "./LocalePicker.module.css"; + +interface Props { + menuAlign?: "start" | "end"; +} + +function LocalePicker({ menuAlign = "start" }: Props) { + const { locale, setLocale } = useLocaleState(); + const { getTheme } = useTheme(); + + const changeTo = (lang: string) => { + changeLocale(lang); + setLocale(lang); + location.reload(); + }; + + const classes = ["btn", "dropdown-toggle", "btn-sm", styles.btn]; + const cns = cn(...classes, getTheme() === "dark" ? "btn-ghost-dark" : "btn-ghost-light"); + + return ( +
+ +
+ {localeOptions.map((item: any) => ( + { + e.preventDefault(); + changeTo(item[0]); + }} + > + + + ))} +
+
+ ); +} + +export { LocalePicker }; diff --git a/frontend/src/components/NavLink.tsx b/frontend/src/components/NavLink.tsx new file mode 100644 index 0000000..b6a414a --- /dev/null +++ b/frontend/src/components/NavLink.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; + +interface Props { + children: React.ReactNode; + to?: string; + isDropdownItem?: boolean; + onClick?: () => void; +} +export function NavLink({ children, to, isDropdownItem, onClick }: Props) { + const navigate = useNavigate(); + + return ( + { + e.preventDefault(); + if (onClick) { + onClick(); + } + if (to) { + navigate(to); + } + }} + > + {children} + + ); +} diff --git a/frontend/src/components/Page.module.css b/frontend/src/components/Page.module.css new file mode 100644 index 0000000..00086bb --- /dev/null +++ b/frontend/src/components/Page.module.css @@ -0,0 +1,5 @@ +.page { + display: grid; + grid-template-rows: auto 1fr auto; /* Header, Main Content, Footer */ + min-height: 100vh; +} diff --git a/frontend/src/components/Page.tsx b/frontend/src/components/Page.tsx new file mode 100644 index 0000000..a96a6b1 --- /dev/null +++ b/frontend/src/components/Page.tsx @@ -0,0 +1,10 @@ +import cn from "classnames"; +import styles from "./Page.module.css"; + +interface Props { + children: React.ReactNode; + className?: string; +} +export function Page({ children, className }: Props) { + return
{children}
; +} diff --git a/frontend/src/components/SiteContainer.tsx b/frontend/src/components/SiteContainer.tsx new file mode 100644 index 0000000..01a9cb5 --- /dev/null +++ b/frontend/src/components/SiteContainer.tsx @@ -0,0 +1,6 @@ +interface Props { + children: React.ReactNode; +} +export function SiteContainer({ children }: Props) { + return
{children}
; +} diff --git a/frontend/src/components/SiteFooter.tsx b/frontend/src/components/SiteFooter.tsx new file mode 100644 index 0000000..57bb219 --- /dev/null +++ b/frontend/src/components/SiteFooter.tsx @@ -0,0 +1,78 @@ +import { useCheckVersion, useHealth } from "src/hooks"; +import { T } from "src/locale"; + +export function SiteFooter() { + const health = useHealth(); + const { data: versionData } = useCheckVersion(); + + const getVersion = () => { + if (!health.data) { + return ""; + } + const v = health.data.version; + return `v${v.major}.${v.minor}.${v.revision}`; + }; + + return ( + + ); +} diff --git a/frontend/src/components/SiteHeader.module.css b/frontend/src/components/SiteHeader.module.css new file mode 100644 index 0000000..a88f81c --- /dev/null +++ b/frontend/src/components/SiteHeader.module.css @@ -0,0 +1,8 @@ +.logo { + font-size: 1.1rem; + font-weight: 500; + + img { + margin-right: 0.8rem; + } +} diff --git a/frontend/src/components/SiteHeader.tsx b/frontend/src/components/SiteHeader.tsx new file mode 100644 index 0000000..f00d38d --- /dev/null +++ b/frontend/src/components/SiteHeader.tsx @@ -0,0 +1,141 @@ +import { IconLock, IconLogout, IconShieldLock, IconUser } from "@tabler/icons-react"; +import { LocalePicker, NavLink, ThemeSwitcher } from "src/components"; +import { useAuthState } from "src/context"; +import { useUser } from "src/hooks"; +import { T } from "src/locale"; +import { showChangePasswordModal, showTwoFactorModal, showUserModal } from "src/modals"; +import styles from "./SiteHeader.module.css"; + +export function SiteHeader() { + const { data: currentUser } = useUser("me"); + const isAdmin = currentUser?.roles.includes("admin"); + const { logout } = useAuthState(); + + return ( +
+
+ +
+ +
+ Logo +
+ Nginx Proxy Manager +
+
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
{currentUser?.nickname}
+
+ +
+
+
+
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: This div is not interactive. */} +
e.stopPropagation()}> +
+
{currentUser?.nickname}
+
+ +
+
+
+ + +
+
+
+
+ { + e.preventDefault(); + showUserModal("me"); + }} + > + + + + { + e.preventDefault(); + showChangePasswordModal("me"); + }} + > + + + + { + e.preventDefault(); + showTwoFactorModal("me"); + }} + > + + + + +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/SiteMenu.tsx b/frontend/src/components/SiteMenu.tsx new file mode 100644 index 0000000..815281a --- /dev/null +++ b/frontend/src/components/SiteMenu.tsx @@ -0,0 +1,213 @@ +import { + IconBook, + IconDeviceDesktop, + IconHome, + IconLock, + IconNetwork, + IconSettings, + IconShield, + IconUser, +} from "@tabler/icons-react"; +import cn from "classnames"; +import React from "react"; +import { HasPermission, NavLink } from "src/components"; +import { T } from "src/locale"; +import { + ACCESS_LISTS, + ADMIN, + CERTIFICATES, + DEAD_HOSTS, + type MANAGE, + PROXY_HOSTS, + REDIRECTION_HOSTS, + type Section, + STREAMS, + VIEW, +} from "src/modules/Permissions"; + +interface MenuItem { + label: string; + icon?: React.ElementType; + to?: string; + items?: MenuItem[]; + permissionSection?: Section | typeof ADMIN; + permission?: typeof VIEW | typeof MANAGE; +} + +const menuItems: MenuItem[] = [ + { + to: "/", + icon: IconHome, + label: "dashboard", + }, + { + icon: IconDeviceDesktop, + label: "hosts", + items: [ + { + to: "/nginx/proxy", + label: "proxy-hosts", + permissionSection: PROXY_HOSTS, + permission: VIEW, + }, + { + to: "/nginx/redirection", + label: "redirection-hosts", + permissionSection: REDIRECTION_HOSTS, + permission: VIEW, + }, + { + to: "/nginx/stream", + label: "streams", + permissionSection: STREAMS, + permission: VIEW, + }, + { + to: "/nginx/404", + label: "dead-hosts", + permissionSection: DEAD_HOSTS, + permission: VIEW, + }, + ], + }, + { + to: "/access", + icon: IconLock, + label: "access-lists", + permissionSection: ACCESS_LISTS, + permission: VIEW, + }, + { + to: "/certificates", + icon: IconShield, + label: "certificates", + permissionSection: CERTIFICATES, + permission: VIEW, + }, + { + to: "/wireguard", + icon: IconNetwork, + label: "wireguard", + }, + { + to: "/users", + icon: IconUser, + label: "users", + permissionSection: ADMIN, + }, + { + to: "/audit-log", + icon: IconBook, + label: "auditlogs", + permissionSection: ADMIN, + }, + { + to: "/settings", + icon: IconSettings, + label: "settings", + permissionSection: ADMIN, + }, +]; + +const getMenuItem = (item: MenuItem, onClick?: () => void) => { + if (item.items && item.items.length > 0) { + return getMenuDropown(item, onClick); + } + + return ( + +
  • + + + {item.icon && React.createElement(item.icon, { height: 24, width: 24 })} + + + + + +
  • +
    + ); +}; + +const getMenuDropown = (item: MenuItem, onClick?: () => void) => { + const cns = cn("nav-item", "dropdown"); + return ( + +
  • + +
    + {item.items?.map((subitem, idx) => { + return ( + + + + + + ); + })} +
    +
  • +
    + ); +}; + +export function SiteMenu() { + const closeMenu = () => setTimeout(() => { + const navbarToggler = document.querySelector(".navbar-toggler"); + const navbarMenu = document.querySelector("#navbar-menu"); + if (navbarToggler && navbarMenu?.classList.contains("show")) { + navbarToggler.click(); + } + }, 300); + + return ( +
    + +
    + ); +} diff --git a/frontend/src/components/Table/EmptyRow.tsx b/frontend/src/components/Table/EmptyRow.tsx new file mode 100644 index 0000000..2daee95 --- /dev/null +++ b/frontend/src/components/Table/EmptyRow.tsx @@ -0,0 +1,16 @@ +import type { Table as ReactTable } from "@tanstack/react-table"; + +interface Props { + tableInstance: ReactTable; +} +function EmptyRow({ tableInstance }: Props) { + return ( + + +

    There are no items

    + + + ); +} + +export { EmptyRow }; diff --git a/frontend/src/components/Table/Formatter/AccessListformatter.tsx b/frontend/src/components/Table/Formatter/AccessListformatter.tsx new file mode 100644 index 0000000..b5830fd --- /dev/null +++ b/frontend/src/components/Table/Formatter/AccessListformatter.tsx @@ -0,0 +1,24 @@ +import type { AccessList } from "src/api/backend"; +import { T } from "src/locale"; +import { showAccessListModal } from "src/modals"; + +interface Props { + access?: AccessList; +} +export function AccessListFormatter({ access }: Props) { + if (!access) { + return ; + } + return ( + + ); +} diff --git a/frontend/src/components/Table/Formatter/CertificateFormatter.tsx b/frontend/src/components/Table/Formatter/CertificateFormatter.tsx new file mode 100644 index 0000000..6b791bf --- /dev/null +++ b/frontend/src/components/Table/Formatter/CertificateFormatter.tsx @@ -0,0 +1,18 @@ +import type { Certificate } from "src/api/backend"; +import { T } from "src/locale"; + +interface Props { + certificate?: Certificate; +} +export function CertificateFormatter({ certificate }: Props) { + let translation = "http-only"; + if (certificate) { + translation = certificate.provider; + if (translation === "letsencrypt") { + translation = "lets-encrypt"; + } else if (translation === "other") { + translation = "certificates.custom"; + } + } + return ; +} diff --git a/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx b/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx new file mode 100644 index 0000000..a49374d --- /dev/null +++ b/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx @@ -0,0 +1,82 @@ +import OverlayTrigger from "react-bootstrap/OverlayTrigger"; +import Popover from "react-bootstrap/Popover"; +import type { DeadHost, ProxyHost, RedirectionHost, Stream } from "src/api/backend"; +import { TrueFalseFormatter } from "src/components"; +import { T } from "src/locale"; + +const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => { + if (items.length === 0) { + return null; + } + return ( + <> +
    + + + +
    + {items.map((host) => ( +
    + {host.domainNames.join(", ")} +
    + ))} + + ); +}; + +const getSectionStream = (items: Stream[]) => { + if (items.length === 0) { + return null; + } + return ( + <> +
    + + + +
    + {items.map((stream) => ( +
    + {stream.forwardingHost}:{stream.forwardingPort} +
    + ))} + + ); +}; + +interface Props { + proxyHosts: ProxyHost[]; + redirectionHosts: RedirectionHost[]; + deadHosts: DeadHost[]; + streams: Stream[]; +} +export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts, streams }: Props) { + const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length + streams?.length; + if (totalCount === 0) { + return ; + } + + proxyHosts.sort(); + redirectionHosts.sort(); + deadHosts.sort(); + streams.sort(); + + const popover = ( + + + {getSection("proxy-hosts", proxyHosts)} + {getSection("redirection-hosts", redirectionHosts)} + {getSection("dead-hosts", deadHosts)} + {getSectionStream(streams)} + + + ); + + return ( + +
    + +
    +
    + ); +} diff --git a/frontend/src/components/Table/Formatter/DateFormatter.tsx b/frontend/src/components/Table/Formatter/DateFormatter.tsx new file mode 100644 index 0000000..830b06e --- /dev/null +++ b/frontend/src/components/Table/Formatter/DateFormatter.tsx @@ -0,0 +1,21 @@ +import cn from "classnames"; +import { differenceInDays, isPast } from "date-fns"; +import { useLocaleState } from "src/context"; +import { formatDateTime, parseDate } from "src/locale"; + +interface Props { + value: string; + highlightPast?: boolean; + highlistNearlyExpired?: boolean; +} +export function DateFormatter({ value, highlightPast, highlistNearlyExpired }: Props) { + const { locale } = useLocaleState(); + const d = parseDate(value); + const dateIsPast = d ? isPast(d) : false; + const days = d ? differenceInDays(d, new Date()) : 0; + const cl = cn({ + "text-danger": highlightPast && dateIsPast, + "text-warning": highlistNearlyExpired && !dateIsPast && days <= 30 && days >= 0, + }); + return {formatDateTime(value, locale)}; +} diff --git a/frontend/src/components/Table/Formatter/DomainsFormatter.tsx b/frontend/src/components/Table/Formatter/DomainsFormatter.tsx new file mode 100644 index 0000000..35e8393 --- /dev/null +++ b/frontend/src/components/Table/Formatter/DomainsFormatter.tsx @@ -0,0 +1,73 @@ +import cn from "classnames"; +import type { ReactNode } from "react"; +import { useLocaleState } from "src/context"; +import { formatDateTime, T } from "src/locale"; + +interface Props { + domains: string[]; + createdOn?: string; + niceName?: string; + provider?: string; + color?: string; +} + +const DomainLink = ({ domain, color }: { domain?: string; color?: string }) => { + // when domain contains a wildcard, make the link go nowhere. + // Apparently the domain can be null or undefined sometimes. + // This try is just a safeguard to prevent the whole formatter from breaking. + if (!domain) return null; + try { + let onClick: ((e: React.MouseEvent) => void) | undefined; + if (domain.includes("*")) { + onClick = (e: React.MouseEvent) => e.preventDefault(); + } + return ( + + {domain} + + ); + } catch { + return null; + } +}; + +export function DomainsFormatter({ domains, createdOn, niceName, provider, color }: Props) { + const { locale } = useLocaleState(); + const elms: ReactNode[] = []; + + if ((!domains || domains.length === 0) && !niceName) { + elms.push( + + Unknown + , + ); + } + if (!domains || (niceName && provider !== "letsencrypt")) { + elms.push( + + {niceName} + , + ); + } + + if (domains) { + domains.map((domain: string) => elms.push()); + } + + return ( +
    +
    {...elms}
    + {createdOn ? ( +
    + +
    + ) : null} +
    + ); +} diff --git a/frontend/src/components/Table/Formatter/EmailFormatter.tsx b/frontend/src/components/Table/Formatter/EmailFormatter.tsx new file mode 100644 index 0000000..3371c66 --- /dev/null +++ b/frontend/src/components/Table/Formatter/EmailFormatter.tsx @@ -0,0 +1,10 @@ +interface Props { + email: string; +} +export function EmailFormatter({ email }: Props) { + return ( + + {email} + + ); +} diff --git a/frontend/src/components/Table/Formatter/EventFormatter.tsx b/frontend/src/components/Table/Formatter/EventFormatter.tsx new file mode 100644 index 0000000..1220fa0 --- /dev/null +++ b/frontend/src/components/Table/Formatter/EventFormatter.tsx @@ -0,0 +1,81 @@ +import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react"; +import cn from "classnames"; +import type { AuditLog } from "src/api/backend"; +import { useLocaleState } from "src/context"; +import { formatDateTime, T } from "src/locale"; + +const getEventValue = (event: AuditLog) => { + switch (event.objectType) { + case "access-list": + case "user": + return event.meta?.name; + case "proxy-host": + case "redirection-host": + case "dead-host": + return event.meta?.domainNames?.join(", ") || "N/A"; + case "stream": + return event.meta?.incomingPort || "N/A"; + case "certificate": + return event.meta?.domainNames?.join(", ") || event.meta?.niceName || "N/A"; + default: + return `UNKNOWN EVENT TYPE: ${event.objectType}`; + } +}; + +const getColorForAction = (action: string) => { + switch (action) { + case "created": + return "text-lime"; + case "deleted": + return "text-red"; + default: + return "text-blue"; + } +}; + +const getIcon = (row: AuditLog) => { + const c = cn(getColorForAction(row.action), "me-1"); + let ico = null; + switch (row.objectType) { + case "user": + ico = ; + break; + case "proxy-host": + ico = ; + break; + case "redirection-host": + ico = ; + break; + case "dead-host": + ico = ; + break; + case "stream": + ico = ; + break; + case "access-list": + ico = ; + break; + case "certificate": + ico = ; + break; + } + + return ico; +}; + +interface Props { + row: AuditLog; +} +export function EventFormatter({ row }: Props) { + const { locale } = useLocaleState(); + return ( +
    +
    + {getIcon(row)} + +   — {getEventValue(row)} +
    +
    {formatDateTime(row.createdOn, locale)}
    +
    + ); +} diff --git a/frontend/src/components/Table/Formatter/GravatarFormatter.tsx b/frontend/src/components/Table/Formatter/GravatarFormatter.tsx new file mode 100644 index 0000000..58b820b --- /dev/null +++ b/frontend/src/components/Table/Formatter/GravatarFormatter.tsx @@ -0,0 +1,19 @@ +const defaultImg = "/images/default-avatar.jpg"; + +interface Props { + url?: string; + name?: string; +} +export function GravatarFormatter({ url, name }: Props) { + return ( +
    + +
    + ); +} diff --git a/frontend/src/components/Table/Formatter/RolesFormatter.tsx b/frontend/src/components/Table/Formatter/RolesFormatter.tsx new file mode 100644 index 0000000..2e02560 --- /dev/null +++ b/frontend/src/components/Table/Formatter/RolesFormatter.tsx @@ -0,0 +1,20 @@ +import { T } from "src/locale"; + +interface Props { + roles: string[]; +} +export function RolesFormatter({ roles }: Props) { + const r = roles || []; + if (r.length === 0) { + r[0] = "standard-user"; + } + return ( + <> + {r.map((role: string) => ( + + + + ))} + + ); +} diff --git a/frontend/src/components/Table/Formatter/TrueFalseFormatter.tsx b/frontend/src/components/Table/Formatter/TrueFalseFormatter.tsx new file mode 100644 index 0000000..6333950 --- /dev/null +++ b/frontend/src/components/Table/Formatter/TrueFalseFormatter.tsx @@ -0,0 +1,24 @@ +import cn from "classnames"; +import { T } from "src/locale"; + +interface Props { + value: boolean; + trueLabel?: string; + trueColor?: string; + falseLabel?: string; + falseColor?: string; +} +export function TrueFalseFormatter({ + value, + trueLabel = "enabled", + trueColor = "lime", + falseLabel = "disabled", + falseColor = "red", +}: Props) { + return ( + + + + + ); +} diff --git a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx new file mode 100644 index 0000000..d83ccbf --- /dev/null +++ b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx @@ -0,0 +1,23 @@ +import { useLocaleState } from "src/context"; +import { formatDateTime, T } from "src/locale"; + +interface Props { + value: string; + createdOn?: string; + disabled?: boolean; +} +export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) { + const { locale } = useLocaleState(); + return ( +
    +
    +
    {value}
    +
    + {createdOn ? ( +
    + +
    + ) : null} +
    + ); +} diff --git a/frontend/src/components/Table/Formatter/index.ts b/frontend/src/components/Table/Formatter/index.ts new file mode 100644 index 0000000..04a4719 --- /dev/null +++ b/frontend/src/components/Table/Formatter/index.ts @@ -0,0 +1,11 @@ +export * from "./AccessListformatter"; +export * from "./CertificateFormatter"; +export * from "./CertificateInUseFormatter"; +export * from "./DateFormatter"; +export * from "./DomainsFormatter"; +export * from "./EmailFormatter"; +export * from "./EventFormatter"; +export * from "./GravatarFormatter"; +export * from "./RolesFormatter"; +export * from "./TrueFalseFormatter"; +export * from "./ValueWithDateFormatter"; diff --git a/frontend/src/components/Table/TableBody.tsx b/frontend/src/components/Table/TableBody.tsx new file mode 100644 index 0000000..d8fe9bf --- /dev/null +++ b/frontend/src/components/Table/TableBody.tsx @@ -0,0 +1,37 @@ +import { flexRender } from "@tanstack/react-table"; +import type { TableLayoutProps } from "src/components"; +import { EmptyRow } from "./EmptyRow"; + +function TableBody(props: TableLayoutProps) { + const { tableInstance, extraStyles, emptyState } = props; + const rows = tableInstance.getRowModel().rows; + + if (rows.length === 0) { + return ( + + {emptyState ? emptyState : } + + ); + } + + return ( + + {rows.map((row: any) => { + return ( + + {row.getVisibleCells().map((cell: any) => { + const { className } = (cell.column.columnDef.meta as any) ?? {}; + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + ); + })} + + ); +} + +export { TableBody }; diff --git a/frontend/src/components/Table/TableHeader.tsx b/frontend/src/components/Table/TableHeader.tsx new file mode 100644 index 0000000..bf627ce --- /dev/null +++ b/frontend/src/components/Table/TableHeader.tsx @@ -0,0 +1,26 @@ +import type { TableLayoutProps } from "src/components"; + +function TableHeader(props: TableLayoutProps) { + const { tableInstance } = props; + const headerGroups = tableInstance.getHeaderGroups(); + + return ( + + {headerGroups.map((headerGroup: any) => ( + + {headerGroup.headers.map((header: any) => { + const { column } = header; + const { className } = (column.columnDef.meta as any) ?? {}; + return ( + + {typeof column.columnDef.header === "string" ? `${column.columnDef.header}` : null} + + ); + })} + + ))} + + ); +} + +export { TableHeader }; diff --git a/frontend/src/components/Table/TableHelpers.ts b/frontend/src/components/Table/TableHelpers.ts new file mode 100644 index 0000000..0df329c --- /dev/null +++ b/frontend/src/components/Table/TableHelpers.ts @@ -0,0 +1,64 @@ +export interface TablePagination { + limit: number; + offset: number; + total: number; +} + +export interface TableSortBy { + id: string; + desc: boolean; +} + +export interface TableFilter { + id: string; + value: any; +} + +const tableEvents = { + FILTERS_CHANGED: "FILTERS_CHANGED", + PAGE_CHANGED: "PAGE_CHANGED", + PAGE_SIZE_CHANGED: "PAGE_SIZE_CHANGED", + TOTAL_COUNT_CHANGED: "TOTAL_COUNT_CHANGED", + SORT_CHANGED: "SORT_CHANGED", +}; + +const tableEventReducer = (state: any, { type, payload }: any) => { + let offset = state.offset; + switch (type) { + case tableEvents.PAGE_CHANGED: + return { + ...state, + offset: payload * state.limit, + }; + case tableEvents.PAGE_SIZE_CHANGED: + return { + ...state, + limit: payload, + }; + case tableEvents.TOTAL_COUNT_CHANGED: + return { + ...state, + total: payload, + }; + case tableEvents.SORT_CHANGED: + return { + ...state, + sortBy: payload, + }; + case tableEvents.FILTERS_CHANGED: + if (state.filters !== payload) { + // this actually was a legit change + // sets to page 1 when filter is modified + offset = 0; + } + return { + ...state, + filters: payload, + offset, + }; + default: + throw new Error(`Unhandled action type: ${type}`); + } +}; + +export { tableEvents, tableEventReducer }; diff --git a/frontend/src/components/Table/TableLayout.tsx b/frontend/src/components/Table/TableLayout.tsx new file mode 100644 index 0000000..6303409 --- /dev/null +++ b/frontend/src/components/Table/TableLayout.tsx @@ -0,0 +1,24 @@ +import type { Table as ReactTable } from "@tanstack/react-table"; +import { TableBody } from "./TableBody"; +import { TableHeader } from "./TableHeader"; + +interface TableLayoutProps { + tableInstance: ReactTable; + emptyState?: React.ReactNode; + extraStyles?: { + row: (rowData: TFields) => any | undefined; + }; +} +function TableLayout(props: TableLayoutProps) { + const hasRows = props.tableInstance.getRowModel().rows.length > 0; + return ( +
    + + {hasRows ? : null} + +
    +
    + ); +} + +export { TableLayout, type TableLayoutProps }; diff --git a/frontend/src/components/Table/index.ts b/frontend/src/components/Table/index.ts new file mode 100644 index 0000000..d471d45 --- /dev/null +++ b/frontend/src/components/Table/index.ts @@ -0,0 +1,4 @@ +export * from "./Formatter"; +export * from "./TableHeader"; +export * from "./TableHelpers"; +export * from "./TableLayout"; diff --git a/frontend/src/components/ThemeSwitcher.module.css b/frontend/src/components/ThemeSwitcher.module.css new file mode 100644 index 0000000..2233d2f --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.module.css @@ -0,0 +1,15 @@ +.darkBtn { + color: var(--tblr-light) !important; + &:hover { + border: var(--tblr-btn-border-width) solid transparent !important; + background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; + } +} + +.lightBtn { + color: var(--tblr-dark) !important; + &:hover { + border: var(--tblr-btn-border-width) solid transparent !important; + background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; + } +} diff --git a/frontend/src/components/ThemeSwitcher.tsx b/frontend/src/components/ThemeSwitcher.tsx new file mode 100644 index 0000000..9cb6697 --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.tsx @@ -0,0 +1,41 @@ +import { IconMoon, IconSun } from "@tabler/icons-react"; +import cn from "classnames"; +import { Button } from "src/components"; +import { useTheme } from "src/hooks"; +import styles from "./ThemeSwitcher.module.css"; + +interface Props { + className?: string; +} +function ThemeSwitcher({ className }: Props) { + const { setTheme } = useTheme(); + + return ( +
    + + +
    + ); +} + +export { ThemeSwitcher }; diff --git a/frontend/src/components/Unhealthy.tsx b/frontend/src/components/Unhealthy.tsx new file mode 100644 index 0000000..d4b6e33 --- /dev/null +++ b/frontend/src/components/Unhealthy.tsx @@ -0,0 +1,17 @@ +import { Page } from "src/components"; + +export function Unhealthy() { + return ( + +
    +
    +
    + +
    +

    The API is not healthy.

    +

    We'll keep checking and hope to be back soon!

    +
    +
    +
    + ); +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..861f637 --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,18 @@ +export * from "./Button"; +export * from "./EmptyData"; +export * from "./ErrorNotFound"; +export * from "./Flag"; +export * from "./Form"; +export * from "./HasPermission"; +export * from "./Loading"; +export * from "./LoadingPage"; +export * from "./LocalePicker"; +export * from "./NavLink"; +export * from "./Page"; +export * from "./SiteContainer"; +export * from "./SiteFooter"; +export * from "./SiteHeader"; +export * from "./SiteMenu"; +export * from "./Table"; +export * from "./ThemeSwitcher"; +export * from "./Unhealthy"; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..34a67ec --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,127 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { createContext, type ReactNode, useContext, useState } from "react"; +import { useIntervalWhen } from "rooks"; +import { + getToken, + isTwoFactorChallenge, + loginAsUser, + refreshToken, + verify2FA, + type TokenResponse, +} from "src/api/backend"; +import AuthStore from "src/modules/AuthStore"; + +// 2FA challenge state +export interface TwoFactorChallenge { + challengeToken: string; +} + +// Context +export interface AuthContextType { + authenticated: boolean; + twoFactorChallenge: TwoFactorChallenge | null; + login: (username: string, password: string) => Promise; + verifyTwoFactor: (code: string) => Promise; + cancelTwoFactor: () => void; + loginAs: (id: number) => Promise; + logout: () => void; + token?: string; +} + +const initalValue = null; +const AuthContext = createContext(initalValue); + +// Provider +interface Props { + children?: ReactNode; + tokenRefreshInterval?: number; +} +function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) { + const queryClient = useQueryClient(); + const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken()); + const [twoFactorChallenge, setTwoFactorChallenge] = useState(null); + + const handleTokenUpdate = (response: TokenResponse) => { + AuthStore.set(response); + setAuthenticated(true); + setTwoFactorChallenge(null); + }; + + const login = async (identity: string, secret: string) => { + const response = await getToken(identity, secret); + if (isTwoFactorChallenge(response)) { + setTwoFactorChallenge({ challengeToken: response.challengeToken }); + return; + } + handleTokenUpdate(response); + }; + + const verifyTwoFactor = async (code: string) => { + if (!twoFactorChallenge) { + throw new Error("No 2FA challenge pending"); + } + const response = await verify2FA(twoFactorChallenge.challengeToken, code); + handleTokenUpdate(response); + }; + + const cancelTwoFactor = () => { + setTwoFactorChallenge(null); + }; + + const loginAs = async (id: number) => { + const response = await loginAsUser(id); + AuthStore.add(response); + queryClient.clear(); + window.location.reload(); + }; + + const logout = () => { + if (AuthStore.count() >= 2) { + AuthStore.drop(); + queryClient.clear(); + window.location.reload(); + return; + } + AuthStore.clear(); + setAuthenticated(false); + queryClient.clear(); + }; + + const refresh = async () => { + const response = await refreshToken(); + handleTokenUpdate(response); + }; + + useIntervalWhen( + () => { + if (authenticated) { + refresh(); + } + }, + tokenRefreshInterval, + true, + ); + + const value = { + authenticated, + twoFactorChallenge, + login, + verifyTwoFactor, + cancelTwoFactor, + loginAs, + logout, + }; + + return {children}; +} + +function useAuthState() { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuthState must be used within a AuthProvider"); + } + return context; +} + +export { AuthProvider, useAuthState }; +export default AuthContext; diff --git a/frontend/src/context/LocaleContext.tsx b/frontend/src/context/LocaleContext.tsx new file mode 100644 index 0000000..ca30a5f --- /dev/null +++ b/frontend/src/context/LocaleContext.tsx @@ -0,0 +1,38 @@ +import { createContext, type ReactNode, useContext, useState } from "react"; +import { getLocale } from "src/locale"; + +// Context +export interface LocaleContextType { + setLocale: (locale: string) => void; + locale?: string; +} + +const initalValue = null; +const LocaleContext = createContext(initalValue); + +// Provider +interface Props { + children?: ReactNode; +} +function LocaleProvider({ children }: Props) { + const [locale, setLocaleValue] = useState(getLocale()); + + const setLocale = async (locale: string) => { + setLocaleValue(locale); + }; + + const value = { locale, setLocale }; + + return {children}; +} + +function useLocaleState() { + const context = useContext(LocaleContext); + if (!context) { + throw new Error("useLocaleState must be used within a LocaleProvider"); + } + return context; +} + +export { LocaleProvider, useLocaleState }; +export default LocaleContext; diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx new file mode 100644 index 0000000..e01ad02 --- /dev/null +++ b/frontend/src/context/ThemeContext.tsx @@ -0,0 +1,70 @@ +import type React from "react"; +import { createContext, type ReactNode, useContext, useEffect, useState } from "react"; + +const StorageKey = "tabler-theme"; +export const Light = "light"; +export const Dark = "dark"; + +// Define theme types +export type Theme = "light" | "dark"; + +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; + setTheme: (theme: Theme) => void; + getTheme: () => Theme; +} + +const ThemeContext = createContext(undefined); + +interface ThemeProviderProps { + children: ReactNode; +} + +const getBrowserDefault = (): Theme => { + if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + return Dark; + } + return Light; +}; + +export const ThemeProvider: React.FC = ({ children }) => { + const [theme, setThemeState] = useState(() => { + // Try to read theme from localStorage or use 'light' as default + if (typeof window !== "undefined") { + const stored = localStorage.getItem(StorageKey) as Theme | null; + return stored || getBrowserDefault(); + } + return getBrowserDefault(); + }); + + useEffect(() => { + document.body.dataset.theme = theme; + document.body.classList.remove(theme === Light ? Dark : Light); + document.body.classList.add(theme); + localStorage.setItem(StorageKey, theme); + }, [theme]); + + const toggleTheme = () => { + setThemeState((prev) => (prev === Light ? Dark : Light)); + }; + + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + }; + + const getTheme = () => { + return theme; + }; + + document.documentElement.setAttribute("data-bs-theme", theme); + return {children}; +}; + +export function useTheme(): ThemeContextType { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} diff --git a/frontend/src/context/index.ts b/frontend/src/context/index.ts new file mode 100644 index 0000000..a24e065 --- /dev/null +++ b/frontend/src/context/index.ts @@ -0,0 +1,3 @@ +export * from "./AuthContext"; +export * from "./LocaleContext"; +export * from "./ThemeContext"; diff --git a/frontend/src/declarations.d.ts b/frontend/src/declarations.d.ts new file mode 100644 index 0000000..18f423b --- /dev/null +++ b/frontend/src/declarations.d.ts @@ -0,0 +1 @@ +declare module "*.md"; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 0000000..744190a --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1,22 @@ +export * from "./useAccessList"; +export * from "./useAccessLists"; +export * from "./useAuditLog"; +export * from "./useAuditLogs"; +export * from "./useCertificate"; +export * from "./useCertificates"; +export * from "./useCheckVersion"; +export * from "./useDeadHost"; +export * from "./useDeadHosts"; +export * from "./useDnsProviders"; +export * from "./useHealth"; +export * from "./useHostReport"; +export * from "./useProxyHost"; +export * from "./useProxyHosts"; +export * from "./useRedirectionHost"; +export * from "./useRedirectionHosts"; +export * from "./useSetting"; +export * from "./useStream"; +export * from "./useStreams"; +export * from "./useTheme"; +export * from "./useUser"; +export * from "./useUsers"; diff --git a/frontend/src/hooks/useAccessList.ts b/frontend/src/hooks/useAccessList.ts new file mode 100644 index 0000000..1f66385 --- /dev/null +++ b/frontend/src/hooks/useAccessList.ts @@ -0,0 +1,60 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + type AccessList, + type AccessListExpansion, + createAccessList, + getAccessList, + updateAccessList, +} from "src/api/backend"; + +const fetchAccessList = (id: number | "new", expand: AccessListExpansion[] = ["owner"]) => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + name: "", + satisfyAny: false, + passAuth: false, + meta: {}, + } as AccessList); + } + return getAccessList(id, expand); +}; + +const useAccessList = (id: number | "new", expand?: AccessListExpansion[], options = {}) => { + return useQuery({ + queryKey: ["access-list", id, expand], + queryFn: () => fetchAccessList(id, expand), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetAccessList = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: AccessList) => (values.id ? updateAccessList(values) : createAccessList(values)), + onMutate: (values: AccessList) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["access-list", values.id]); + queryClient.setQueryData(["access-list", values.id], (old: AccessList) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["access-list", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: AccessList) => { + queryClient.invalidateQueries({ queryKey: ["access-list", id] }); + queryClient.invalidateQueries({ queryKey: ["access-lists"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); + }, + }); +}; + +export { useAccessList, useSetAccessList }; diff --git a/frontend/src/hooks/useAccessLists.ts b/frontend/src/hooks/useAccessLists.ts new file mode 100644 index 0000000..cb052f6 --- /dev/null +++ b/frontend/src/hooks/useAccessLists.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type AccessList, type AccessListExpansion, getAccessLists } from "src/api/backend"; + +const fetchAccessLists = (expand?: AccessListExpansion[]) => { + return getAccessLists(expand); +}; + +const useAccessLists = (expand?: AccessListExpansion[], options = {}) => { + return useQuery({ + queryKey: ["access-lists", { expand }], + queryFn: () => fetchAccessLists(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchAccessLists, useAccessLists }; diff --git a/frontend/src/hooks/useAuditLog.ts b/frontend/src/hooks/useAuditLog.ts new file mode 100644 index 0000000..95e08eb --- /dev/null +++ b/frontend/src/hooks/useAuditLog.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type AuditLog, getAuditLog } from "src/api/backend"; + +const fetchAuditLog = (id: number) => { + return getAuditLog(id, ["user"]); +}; + +const useAuditLog = (id: number, options = {}) => { + return useQuery({ + queryKey: ["audit-log", id], + queryFn: () => fetchAuditLog(id), + staleTime: 5 * 60 * 1000, // 5 minutes + ...options, + }); +}; + +export { useAuditLog }; diff --git a/frontend/src/hooks/useAuditLogs.ts b/frontend/src/hooks/useAuditLogs.ts new file mode 100644 index 0000000..bbe8b50 --- /dev/null +++ b/frontend/src/hooks/useAuditLogs.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type AuditLog, type AuditLogExpansion, getAuditLogs } from "src/api/backend"; + +const fetchAuditLogs = (expand?: AuditLogExpansion[]) => { + return getAuditLogs(expand); +}; + +const useAuditLogs = (expand?: AuditLogExpansion[], options = {}) => { + return useQuery({ + queryKey: ["audit-logs", { expand }], + queryFn: () => fetchAuditLogs(expand), + staleTime: 10 * 1000, + ...options, + }); +}; + +export { fetchAuditLogs, useAuditLogs }; diff --git a/frontend/src/hooks/useCertificate.ts b/frontend/src/hooks/useCertificate.ts new file mode 100644 index 0000000..fc99c84 --- /dev/null +++ b/frontend/src/hooks/useCertificate.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type Certificate, getCertificate } from "src/api/backend"; + +const fetchCertificate = (id: number) => { + return getCertificate(id, ["owner"]); +}; + +const useCertificate = (id: number, options = {}) => { + return useQuery({ + queryKey: ["certificate", id], + queryFn: () => fetchCertificate(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +export { useCertificate }; diff --git a/frontend/src/hooks/useCertificates.ts b/frontend/src/hooks/useCertificates.ts new file mode 100644 index 0000000..261c79d --- /dev/null +++ b/frontend/src/hooks/useCertificates.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type Certificate, type CertificateExpansion, getCertificates } from "src/api/backend"; + +const fetchCertificates = (expand?: CertificateExpansion[]) => { + return getCertificates(expand); +}; + +const useCertificates = (expand?: CertificateExpansion[], options = {}) => { + return useQuery({ + queryKey: ["certificates", { expand }], + queryFn: () => fetchCertificates(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchCertificates, useCertificates }; diff --git a/frontend/src/hooks/useCheckVersion.ts b/frontend/src/hooks/useCheckVersion.ts new file mode 100644 index 0000000..83ee996 --- /dev/null +++ b/frontend/src/hooks/useCheckVersion.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query"; +import { checkVersion, type VersionCheckResponse } from "src/api/backend"; + +const fetchVersion = () => checkVersion(); + +const useCheckVersion = (options = {}) => { + return useQuery({ + queryKey: ["version-check"], + queryFn: fetchVersion, + refetchOnWindowFocus: false, + retry: 5, + refetchInterval: 30 * 1000, // 30 seconds + staleTime: 5 * 60 * 1000, // 5 mins + ...options, + }); +}; + +export { fetchVersion, useCheckVersion }; diff --git a/frontend/src/hooks/useDeadHost.ts b/frontend/src/hooks/useDeadHost.ts new file mode 100644 index 0000000..dd8355e --- /dev/null +++ b/frontend/src/hooks/useDeadHost.ts @@ -0,0 +1,60 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createDeadHost, type DeadHost, getDeadHost, updateDeadHost } from "src/api/backend"; + +const fetchDeadHost = (id: number | "new") => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + domainNames: [], + certificateId: 0, + sslForced: false, + advancedConfig: "", + meta: {}, + http2Support: false, + enabled: true, + hstsEnabled: false, + hstsSubdomains: false, + } as DeadHost); + } + return getDeadHost(id, ["owner"]); +}; + +const useDeadHost = (id: number | "new", options = {}) => { + return useQuery({ + queryKey: ["dead-host", id], + queryFn: () => fetchDeadHost(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetDeadHost = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: DeadHost) => (values.id ? updateDeadHost(values) : createDeadHost(values)), + onMutate: (values: DeadHost) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["dead-host", values.id]); + queryClient.setQueryData(["dead-host", values.id], (old: DeadHost) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["dead-host", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: DeadHost) => { + queryClient.invalidateQueries({ queryKey: ["dead-host", id] }); + queryClient.invalidateQueries({ queryKey: ["dead-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, + }); +}; + +export { useDeadHost, useSetDeadHost }; diff --git a/frontend/src/hooks/useDeadHosts.ts b/frontend/src/hooks/useDeadHosts.ts new file mode 100644 index 0000000..744d28c --- /dev/null +++ b/frontend/src/hooks/useDeadHosts.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type DeadHost, getDeadHosts, type HostExpansion } from "src/api/backend"; + +const fetchDeadHosts = (expand?: HostExpansion[]) => { + return getDeadHosts(expand); +}; + +const useDeadHosts = (expand?: HostExpansion[], options = {}) => { + return useQuery({ + queryKey: ["dead-hosts", { expand }], + queryFn: () => fetchDeadHosts(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchDeadHosts, useDeadHosts }; diff --git a/frontend/src/hooks/useDnsProviders.ts b/frontend/src/hooks/useDnsProviders.ts new file mode 100644 index 0000000..ec6a573 --- /dev/null +++ b/frontend/src/hooks/useDnsProviders.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { type DNSProvider, getCertificateDNSProviders } from "src/api/backend"; + +const fetchDnsProviders = () => { + return getCertificateDNSProviders(); +}; + +const useDnsProviders = (options = {}) => { + return useQuery({ + queryKey: ["dns-providers"], + queryFn: () => fetchDnsProviders(), + staleTime: 300 * 1000, + ...options, + }); +}; + +export { fetchDnsProviders, useDnsProviders }; diff --git a/frontend/src/hooks/useHealth.ts b/frontend/src/hooks/useHealth.ts new file mode 100644 index 0000000..6a261c9 --- /dev/null +++ b/frontend/src/hooks/useHealth.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query"; +import { getHealth, type HealthResponse } from "src/api/backend"; + +const fetchHealth = () => getHealth(); + +const useHealth = (options = {}) => { + return useQuery({ + queryKey: ["health"], + queryFn: fetchHealth, + refetchOnWindowFocus: false, + retry: 5, + refetchInterval: 15 * 1000, // 15 seconds + staleTime: 14 * 1000, // 14 seconds + ...options, + }); +}; + +export { fetchHealth, useHealth }; diff --git a/frontend/src/hooks/useHostReport.ts b/frontend/src/hooks/useHostReport.ts new file mode 100644 index 0000000..c54096d --- /dev/null +++ b/frontend/src/hooks/useHostReport.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query"; +import { getHostsReport } from "src/api/backend"; + +const fetchHostReport = () => getHostsReport(); + +const useHostReport = (options = {}) => { + return useQuery, Error>({ + queryKey: ["host-report"], + queryFn: fetchHostReport, + refetchOnWindowFocus: false, + retry: 5, + refetchInterval: 15 * 1000, // 15 seconds + staleTime: 14 * 1000, // 14 seconds + ...options, + }); +}; + +export { fetchHostReport, useHostReport }; diff --git a/frontend/src/hooks/useProxyHost.ts b/frontend/src/hooks/useProxyHost.ts new file mode 100644 index 0000000..24e7f4f --- /dev/null +++ b/frontend/src/hooks/useProxyHost.ts @@ -0,0 +1,68 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createProxyHost, getProxyHost, type ProxyHost, updateProxyHost } from "src/api/backend"; + +const fetchProxyHost = (id: number | "new") => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + domainNames: [], + forwardHost: "", + forwardPort: 0, + accessListId: 0, + certificateId: 0, + sslForced: false, + cachingEnabled: false, + blockExploits: false, + advancedConfig: "", + meta: {}, + allowWebsocketUpgrade: false, + http2Support: false, + forwardScheme: "", + enabled: true, + hstsEnabled: false, + hstsSubdomains: false, + trustForwardedProto: false, + } as ProxyHost); + } + return getProxyHost(id, ["owner"]); +}; + +const useProxyHost = (id: number | "new", options = {}) => { + return useQuery({ + queryKey: ["proxy-host", id], + queryFn: () => fetchProxyHost(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetProxyHost = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: ProxyHost) => (values.id ? updateProxyHost(values) : createProxyHost(values)), + onMutate: (values: ProxyHost) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["proxy-host", values.id]); + queryClient.setQueryData(["proxy-host", values.id], (old: ProxyHost) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["proxy-host", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: ProxyHost) => { + queryClient.invalidateQueries({ queryKey: ["proxy-host", id] }); + queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, + }); +}; + +export { useProxyHost, useSetProxyHost }; diff --git a/frontend/src/hooks/useProxyHosts.ts b/frontend/src/hooks/useProxyHosts.ts new file mode 100644 index 0000000..86366fe --- /dev/null +++ b/frontend/src/hooks/useProxyHosts.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { getProxyHosts, type ProxyHost, type ProxyHostExpansion } from "src/api/backend"; + +const fetchProxyHosts = (expand?: ProxyHostExpansion[]) => { + return getProxyHosts(expand); +}; + +const useProxyHosts = (expand?: ProxyHostExpansion[], options = {}) => { + return useQuery({ + queryKey: ["proxy-hosts", { expand }], + queryFn: () => fetchProxyHosts(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchProxyHosts, useProxyHosts }; diff --git a/frontend/src/hooks/useRedirectionHost.ts b/frontend/src/hooks/useRedirectionHost.ts new file mode 100644 index 0000000..ff21265 --- /dev/null +++ b/frontend/src/hooks/useRedirectionHost.ts @@ -0,0 +1,71 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + createRedirectionHost, + getRedirectionHost, + type RedirectionHost, + updateRedirectionHost, +} from "src/api/backend"; + +const fetchRedirectionHost = (id: number | "new") => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + domainNames: [], + forwardDomainName: "", + preservePath: false, + certificateId: 0, + sslForced: false, + advancedConfig: "", + meta: {}, + http2Support: false, + forwardScheme: "auto", + forwardHttpCode: 301, + blockExploits: false, + enabled: true, + hstsEnabled: false, + hstsSubdomains: false, + } as RedirectionHost); + } + return getRedirectionHost(id, ["owner"]); +}; + +const useRedirectionHost = (id: number | "new", options = {}) => { + return useQuery({ + queryKey: ["redirection-host", id], + queryFn: () => fetchRedirectionHost(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetRedirectionHost = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: RedirectionHost) => + values.id ? updateRedirectionHost(values) : createRedirectionHost(values), + onMutate: (values: RedirectionHost) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["redirection-host", values.id]); + queryClient.setQueryData(["redirection-host", values.id], (old: RedirectionHost) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["redirection-host", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: RedirectionHost) => { + queryClient.invalidateQueries({ queryKey: ["redirection-host", id] }); + queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, + }); +}; + +export { useRedirectionHost, useSetRedirectionHost }; diff --git a/frontend/src/hooks/useRedirectionHosts.ts b/frontend/src/hooks/useRedirectionHosts.ts new file mode 100644 index 0000000..3b7eda6 --- /dev/null +++ b/frontend/src/hooks/useRedirectionHosts.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { getRedirectionHosts, type HostExpansion, type RedirectionHost } from "src/api/backend"; + +const fetchRedirectionHosts = (expand?: HostExpansion[]) => { + return getRedirectionHosts(expand); +}; + +const useRedirectionHosts = (expand?: HostExpansion[], options = {}) => { + return useQuery({ + queryKey: ["redirection-hosts", { expand }], + queryFn: () => fetchRedirectionHosts(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchRedirectionHosts, useRedirectionHosts }; diff --git a/frontend/src/hooks/useSetting.ts b/frontend/src/hooks/useSetting.ts new file mode 100644 index 0000000..ce843d6 --- /dev/null +++ b/frontend/src/hooks/useSetting.ts @@ -0,0 +1,40 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getSetting, type Setting, updateSetting } from "src/api/backend"; + +const fetchSetting = (id: string) => { + return getSetting(id); +}; + +const useSetting = (id: string, options = {}) => { + return useQuery({ + queryKey: ["setting", id], + queryFn: () => fetchSetting(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetSetting = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: Setting) => updateSetting(values), + onMutate: (values: Setting) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["setting", values.id]); + queryClient.setQueryData(["setting", values.id], (old: Setting) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["setting", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: Setting) => { + queryClient.invalidateQueries({ queryKey: ["setting", id] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + }, + }); +}; + +export { useSetting, useSetSetting }; diff --git a/frontend/src/hooks/useStream.ts b/frontend/src/hooks/useStream.ts new file mode 100644 index 0000000..dfdddc1 --- /dev/null +++ b/frontend/src/hooks/useStream.ts @@ -0,0 +1,56 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createStream, getStream, type Stream, updateStream } from "src/api/backend"; + +const fetchStream = (id: number | "new") => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + tcpForwarding: true, + udpForwarding: false, + meta: {}, + enabled: true, + certificateId: 0, + } as Stream); + } + return getStream(id, ["owner"]); +}; + +const useStream = (id: number | "new", options = {}) => { + return useQuery({ + queryKey: ["stream", id], + queryFn: () => fetchStream(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetStream = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: Stream) => (values.id ? updateStream(values) : createStream(values)), + onMutate: (values: Stream) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["stream", values.id]); + queryClient.setQueryData(["stream", values.id], (old: Stream) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["stream", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: Stream) => { + queryClient.invalidateQueries({ queryKey: ["stream", id] }); + queryClient.invalidateQueries({ queryKey: ["streams"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, + }); +}; + +export { useStream, useSetStream }; diff --git a/frontend/src/hooks/useStreams.ts b/frontend/src/hooks/useStreams.ts new file mode 100644 index 0000000..0f0129d --- /dev/null +++ b/frontend/src/hooks/useStreams.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { getStreams, type HostExpansion, type Stream } from "src/api/backend"; + +const fetchStreams = (expand?: HostExpansion[]) => { + return getStreams(expand); +}; + +const useStreams = (expand?: HostExpansion[], options = {}) => { + return useQuery({ + queryKey: ["streams", { expand }], + queryFn: () => fetchStreams(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchStreams, useStreams }; diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts new file mode 100644 index 0000000..941c062 --- /dev/null +++ b/frontend/src/hooks/useTheme.ts @@ -0,0 +1,8 @@ +import { Dark, Light, useTheme as useThemeContext } from "src/context"; + +// Simple hook wrapper for clarity and scalability +const useTheme = () => { + return useThemeContext(); +}; + +export { useTheme, Dark, Light }; diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts new file mode 100644 index 0000000..4f39949 --- /dev/null +++ b/frontend/src/hooks/useUser.ts @@ -0,0 +1,54 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createUser, getUser, type User, updateUser } from "src/api/backend"; + +const fetchUser = (id: number | string) => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + isDisabled: false, + email: "", + name: "", + nickname: "", + roles: [], + avatar: "", + } as User); + } + return getUser(id, ["permissions"]); +}; + +const useUser = (id: string | number, options = {}) => { + return useQuery({ + queryKey: ["user", id], + queryFn: () => fetchUser(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetUser = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)), + onMutate: (values: User) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["user", values.id]); + queryClient.setQueryData(["user", values.id], (old: User) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["user", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: User) => { + queryClient.invalidateQueries({ queryKey: ["user", id] }); + queryClient.invalidateQueries({ queryKey: ["users"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + }, + }); +}; + +export { useUser, useSetUser }; diff --git a/frontend/src/hooks/useUsers.ts b/frontend/src/hooks/useUsers.ts new file mode 100644 index 0000000..8c4d802 --- /dev/null +++ b/frontend/src/hooks/useUsers.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { getUsers, type User, type UserExpansion } from "src/api/backend"; + +const fetchUsers = (expand?: UserExpansion[]) => { + return getUsers(expand); +}; + +const useUsers = (expand?: UserExpansion[], options = {}) => { + return useQuery({ + queryKey: ["users", { expand }], + queryFn: () => fetchUsers(expand), + staleTime: 60 * 1000, + ...options, + }); +}; + +export { fetchUsers, useUsers }; diff --git a/frontend/src/hooks/useWireGuard.ts b/frontend/src/hooks/useWireGuard.ts new file mode 100644 index 0000000..59af648 --- /dev/null +++ b/frontend/src/hooks/useWireGuard.ts @@ -0,0 +1,61 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getWgClients, + getWgInterface, + createWgClient, + deleteWgClient, + enableWgClient, + disableWgClient, + type WgClient, + type WgInterface, +} from "src/api/backend/wireguard"; + +export const useWgClients = (options = {}) => { + return useQuery({ + queryKey: ["wg-clients"], + queryFn: getWgClients, + refetchInterval: 5000, + staleTime: 3000, + ...options, + }); +}; + +export const useWgInterface = (options = {}) => { + return useQuery({ + queryKey: ["wg-interface"], + queryFn: getWgInterface, + staleTime: 60 * 1000, + ...options, + }); +}; + +export const useCreateWgClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: { name: string }) => createWgClient(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["wg-clients"] }); + }, + }); +}; + +export const useDeleteWgClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => deleteWgClient(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["wg-clients"] }); + }, + }); +}; + +export const useToggleWgClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, enabled }: { id: number; enabled: boolean }) => + enabled ? enableWgClient(id) : disableWgClient(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["wg-clients"] }); + }, + }); +}; diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx new file mode 100644 index 0000000..dabe81b --- /dev/null +++ b/frontend/src/locale/IntlProvider.tsx @@ -0,0 +1,143 @@ +import { createIntl, createIntlCache } from "react-intl"; +import langBg from "./lang/bg.json"; +import langDe from "./lang/de.json"; +import langPt from "./lang/pt.json"; +import langEn from "./lang/en.json"; +import langEs from "./lang/es.json"; +import langEt from "./lang/et.json"; +import langFr from "./lang/fr.json"; +import langGa from "./lang/ga.json"; +import langId from "./lang/id.json"; +import langIt from "./lang/it.json"; +import langJa from "./lang/ja.json"; +import langKo from "./lang/ko.json"; +import langNl from "./lang/nl.json"; +import langPl from "./lang/pl.json"; +import langRu from "./lang/ru.json"; +import langSk from "./lang/sk.json"; +import langCs from "./lang/cs.json"; +import langVi from "./lang/vi.json"; +import langZh from "./lang/zh.json"; +import langTr from "./lang/tr.json"; +import langHu from "./lang/hu.json"; +import langNo from "./lang/no.json"; +import langList from "./lang/lang-list.json"; + +// first item of each array should be the language code, +// not the country code +// Remember when adding to this list, also update check-locales.js script +const localeOptions = [ + ["en", "en-US", langEn], + ["de", "de-DE", langDe], + ["es", "es-ES", langEs], + ["et", "et-EE", langEt], + ["pt", "pt-PT", langPt], + ["fr", "fr-FR", langFr], + ["ga", "ga-IE", langGa], + ["ja", "ja-JP", langJa], + ["it", "it-IT", langIt], + ["nl", "nl-NL", langNl], + ["pl", "pl-PL", langPl], + ["ru", "ru-RU", langRu], + ["sk", "sk-SK", langSk], + ["cs", "cs-CZ", langCs], + ["vi", "vi-VN", langVi], + ["zh", "zh-CN", langZh], + ["ko", "ko-KR", langKo], + ["bg", "bg-BG", langBg], + ["id", "id-ID", langId], + ["tr", "tr-TR", langTr], + ["hu", "hu-HU", langHu], + ["no", "no-NO", langNo], +]; + +const loadMessages = (locale?: string): typeof langList & typeof langEn => { + const thisLocale = (locale || "en").slice(0, 2); + + // ensure this lang exists in localeOptions above, otherwise fallback to en + if (thisLocale === "en" || !localeOptions.some(([code]) => code === thisLocale)) { + return Object.assign({}, langList, langEn); + } + + return Object.assign({}, langList, langEn, localeOptions.find(([code]) => code === thisLocale)?.[2]); +}; + +const getFlagCodeForLocale = (locale?: string) => { + const thisLocale = (locale || "en").slice(0, 2); + + // only add to this if your flag is different from the locale code + const specialCases: Record = { + ja: "jp", // Japan + zh: "cn", // China + vi: "vn", // Vietnam + ko: "kr", // Korea + cs: "cz", // Czechia + }; + + if (specialCases[thisLocale]) { + return specialCases[thisLocale].toUpperCase(); + } + return thisLocale.toUpperCase(); +}; + +const getLocale = (short = false) => { + let loc = window.localStorage.getItem("locale"); + if (!loc) { + loc = document.documentElement.lang; + } + if (short) { + return loc.slice(0, 2); + } + // finally, fallback + if (!loc) { + loc = "en"; + } + return loc; +}; + +const cache = createIntlCache(); + +const initialMessages = loadMessages(getLocale()); +let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache); + +const changeLocale = (locale: string): void => { + const messages = loadMessages(locale); + intl = createIntl({ locale, messages }, cache); + window.localStorage.setItem("locale", locale); + document.documentElement.lang = locale; +}; + +// This is a translation component that wraps the translation in a span with a data +// attribute so devs can inspect the element to see the translation ID +const T = ({ + id, + data, + tData, +}: { + id: string; + data?: Record; + tData?: Record; +}) => { + const translatedData: Record = {}; + if (tData) { + // iterate over tData and translate each value + Object.entries(tData).forEach(([key, value]) => { + translatedData[key] = intl.formatMessage({ id: value }); + }); + } + return ( + + {intl.formatMessage( + { id }, + { + ...data, + ...translatedData, + }, + )} + + ); +}; + +//console.log("L:", localeOptions); + +export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T }; diff --git a/frontend/src/locale/README.md b/frontend/src/locale/README.md new file mode 100644 index 0000000..7016cf6 --- /dev/null +++ b/frontend/src/locale/README.md @@ -0,0 +1,50 @@ +# Internationalisation support + +## Before you start + +It's highly recommended that you spin up a development instance of this project +on your docker capable server. It's pretty easy: + +```bash +git clone https://github.com/NginxProxyManager/nginx-proxy-manager.git +cd nginx-proxy-manager +./scripts/start-dev -f +``` + +Then after a while, you can access http://yourserverip:3081 + +This stack will watch the file system for changes, especially to language files, +and reload the site you have open in the browser. + + +## Adding new translations + +Modify the files in the `src` folder. Follow the conventions already there. + +When the development stack is running, it will sort the locale lang files +for you when you save. + + +## After making changes + +If you're NOT running the development stack, you will need to run +`yarn locale-compile` in the `frontend` folder for +the new translations to be compiled into the `lang` folder. + + +## Adding a whole new language + +There's a fair bit you'll need to touch. Here's a list that may +not be complete by the time you're reading this: + +- frontend/src/locale/src/[yourlang].json +- frontend/src/locale/src/lang-list.json +- frontend/src/locale/src/HelpDoc/[yourlang]/* +- frontend/src/locale/src/HelpDoc/index.tsx +- frontend/src/locale/IntlProvider.tsx +- frontend/check-locales.cjs + + +## Checking for missing translations in languages + +Run `node check-locales.cjs` in this frontend folder. diff --git a/frontend/src/locale/Utils.test.tsx b/frontend/src/locale/Utils.test.tsx new file mode 100644 index 0000000..e998dc3 --- /dev/null +++ b/frontend/src/locale/Utils.test.tsx @@ -0,0 +1,74 @@ +import { formatDateTime } from "src/locale"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +describe("DateFormatter", () => { + // Keep a reference to the real Intl to restore later + const RealIntl = global.Intl; + const desiredTimeZone = "Europe/London"; + const desiredLocale = "en-GB"; + + beforeAll(() => { + // Ensure Node-based libs using TZ behave deterministically + try { + process.env.TZ = desiredTimeZone; + } catch { + // ignore if not available + } + + // Mock Intl.DateTimeFormat so formatting is stable regardless of host + const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat { + constructor(_locales?: string | string[], options?: Intl.DateTimeFormatOptions) { + super(desiredLocale, { + ...options, + timeZone: desiredTimeZone, + }); + } + } as unknown as typeof Intl.DateTimeFormat; + + global.Intl = { + ...RealIntl, + DateTimeFormat: MockedDateTimeFormat, + }; + }); + + afterAll(() => { + // Restore original Intl after tests + global.Intl = RealIntl; + }); + + it("format date from iso date", () => { + const value = "2024-01-01T00:00:00.000Z"; + const text = formatDateTime(value); + expect(text).toBe("1 Jan 2024, 12:00:00 am"); + }); + + it("format date from unix timestamp number", () => { + const value = 1762476112; + const text = formatDateTime(value); + expect(text).toBe("7 Nov 2025, 12:41:52 am"); + }); + + it("format date from unix timestamp string", () => { + const value = "1762476112"; + const text = formatDateTime(value); + expect(text).toBe("7 Nov 2025, 12:41:52 am"); + }); + + it("catch bad format from string", () => { + const value = "this is not a good date"; + const text = formatDateTime(value); + expect(text).toBe("this is not a good date"); + }); + + it("catch bad format from number", () => { + const value = -100; + const text = formatDateTime(value); + expect(text).toBe("-100"); + }); + + it("catch bad format from number as string", () => { + const value = "-100"; + const text = formatDateTime(value); + expect(text).toBe("-100"); + }); +}); diff --git a/frontend/src/locale/Utils.ts b/frontend/src/locale/Utils.ts new file mode 100644 index 0000000..d422427 --- /dev/null +++ b/frontend/src/locale/Utils.ts @@ -0,0 +1,46 @@ +import { + fromUnixTime, + type IntlFormatFormatOptions, + intlFormat, + parseISO, +} from "date-fns"; + +const isUnixTimestamp = (value: unknown): boolean => { + if (typeof value !== "number" && typeof value !== "string") return false; + const num = Number(value); + if (!Number.isFinite(num)) return false; + // Check plausible Unix timestamp range: from 1970 to ~year 3000 + // Support both seconds and milliseconds + if (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits) + if (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits) + return false; +}; + +const parseDate = (value: string | number): Date | null => { + if (typeof value !== "number" && typeof value !== "string") return null; + try { + return isUnixTimestamp(value) ? fromUnixTime(+value) : parseISO(`${value}`); + } catch { + return null; + } +}; + +const formatDateTime = (value: string | number, locale = "en-US"): string => { + const d = parseDate(value); + if (!d) return `${value}`; + try { + return intlFormat( + d, + { + dateStyle: "medium", + timeStyle: "medium", + hourCycle: "h12", + } as IntlFormatFormatOptions, + { locale }, + ); + } catch { + return `${value}`; + } +}; + +export { formatDateTime, parseDate, isUnixTimestamp }; diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts new file mode 100644 index 0000000..bdd1343 --- /dev/null +++ b/frontend/src/locale/index.ts @@ -0,0 +1,2 @@ +export * from "./IntlProvider"; +export * from "./Utils"; diff --git a/frontend/src/locale/scripts/locale-sort.cjs b/frontend/src/locale/scripts/locale-sort.cjs new file mode 100644 index 0000000..f7f72cd --- /dev/null +++ b/frontend/src/locale/scripts/locale-sort.cjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const DIR = path.resolve(__dirname, "../src"); + +// Function to sort object keys recursively +function sortKeys(obj) { + if (obj === null || typeof obj !== "object" || obj instanceof Array) { + return obj; + } + + const sorted = {}; + const keys = Object.keys(obj).sort(); + for (const key of keys) { + const value = obj[key]; + if (typeof value === "object" && value !== null && !(value instanceof Array)) { + sorted[key] = sortKeys(value); + } else { + sorted[key] = value; + } + } + return sorted; +} + +// Get all JSON files in the directory +const files = fs.readdirSync(DIR).filter((file) => { + return file.endsWith(".json") && file !== "lang-list.json"; +}); + +files.forEach((file) => { + const filePath = path.join(DIR, file); + const stats = fs.statSync(filePath); + + if (!stats.isFile()) { + return; + } + + if (stats.size === 0) { + console.log(`Skipping empty file ${file}`); + return; + } + + try { + // Read original content + const originalContent = fs.readFileSync(filePath, "utf8"); + const originalJson = JSON.parse(originalContent); + + // Sort keys + const sortedJson = sortKeys(originalJson); + + // Convert back to string with tabs + const sortedContent = JSON.stringify(sortedJson, null, "\t") + "\n"; + + // Compare (normalize whitespace) + if (originalContent.trim() === sortedContent.trim()) { + console.log(`${file} is already sorted`); + return; + } + + // Write sorted content + fs.writeFileSync(filePath, sortedContent, "utf8"); + console.log(`Sorted ${file}`); + } catch (error) { + console.error(`Error processing ${file}:`, error.message); + } +}); + diff --git a/frontend/src/locale/scripts/locale-sort.sh b/frontend/src/locale/scripts/locale-sort.sh new file mode 100644 index 0000000..f745888 --- /dev/null +++ b/frontend/src/locale/scripts/locale-sort.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e -o pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$DIR/../src" || exit 1 + +if ! command -v jq &> /dev/null; then + echo "jq could not be found, please install it to sort JSON files." + exit 1 +fi + +# iterate over all json files in the current directory +for file in *.json; do + if [[ -f "$file" ]]; then + if [[ ! -s "$file" ]]; then + echo "Skipping empty file $file" + continue + fi + + if [ "$file" == "lang-list.json" ]; then + continue + fi + + # get content of file before sorting + original_content=$(<"$file") + # compare with sorted content + sorted_content=$(jq --tab --sort-keys . "$file") + if [ "$original_content" == "$sorted_content" ]; then + echo "$file is already sorted" + continue + fi + + echo "Sorting $file" + tmp=$(mktemp) && jq --tab --sort-keys . "$file" > "$tmp" && mv "$tmp" "$file" + fi +done diff --git a/frontend/src/locale/src/HelpDoc/bg/AccessLists.md b/frontend/src/locale/src/HelpDoc/bg/AccessLists.md new file mode 100644 index 0000000..8e7f250 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/AccessLists.md @@ -0,0 +1,7 @@ +## Какво представлява Списъкът за достъп? + +Списъците за достъп предоставят черен или бял списък от конкретни клиентски IP адреси, както и удостоверяване за Прокси хостове чрез базова HTTP автентикация. + +Можете да конфигурирате множество клиентски правила, потребителски имена и пароли в един Списък за достъп и след това да го приложите към един или повече _Прокси хостове_. + +Това е най-полезно при препращани уеб услуги, които нямат вградени механизми за удостоверяване, или когато искате да защитите достъпа от неизвестни клиенти. diff --git a/frontend/src/locale/src/HelpDoc/bg/Certificates.md b/frontend/src/locale/src/HelpDoc/bg/Certificates.md new file mode 100644 index 0000000..c138a75 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/Certificates.md @@ -0,0 +1,21 @@ +## Помощ за сертификати + +### HTTP сертификат + +HTTP валидираният сертификат означава, че сървърите на Let’s Encrypt ще се опитат да достигнат вашите домейни по HTTP (не по HTTPS!) и ако успеят, ще издадат сертификата. + +За този метод трябва да имате създаден _Прокси хост_ за вашия/вашите домейни, който да е достъпен по HTTP и да сочи към тази Nginx инсталация. След като бъде издаден сертификат, можете да промените _Прокси хоста_ така, че да използва сертификата и за HTTPS връзки. Въпреки това, _Прокси хостът_ трябва да остане конфигуриран за достъп по HTTP, за да може сертификатът да се подновява. + +Този процес _не_ поддържа wildcard домейни. + +### DNS сертификат + +DNS валидираният сертификат изисква използването на DNS Provider плъгин. Този DNS Provider ще бъде използван за временно създаване на записи във вашия домейн, след което Let’s Encrypt ще ги провери, за да се увери, че сте собственикът, и при успех ще издаде сертификата. + +Не е необходимо да имате _Прокси хост_, създаден предварително, за да заявите този тип сертификат. Нито е нужно вашият _Прокси хост_ да бъде конфигуриран за достъп по HTTP. + +Този процес _поддържа_ wildcard домейни. + +### Персонализиран сертификат + +Използвайте тази опция, за да качите собствен SSL сертификат, предоставен от ваша сертификатна агенция. diff --git a/frontend/src/locale/src/HelpDoc/bg/DeadHosts.md b/frontend/src/locale/src/HelpDoc/bg/DeadHosts.md new file mode 100644 index 0000000..79ad960 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/DeadHosts.md @@ -0,0 +1,10 @@ +## Какво представлява 404 хост? + +404 хост е просто конфигурация на хост, който показва страница с грешка 404. + +Това може да е полезно, когато вашият домейн е индексиран в търсачките и искате +да предоставите по-приятна страница за грешка или да уведомите индексиращите системи, +че страниците на домейна вече не съществуват. + +Допълнително предимство на този хост е възможността да проследявате логовете на заявките +към него и да виждате реферерите. diff --git a/frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md new file mode 100644 index 0000000..ef82c39 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/ProxyHosts.md @@ -0,0 +1,7 @@ +## Какво представлява Прокси хост? + +Прокси хост е входна точка за уеб услуга, която искате да препращате. + +Той предоставя възможност за SSL терминaция на услуга, която може да няма вградена поддръжка на SSL. + +Прокси хостовете са най-често използваната функция в Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md new file mode 100644 index 0000000..06890dc --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Какво представлява Хост за пренасочване? + +Хостът за пренасочване пренасочва заявките от входящия домейн и прехвърля +потребителя към друг домейн. + +Най-честата причина за използване на този тип хост е, когато вашият уебсайт +промени домейна си, но все още има линкове от търсачки или реферери, които сочат към стария домейн. diff --git a/frontend/src/locale/src/HelpDoc/bg/Streams.md b/frontend/src/locale/src/HelpDoc/bg/Streams.md new file mode 100644 index 0000000..4beb381 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/Streams.md @@ -0,0 +1,6 @@ +## Какво представлява Потокът (Stream)? + +Относително нова функция за Nginx, Потокът позволява препращане на TCP/UDP +трафик директно към друг компютър в мрежата. + +Това е полезно, ако хоствате игрови сървъри, FTP или SSH сървъри. diff --git a/frontend/src/locale/src/HelpDoc/bg/index.ts b/frontend/src/locale/src/HelpDoc/bg/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/bg/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/cs/AccessLists.md b/frontend/src/locale/src/HelpDoc/cs/AccessLists.md new file mode 100644 index 0000000..78d9be6 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/AccessLists.md @@ -0,0 +1,7 @@ +## Co je seznam přístupů? + +Seznamy přístupů poskytují blacklist nebo whitelist konkrétních IP adres klientů spolu s ověřením pro proxy hostitele prostřednictvím základního ověřování HTTP. + +Můžete nakonfigurovat více pravidel pro klienty, uživatelská jména a hesla pro jeden seznam přístupu a poté ho použít na jednoho nebo více proxy hostitelů. + +Toto je nejužitečnější pro přesměrované webové služby, které nemají vestavěné ověřovací mechanismy, nebo pokud se chcete chránit před neznámými klienty. diff --git a/frontend/src/locale/src/HelpDoc/cs/Certificates.md b/frontend/src/locale/src/HelpDoc/cs/Certificates.md new file mode 100644 index 0000000..78e67f3 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/Certificates.md @@ -0,0 +1,32 @@ +## Pomoc s certifikáty + +### Certifikát HTTP + +Certifikát ověřený prostřednictvím protokolu HTTP znamená, že servery Let's Encrypt se +pokusí připojit k vašim doménám přes protokol HTTP (nikoli HTTPS!) a v případě úspěchu +vydají váš certifikát. + +Pro tuto metodu budete muset mít pro své domény vytvořeného _Proxy Host_, který +je přístupný přes HTTP a směruje na tuto instalaci Nginx. Po vydání certifikátu +můžete změnit _Proxy Host_ tak, aby tento certifikát používal i pro HTTPS +připojení. _Proxy Host_ však bude stále potřeba nakonfigurovat pro přístup přes HTTP, +aby se certifikát mohl obnovit. + +Tento proces _nepodporuje_ domény se zástupnými znaky. + +### Certifikát DNS + +Certifikát ověřený DNS vyžaduje použití pluginu DNS Provider. Tento DNS +Provider se použije na vytvoření dočasných záznamů ve vaší doméně a poté Let's +Encrypt ověří tyto záznamy, aby se ujistil, že jste vlastníkem, a pokud bude úspěšný, +vydá váš certifikát. + +Před požádáním o tento typ certifikátu není potřeba vytvořit _Proxy Host_. +Není také potřeba mít _Proxy Host_ nakonfigurovaný pro přístup HTTP. + +Tento proces _podporuje_ domény se zástupnými znaky. + +### Vlastní certifikát + +Tuto možnost použijte na nahrání vlastního SSL certifikátu, který vám poskytla vaše +certifikační autorita. diff --git a/frontend/src/locale/src/HelpDoc/cs/DeadHosts.md b/frontend/src/locale/src/HelpDoc/cs/DeadHosts.md new file mode 100644 index 0000000..61d4dcf --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/DeadHosts.md @@ -0,0 +1,10 @@ +## Co je to 404 Host? + +404 Host je jednoduše nastavení hostitele, které zobrazuje stránku 404. + +To může být užitečné, pokud je vaše doména uvedena ve vyhledávačích a chcete +poskytnout hezčí chybovou stránku nebo konkrétně oznámit vyhledávačům, že +stránky domény již neexistují. + +Další výhodou tohoto hostitele je sledování protokolů o návštěvách a +zobrazení odkazů. diff --git a/frontend/src/locale/src/HelpDoc/cs/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/cs/ProxyHosts.md new file mode 100644 index 0000000..3637641 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/ProxyHosts.md @@ -0,0 +1,7 @@ +## Co je proxy hostitel? + +Proxy hostitel je příchozí koncový bod pro webovou službu, kterou chcete přesměrovat. + +Poskytuje volitelné ukončení SSL pro vaši službu, která nemusí mít zabudovanou podporu SSL. + +Proxy hostitelé jsou nejběžnějším použitím pro Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/cs/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/cs/RedirectionHosts.md new file mode 100644 index 0000000..ed26166 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Co je přesměrovací hostitel? + +Přesměrovací hostitel přesměruje požadavky z příchozí domény a přesměruje +návštěvníka na jinou doménu. + +Nejčastějším důvodem pro použití tohoto typu hostitele je situace, kdy vaše webová stránka změní +doménu, ale stále máte odkazy ve vyhledávačích nebo referenční odkazy směřující na starou doménu. diff --git a/frontend/src/locale/src/HelpDoc/cs/Streams.md b/frontend/src/locale/src/HelpDoc/cs/Streams.md new file mode 100644 index 0000000..8dc3f4e --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/Streams.md @@ -0,0 +1,6 @@ +## Co je stream? + +Stream je relativně nová funkce pro Nginx, která slouží na přesměrování TCP/UDP +datového toku přímo do jiného počítače v síti. + +Pokud provozujete herní servery, FTP nebo SSH servery, tato funkce se vám může hodit. diff --git a/frontend/src/locale/src/HelpDoc/cs/index.ts b/frontend/src/locale/src/HelpDoc/cs/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/cs/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/de/AccessLists.md b/frontend/src/locale/src/HelpDoc/de/AccessLists.md new file mode 100644 index 0000000..315406a --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/AccessLists.md @@ -0,0 +1,7 @@ +## Was ist eine Zugriffsliste? + +Zugriffslisten bieten eine Blacklist oder Whitelist mit bestimmten Client-IP-Adressen sowie eine Authentifizierung für die Proxy-Hosts über die grundlegende HTTP-Authentifizierung. + +Sie können mehrere Client-Regeln, Benutzernamen und Passwörter für eine einzelne Zugriffsliste konfigurieren und diese dann auf einen oder mehrere Proxy-Hosts anwenden. + +Dies ist besonders nützlich für weitergeleitete Webdienste, die keine integrierten Authentifizierungsmechanismen haben, oder wenn Sie sich vor unbekannten Clients schützen möchten. diff --git a/frontend/src/locale/src/HelpDoc/de/Certificates.md b/frontend/src/locale/src/HelpDoc/de/Certificates.md new file mode 100644 index 0000000..be15ffa --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/Certificates.md @@ -0,0 +1,32 @@ +## Hilfe zu Zertifikaten + +### HTTP-Zertifikat + +Ein HTTP-validiertes Zertifikat bedeutet, dass Let's Encrypt-Server +versuchen, Ihre Domains über HTTP (nicht HTTPS!) zu erreichen, und wenn dies erfolgreich ist, +stellen sie Ihr Zertifikat aus. + +Für diese Methode müssen Sie einen _Proxy-Host_ für Ihre Domain(s) erstellen, der +über HTTP zugänglich ist und auf diese Nginx-Installation verweist. Nachdem ein Zertifikat +ausgestellt wurde, können Sie den _Proxy-Host_ so ändern, dass dieses Zertifikat auch für HTTPS-Verbindungen +verwendet wird. Der _Proxy-Host_ muss jedoch weiterhin für den HTTP-Zugriff konfiguriert sein, + damit das Zertifikat erneuert werden kann. + +Dieser Prozess unterstützt keine Wildcard-Domains. + +### DNS-Zertifikat + +Für ein DNS-validiertes Zertifikat müssen Sie ein DNS-Provider-Plugin verwenden. Dieser DNS- +Provider wird verwendet, um temporäre Einträge auf Ihrer Domain zu erstellen. Anschließend fragt Let's +Encrypt diese Einträge ab, um sicherzustellen, dass Sie der Eigentümer sind. Bei Erfolg wird +Ihr Zertifikat ausgestellt. + +Sie müssen vor der Beantragung dieser Art von Zertifikat keinen _Proxy-Host_ erstellen. +Sie müssen Ihren _Proxy-Host_ auch nicht für den HTTP-Zugriff konfigurieren. + +Dieser Prozess unterstützt Wildcard-Domains. + +### Benutzerdefiniertes Zertifikat + +Verwenden Sie diese Option, um Ihr eigenes SSL-Zertifikat hochzuladen, das Ihnen von Ihrer eigenen +Zertifizierungsstelle bereitgestellt wurde. diff --git a/frontend/src/locale/src/HelpDoc/de/DeadHosts.md b/frontend/src/locale/src/HelpDoc/de/DeadHosts.md new file mode 100644 index 0000000..a73992c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/DeadHosts.md @@ -0,0 +1,10 @@ +## Was ist ein 404-Host? + +Ein 404-Host ist ein Host-Setup, das eine 404-Seite anzeigt. + +Dies kann nützlich sein, wenn Ihre Domain in Suchmaschinen gelistet ist und Sie +eine ansprechendere Fehlerseite bereitstellen oder den Suchindexern ausdrücklich mitteilen möchten, dass +die Domain-Seiten nicht mehr existieren. + +Ein weiterer Vorteil dieses Hosts besteht darin, dass Sie die Protokolle für Zugriffe darauf verfolgen und +die Verweise anzeigen können. diff --git a/frontend/src/locale/src/HelpDoc/de/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/de/ProxyHosts.md new file mode 100644 index 0000000..f116b85 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/ProxyHosts.md @@ -0,0 +1,7 @@ +## Was ist ein Proxy-Host? + +Ein Proxy-Host ist der eingehende Endpunkt für einen Webdienst, den Sie weiterleiten möchten. + +Er bietet optionale SSL-Terminierung für Ihren Dienst, der möglicherweise keine integrierte SSL-Unterstützung hat. + +Proxy-Hosts sind die häufigste Verwendung für den Nginx Proxy Manager. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/de/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/de/RedirectionHosts.md new file mode 100644 index 0000000..3e06919 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Was ist ein Redirection Host? + +Ein Redirection Host leitet Anfragen von der eingehenden Domain weiter und leitet den +Besucher zu einer anderen Domain weiter. + +Der häufigste Grund für die Verwendung dieses Host-Typs ist, wenn Ihre Website die +Domain wechselt, aber Sie noch Suchmaschinen- oder Referrer-Links haben, die auf die alte Domain verweisen. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/de/Streams.md b/frontend/src/locale/src/HelpDoc/de/Streams.md new file mode 100644 index 0000000..35cd539 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/Streams.md @@ -0,0 +1,6 @@ +## Was ist ein Stream? + +Ein Stream ist eine relativ neue Funktion von Nginx, die dazu dient, TCP/UDP-Datenverkehr +direkt an einen anderen Computer im Netzwerk weiterzuleiten. + +Wenn Sie Spielserver, FTP- oder SSH-Server betreiben, kann dies sehr nützlich sein. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/de/index.ts b/frontend/src/locale/src/HelpDoc/de/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/en/AccessLists.md b/frontend/src/locale/src/HelpDoc/en/AccessLists.md new file mode 100644 index 0000000..cef5826 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/AccessLists.md @@ -0,0 +1,7 @@ +## What is an Access List? + +Access Lists provide a blacklist or whitelist of specific client IP addresses along with authentication for the Proxy Hosts via Basic HTTP Authentication. + +You can configure multiple client rules, usernames and passwords for a single Access List and then apply that to one or more _Proxy Hosts_. + +This is most useful for forwarded web services that do not have authentication mechanisms built in or when you want to protect from unknown clients. diff --git a/frontend/src/locale/src/HelpDoc/en/Certificates.md b/frontend/src/locale/src/HelpDoc/en/Certificates.md new file mode 100644 index 0000000..d79dd04 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/Certificates.md @@ -0,0 +1,32 @@ +## Certificates Help + +### HTTP Certificate + +A HTTP validated certificate means Let's Encrypt servers will +attempt to reach your domains over HTTP (not HTTPS!) and if successful, they +will issue your certificate. + +For this method, you will have to have a _Proxy Host_ created for your domains(s) that +is accessible with HTTP and pointing to this Nginx installation. After a certificate +has been given, you can modify the _Proxy Host_ to also use this certificate for HTTPS +connections. However, the _Proxy Host_ will still need to be configured for HTTP access +in order for the certificate to renew. + +This process _does not_ support wildcard domains. + +### DNS Certificate + +A DNS validated certificate requires you to use a DNS Provider plugin. This DNS +Provider will be used to create temporary records on your domain and then Let's +Encrypt will query those records to be sure you're the owner and if successful, they +will issue your certificate. + +You do not need a _Proxy Host_ to be created prior to requesting this type of +certificate. Nor do you need to have your _Proxy Host_ configured for HTTP access. + +This process _does_ support wildcard domains. + +### Custom Certificate + +Use this option to upload your own SSL Certificate, as provided by your own +Certificate Authority. diff --git a/frontend/src/locale/src/HelpDoc/en/DeadHosts.md b/frontend/src/locale/src/HelpDoc/en/DeadHosts.md new file mode 100644 index 0000000..ef4f3bc --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/DeadHosts.md @@ -0,0 +1,10 @@ +## What is a 404 Host? + +A 404 Host is simply a host setup that shows a 404 page. + +This can be useful when your domain is listed in search engines and you want +to provide a nicer error page or specifically to tell the search indexers that +the domain pages no longer exist. + +Another benefit of having this host is to track the logs for hits to it and +view the referrers. diff --git a/frontend/src/locale/src/HelpDoc/en/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/en/ProxyHosts.md new file mode 100644 index 0000000..e9630d0 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/ProxyHosts.md @@ -0,0 +1,7 @@ +## What is a Proxy Host? + +A Proxy Host is the incoming endpoint for a web service that you want to forward. + +It provides optional SSL termination for your service that might not have SSL support built in. + +Proxy Hosts are the most common use for the Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md new file mode 100644 index 0000000..e57b1b8 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/RedirectionHosts.md @@ -0,0 +1,7 @@ +## What is a Redirection Host? + +A Redirection Host will redirect requests from the incoming domain and push the +viewer to another domain. + +The most common reason to use this type of host is when your website changes +domains but you still have search engine or referrer links pointing to the old domain. diff --git a/frontend/src/locale/src/HelpDoc/en/Streams.md b/frontend/src/locale/src/HelpDoc/en/Streams.md new file mode 100644 index 0000000..358f3e5 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/Streams.md @@ -0,0 +1,6 @@ +## What is a Stream? + +A relatively new feature for Nginx, a Stream will serve to forward TCP/UDP +traffic directly to another computer on the network. + +If you're running game servers, FTP or SSH servers this can come in handy. diff --git a/frontend/src/locale/src/HelpDoc/en/index.ts b/frontend/src/locale/src/HelpDoc/en/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/es/AccessLists.md b/frontend/src/locale/src/HelpDoc/es/AccessLists.md new file mode 100644 index 0000000..f0a089f --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/AccessLists.md @@ -0,0 +1,7 @@ +## ¿Qué es una Lista de Acceso? + +Las Listas de Acceso proporcionan una lista negra o blanca de direcciones IP de cliente específicas junto con autenticación para los Hosts Proxy a través de Autenticación HTTP Básica. + +Puede configurar múltiples reglas de cliente, nombres de usuario y contraseñas para una única Lista de Acceso y luego aplicarla a uno o más _Hosts Proxy_. + +Esto es más útil para servicios web reenviados que no tienen mecanismos de autenticación integrados o cuando desea protegerse de clientes desconocidos. diff --git a/frontend/src/locale/src/HelpDoc/es/Certificates.md b/frontend/src/locale/src/HelpDoc/es/Certificates.md new file mode 100644 index 0000000..5907147 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/Certificates.md @@ -0,0 +1,32 @@ +## Ayuda de Certificados + +### Certificado HTTP + +Un certificado validado por HTTP significa que los servidores de Let's Encrypt +intentarán acceder a tus dominios a través de HTTP (¡no HTTPS!) y, si tienen éxito, +emitirán tu certificado. + +Para este método, deberás tener un _Host Proxy_ creado para tu(s) dominio(s) que +sea accesible por HTTP y que apunte a esta instalación de Nginx. Después de que se +haya emitido un certificado, puedes modificar el _Host Proxy_ para que también use +este certificado para conexiones HTTPS. Sin embargo, el _Host Proxy_ seguirá +necesitando estar configurado para acceso HTTP para que el certificado se renueve. + +Este proceso _no_ admite dominios comodín. + +### Certificado DNS + +Un certificado validado por DNS requiere que uses un complemento de Proveedor de DNS. +Este Proveedor de DNS se usará para crear registros temporales en tu dominio y luego +Let's Encrypt consultará esos registros para asegurarse de que eres el propietario y, +si tiene éxito, emitirá tu certificado. + +No necesitas tener un _Host Proxy_ creado antes de solicitar este tipo de certificado. +Tampoco necesitas tener tu _Host Proxy_ configurado para acceso HTTP. + +Este proceso _sí_ admite dominios comodín. + +### Certificado Personalizado + +Usa esta opción para cargar tu propio Certificado SSL, proporcionado por tu propia +Autoridad de Certificación. diff --git a/frontend/src/locale/src/HelpDoc/es/DeadHosts.md b/frontend/src/locale/src/HelpDoc/es/DeadHosts.md new file mode 100644 index 0000000..bc77568 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/DeadHosts.md @@ -0,0 +1,10 @@ +## ¿Qué es un Host 404? + +Un Host 404 es simplemente una configuración de host que muestra una página 404. + +Esto puede ser útil cuando tu dominio está listado en los motores de búsqueda y deseas +proporcionar una página de error más agradable o específicamente para indicar a los indexadores de búsqueda que +las páginas del dominio ya no existen. + +Otro beneficio de tener este host es rastrear los registros de visitas a él y +ver los referentes. diff --git a/frontend/src/locale/src/HelpDoc/es/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/es/ProxyHosts.md new file mode 100644 index 0000000..57994b7 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/ProxyHosts.md @@ -0,0 +1,7 @@ +## ¿Qué es un Host Proxy? + +Un Host Proxy es el punto de entrada para un servicio web que deseas reenviar. + +Proporciona terminación SSL opcional para tu servicio que podría no tener soporte SSL integrado. + +Los Hosts Proxy son el uso más común del Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/es/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/es/RedirectionHosts.md new file mode 100644 index 0000000..7a05a30 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/RedirectionHosts.md @@ -0,0 +1,7 @@ +## ¿Qué es un Host de Redirección? + +Un Host de Redirección redirigirá las solicitudes del dominio entrante e impulsará al +visitante a otro dominio. + +La razón más común para usar este tipo de host es cuando tu sitio web cambia de +dominios pero aún tienes enlaces de motores de búsqueda o referencias apuntando al dominio anterior. diff --git a/frontend/src/locale/src/HelpDoc/es/Streams.md b/frontend/src/locale/src/HelpDoc/es/Streams.md new file mode 100644 index 0000000..f085352 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/Streams.md @@ -0,0 +1,6 @@ +## ¿Qué es un Stream? + +Una característica relativamente nueva para Nginx, un Stream servirá para reenviar tráfico TCP/UDP +directamente a otra computadora en la red. + +Si estás ejecutando servidores de juegos, FTP o servidores SSH esto puede ser muy útil. diff --git a/frontend/src/locale/src/HelpDoc/es/index.ts b/frontend/src/locale/src/HelpDoc/es/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/es/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/et/AccessLists.md b/frontend/src/locale/src/HelpDoc/et/AccessLists.md new file mode 100644 index 0000000..320214c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/AccessLists.md @@ -0,0 +1,7 @@ +## Mis on juurdepääsuloend? + +Ligipääsuloendid pakuvad konkreetsete klientide IP-aadresside musta või valget nimekirja koos puhverserverite autentimisega põhilise HTTP-autentimise kaudu. + +Saate ühe juurdepääsuloendi jaoks konfigureerida mitu kliendireeglit, kasutajanime ja parooli ning seejärel rakendada neid ühele või mitmele _puhverserverile_. + +See on kõige kasulikum edastatud veebiteenuste puhul, millel pole sisseehitatud autentimismehhanisme või kui soovite kaitsta tundmatute klientide eest. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/et/Certificates.md b/frontend/src/locale/src/HelpDoc/et/Certificates.md new file mode 100644 index 0000000..551b57c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/Certificates.md @@ -0,0 +1,26 @@ +## Sertifikaatide abi + +### HTTP-sertifikaat + +HTTP-valideeritud sertifikaat tähendab, et Let's Encrypti serverid + +proovivad teie domeenidega ühendust luua HTTP (mitte HTTPS!) kaudu ja kui see õnnestub, +väljastavad nad teile sertifikaadi. + +Selle meetodi jaoks peate oma domeeni(de) jaoks looma _Proxy Host_, millele pääseb ligi HTTP kaudu ja mis osutab sellele Nginxi installile. Pärast sertifikaadi väljastamist saate muuta _Proxy Host_'i, et seda sertifikaati ka HTTPS +ühenduste jaoks kasutada. Sertifikaadi uuendamiseks tuleb aga _Proxy Host_ ikkagi HTTP-juurdepääsu jaoks konfigureerida. + +See protsess _ei_ toeta metamärke kasutavaid domeene. + +### DNS-sertifikaat + +DNS-i poolt valideeritud sertifikaadi saamiseks peate kasutama DNS-pakkuja pistikprogrammi. Seda DNS-teenuse pakkujat kasutatakse teie domeenis ajutiste kirjete loomiseks ja seejärel pärib Let's +Encrypt nende kirjete kohta päringu, et veenduda, et olete omanik, ja kui see õnnestub, väljastavad nad teile sertifikaadi. + +Selle tüüpi sertifikaadi taotlemiseks ei ole vaja luua _Proxy Host_'i. Samuti ei pea teie _Proxy Host_ olema HTTP-juurdepääsu jaoks konfigureeritud. + +See protsess _toetab_ metamärke kasutavaid domeene. + +### Kohandatud sertifikaat + +Kasutage seda valikut oma SSL-sertifikaadi üleslaadimiseks, mille on esitanud teie enda sertifitseerimisasutus. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/et/DeadHosts.md b/frontend/src/locale/src/HelpDoc/et/DeadHosts.md new file mode 100644 index 0000000..7bec575 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/DeadHosts.md @@ -0,0 +1,9 @@ +## Mis on 404 host? + +404 host on lihtsalt hosti seadistus, mis kuvab 404 lehte. + +See võib olla kasulik, kui teie domeen on otsingumootorites loetletud ja soovite +esitada kenama vealehe või konkreetselt otsingu indekseerijatele öelda, et +domeenilehed enam ei eksisteeri. + +Selle hosti teine eelis on selle külastatavuste logide jälgimine ja suunajate vaatamine. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/et/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/et/ProxyHosts.md new file mode 100644 index 0000000..9d10f52 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/ProxyHosts.md @@ -0,0 +1,7 @@ +## Mis on puhverserver? + +Puhverserver on veebiteenuse sissetuleva andmevoo lõpp-punkt, mida soovite edastada. + +See pakub valikulist SSL-i lõpetamist teie teenusele, millel ei pruugi olla sisseehitatud SSL-tuge. + +Puhverserverid on Nginxi puhverserveri halduri kõige levinum kasutusala. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/et/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/et/RedirectionHosts.md new file mode 100644 index 0000000..8d3eaa0 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Mis on ümbersuunamishost? + +Ümbersuunamishost suunab sissetuleva domeeni päringud ümber ja suunab vaataja teisele domeenile. + +Kõige levinum põhjus seda tüüpi hosti kasutamiseks on see, kui teie veebisaidi domeenid muutuvad, kuid otsingumootori või suunaja lingid osutavad endiselt vanale domeenile. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/et/Streams.md b/frontend/src/locale/src/HelpDoc/et/Streams.md new file mode 100644 index 0000000..55422c1 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/Streams.md @@ -0,0 +1,5 @@ +## Mis on voog? + +Nginxi suhteliselt uus funktsioon, voog, edastab TCP/UDP liiklust otse võrgus olevale teisele arvutile. + +Kui sul on mänguserverid, FTP- või SSH-serverid, võib see kasuks tulla. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/et/index.ts b/frontend/src/locale/src/HelpDoc/et/index.ts new file mode 100644 index 0000000..d0178a5 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/et/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/fr/AccessLists.md b/frontend/src/locale/src/HelpDoc/fr/AccessLists.md new file mode 100644 index 0000000..ca76bf6 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/AccessLists.md @@ -0,0 +1,7 @@ +## Qu'est-ce qu'une liste d'accès ? + +Les listes d'accès permettent de définir une liste noire ou une liste blanche d'adresses IP clientes spécifiques, ainsi que l'authentification des Hôtes Proxy via l'authentification HTTP de base. + +Vous pouvez configurer plusieurs règles client, noms d'utilisateur et mots de passe pour une même liste d'accès, puis l'appliquer à un ou plusieurs Hôtes Proxy. + +Ceci est particulièrement utile pour les services web redirigés qui ne disposent pas de mécanismes d'authentification intégrés ou lorsque vous souhaitez vous protéger contre les clients inconnus. diff --git a/frontend/src/locale/src/HelpDoc/fr/Certificates.md b/frontend/src/locale/src/HelpDoc/fr/Certificates.md new file mode 100644 index 0000000..f5fa325 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/Certificates.md @@ -0,0 +1,23 @@ +## Aide concernant les certificats + +### Certificat HTTP + +Un certificat HTTP validé signifie que les serveurs de Let's Encrypt testeront d'accéder à vos domaines via HTTP (et non HTTPS !). En cas de succès, ils émettront votre certificat. + +Pour cette méthode, vous devrez créer un Hôte Proxy pour votre ou vos domaines. Cet Hôte Proxy devra être accessible via HTTP et pointer vers cette installation Nginx. Une fois le certificat émis, vous pourrez modifier l'Hôte Proxy pour qu'il utilise également ce certificat pour les connexions HTTPS. Cependant, l'Hôte Proxy devra toujours être configuré pour l'accès HTTP afin que le certificat puisse être renouvelé. + +Ce processus ne prend pas en charge les domaines génériques. + +### Certificat DNS + +Un certificat DNS validé nécessite l'utilisation du plugin Fournisseur DNS. Fournisseur DNS créera des enregistrements temporaires sur votre domaine. Let's Encrypt interrogera ensuite ces enregistrements pour vérifier que vous en êtes bien le propriétaire. En cas de succès, votre certificat sera émis. + +Il n'est pas nécessaire de créer un Hôte Proxy avant de demander ce type de certificat. + +Il n'est pas non plus nécessaire de configurer votre Hôte Proxy pour l'accès HTTP. + +Ce processus prend en charge les domaines génériques. + +## Certificat personnalisé + +Utilisez cette option pour importer votre propre certificat SSL, fourni par votre autorité de certification. diff --git a/frontend/src/locale/src/HelpDoc/fr/DeadHosts.md b/frontend/src/locale/src/HelpDoc/fr/DeadHosts.md new file mode 100644 index 0000000..8d22c29 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/DeadHosts.md @@ -0,0 +1,7 @@ +## Qu'est-ce qu'un serveur 404 ? + +Un Hôte 404 est simplement un hôte configuré pour afficher une page 404. + +Cela peut s'avérer utile lorsque votre domaine est indexé par les moteurs de recherche et que vous souhaitez fournir une page d'erreur plus conviviale ou, plus précisément, indiquer aux moteurs de recherche que les pages du domaine n'existent plus. + +Un autre avantage de cet hôte est la possibilité de suivre les journaux et de consulter les sites référenceurs. diff --git a/frontend/src/locale/src/HelpDoc/fr/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/fr/ProxyHosts.md new file mode 100644 index 0000000..10c75b9 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/ProxyHosts.md @@ -0,0 +1,7 @@ +## Qu'est-ce qu'un hôte proxy ? + +Un Hôte Proxy est le point de terminaison entrant d'un service web que vous souhaitez rediriger. + +Il assure la terminaison SSL optionnelle pour votre service qui ne prend pas en charge SSL nativement. + +Les Hôtes Proxy constituent l'utilisation la plus courante du Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/fr/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/fr/RedirectionHosts.md new file mode 100644 index 0000000..f12319d --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Qu'est-ce qu'un serveur de redirection ? + +Un Hôte de Redirection redirige les requêtes provenant du domaine entrant vers un autre domaine. + +On utilise généralement ce type d'hôte lorsque votre site web change de domaine, mais que des liens provenant des moteurs de recherche ou des sites référenceurs pointent toujours vers l'ancien domaine. diff --git a/frontend/src/locale/src/HelpDoc/fr/Streams.md b/frontend/src/locale/src/HelpDoc/fr/Streams.md new file mode 100644 index 0000000..74668b9 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/Streams.md @@ -0,0 +1,5 @@ +## Qu'est-ce qu'un Stream ? + +Fonctionnalité relativement récente de Nginx, un Stream permet de rediriger le trafic TCP/UDP directement vers un autre ordinateur du réseau. + +Si vous gérez des serveurs de jeux, FTP ou SSH, cela peut s'avérer très utile. diff --git a/frontend/src/locale/src/HelpDoc/fr/index.ts b/frontend/src/locale/src/HelpDoc/fr/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fr/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/ga/AccessLists.md b/frontend/src/locale/src/HelpDoc/ga/AccessLists.md new file mode 100644 index 0000000..64b35e9 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/AccessLists.md @@ -0,0 +1,7 @@ +## Cad is Liosta Rochtana ann? + +Soláthraíonn Liostaí Rochtana liosta dubh nó liosta bán de sheoltaí IP cliant ar leith mar aon le fíordheimhniú do na hÓstaigh Seachfhreastalaí trí Fhíordheimhniú Bunúsach HTTP. + +Is féidir leat rialacha cliant, ainmneacha úsáideora agus pasfhocail iolracha a chumrú le haghaidh Liosta Rochtana aonair agus ansin iad sin a chur i bhfeidhm ar _Óstach Seachfhreastalaí_ amháin nó níos mó. + +Tá sé seo an-úsáideach i gcás seirbhísí gréasáin atreoraithe nach bhfuil meicníochtaí fíordheimhnithe ionsuite iontu nó nuair is mian leat cosaint a dhéanamh ar chliaint anaithnide. diff --git a/frontend/src/locale/src/HelpDoc/ga/Certificates.md b/frontend/src/locale/src/HelpDoc/ga/Certificates.md new file mode 100644 index 0000000..c69d7b4 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/Certificates.md @@ -0,0 +1,21 @@ +## Cabhair le Deimhnithe + +### Teastas HTTP + +Ciallaíonn deimhniú bailíochtaithe HTTP go ndéanfaidh freastalaithe Let's Encrypt iarracht teacht ar do fhearainn thar HTTP (ní HTTPS!) agus má éiríonn leo, eiseoidh siad do theastas. + +Chun an modh seo a dhéanamh, beidh ort _Óstach Proxy_ a chruthú do do fhearainn(eanna) atá inrochtana le HTTP agus ag pointeáil chuig an suiteáil Nginx seo. Tar éis deimhniú a thabhairt, is féidir leat an _Óstach Proxy_ a mhodhnú chun an deimhniú seo a úsáid le haghaidh naisc HTTPS freisin. Mar sin féin, beidh ort an _Óstach Proxy_ a chumrú fós le haghaidh rochtain HTTP chun go ndéanfar an deimhniú a athnuachan. + +_Ní thacaíonn_ an próiseas seo le fearainn fiáine. + +### Teastas DNS + +Éilíonn deimhniú bailíochtaithe DNS ort breiseán Soláthraí DNS a úsáid. Úsáidfear an Soláthraí DNS seo chun taifid shealadacha a chruthú ar do fhearann agus ansin déanfaidh Let's Encrypt fiosrúchán ar na taifid sin lena chinntiú gurb tusa an t-úinéir agus má éiríonn leo, eiseoidh siad do theastas. + +Ní gá duit _Óstach Proxy_ a chruthú sula n-iarrann tú an cineál seo teastais. Ní gá duit do _Óstach Proxy_ a chumrú le haghaidh rochtana HTTP ach an oiread. + +_Tacaíonn_ an próiseas seo le fearainn fiáine. + +### Teastas Saincheaptha + +Úsáid an rogha seo chun do Theastas SSL féin a uaslódáil, mar a sholáthraíonn d'Údarás Deimhnithe féin é. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/ga/DeadHosts.md b/frontend/src/locale/src/HelpDoc/ga/DeadHosts.md new file mode 100644 index 0000000..f6d20ed --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/DeadHosts.md @@ -0,0 +1,7 @@ +## Cad is Óstach 404 ann? + +Is socrú óstach a thaispeánann leathanach 404 é Óstach 404. + +Is féidir leis seo a bheith úsáideach nuair a bhíonn do fhearann liostaithe in innill chuardaigh agus más mian leat leathanach earráide níos deise a sholáthar nó a chur in iúl do na hinnéacsóirí cuardaigh go sonrach nach bhfuil na leathanaigh fearainn ann a thuilleadh. + +Buntáiste eile a bhaineann leis an óstach seo a bheith agat ná go bhfeictear na logaí le haghaidh amas agus go bhfeictear na tagairtí. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md new file mode 100644 index 0000000..542b3ec --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/ProxyHosts.md @@ -0,0 +1,7 @@ +## Cad is Óstach Seachfhreastalaí ann? + +Is é Óstach Seachfhreastalaí an críochphointe isteach do sheirbhís ghréasáin ar mhaith leat a atreorú. + +Soláthraíonn sé foirceannadh SSL roghnach do do sheirbhís nach bhfuil tacaíocht SSL ionsuite inti b'fhéidir. + +Is iad Óstaigh Seachfhreastalaí an úsáid is coitianta a bhaintear as Bainisteoir Seachfhreastalaí Nginx. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md new file mode 100644 index 0000000..5995c14 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Cad is Óstach Athsheolta ann? + +Déanfaidh Óstach Athsheolta iarratais a atreorú ón bhfearann ag teacht isteach agus an breathnóir a bhrú chuig fearann eile. + +Is é an chúis is coitianta le húsáid a bhaint as an gcineál seo óstála ná nuair a athraíonn do shuíomh Gréasáin fearainn ach go bhfuil naisc innill chuardaigh nó atreoraithe agat fós ag tagairt don seanfhearann. diff --git a/frontend/src/locale/src/HelpDoc/ga/Streams.md b/frontend/src/locale/src/HelpDoc/ga/Streams.md new file mode 100644 index 0000000..cac45da --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/Streams.md @@ -0,0 +1,5 @@ +## Cad is Sruth ann? + +Gné réasúnta nua do Nginx is ea Sruth a sheolfaidh trácht TCP/UDP go díreach chuig ríomhaire eile ar an líonra. + +Más freastalaithe cluichí, freastalaithe FTP nó SSH atá á rith agat, d’fhéadfadh sé seo a bheith úsáideach. diff --git a/frontend/src/locale/src/HelpDoc/ga/index.ts b/frontend/src/locale/src/HelpDoc/ga/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ga/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/hu/AccessLists.md b/frontend/src/locale/src/HelpDoc/hu/AccessLists.md new file mode 100644 index 0000000..c3a62dd --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/AccessLists.md @@ -0,0 +1,7 @@ +## Mi az a hozzáférési lista? + +A hozzáférési listák feketelistát vagy fehérlistát biztosítanak meghatározott kliens IP-címekhez, valamint alap HTTP-hitelesítést (Basic HTTP Authentication) a proxy kiszolgálókhoz. + +Egyetlen hozzáférési listához több kliensszabályt, felhasználónevet és jelszót is beállíthatsz, majd ezt alkalmazhatod egy vagy több _Proxy Kiszolgáló_-ra. + +Ez különösen hasznos olyan továbbított webszolgáltatásoknál, amelyekben nincs beépített hitelesítési mechanizmus, vagy amikor ismeretlen kliensektől szeretnél védeni. diff --git a/frontend/src/locale/src/HelpDoc/hu/Certificates.md b/frontend/src/locale/src/HelpDoc/hu/Certificates.md new file mode 100644 index 0000000..8df4a23 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/Certificates.md @@ -0,0 +1,21 @@ +## Tanúsítványok súgó + +### HTTP tanúsítvány + +A HTTP érvényes tanúsítvány azt jelenti, hogy a Let's Encrypt szerverek megpróbálják elérni a domaineket HTTP-n keresztül (nem HTTPS-en!), és ha sikerül, kiállítják a tanúsítványt. + +Ehhez a módszerhez létre kell hoznod egy _Proxy Kiszolgáló_-t a domain(ek)hez, amely HTTP-n keresztül elérhető és erre az Nginx telepítésre mutat. Miután a tanúsítvány megérkezett, módosíthatod a _Proxy Kiszolgáló_-t, hogy ezt a tanúsítványt használja a HTTPS kapcsolatokhoz is. Azonban a _Proxy Kiszolgáló_-nak továbbra is konfigurálva kell lennie HTTP hozzáféréshez, hogy a tanúsítvány megújulhasson. + +Ez a folyamat _nem_ támogatja a helyettesítő karakteres domaineket. + +### DNS tanúsítvány + +A DNS érvényes tanúsítvány megköveteli, hogy DNS szolgáltató plugint használj. Ez a DNS szolgáltató ideiglenes rekordokat hoz létre a domainen, majd a Let's Encrypt lekérdezi ezeket a rekordokat, hogy megbizonyosodjon a tulajdonjogról, és ha sikeres, kiállítják a tanúsítványt. + +Nem szükséges előzetesen _Proxy Kiszolgáló_-t létrehozni az ilyen típusú tanúsítvány igényléséhez. Nem is kell a _Proxy Kiszolgáló_-t HTTP hozzáférésre konfigurálni. + +Ez a folyamat _támogatja_ a helyettesítő karakteres domaineket. + +### Egyéni tanúsítvány + +Ezt az opciót használd a saját SSL tanúsítvány feltöltéséhez, amelyet a saját tanúsítványkibocsátód biztosított. diff --git a/frontend/src/locale/src/HelpDoc/hu/DeadHosts.md b/frontend/src/locale/src/HelpDoc/hu/DeadHosts.md new file mode 100644 index 0000000..78ad57e --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/DeadHosts.md @@ -0,0 +1,7 @@ +## Mi az a 404-es Kiszolgáló? + +A 404-es Kiszolgáló egyszerűen egy olyan kiszolgáló beállítás, amely egy 404-es oldalt jelenít meg. + +Ez akkor lehet hasznos, ha a domained szerepel a keresőmotorokban, és egy szebb hibaoldalt szeretnél nyújtani, vagy kifejezetten jelezni akarod a keresőrobotoknak, hogy a domain oldalai már nem léteznek. + +Ennek a kiszolgálónak egy további előnye, hogy nyomon követheted a rá érkező találatokat a naplókban, és megtekintheted a hivatkozó oldalakat. diff --git a/frontend/src/locale/src/HelpDoc/hu/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/hu/ProxyHosts.md new file mode 100644 index 0000000..74a333b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/ProxyHosts.md @@ -0,0 +1,7 @@ +## Mi az a Proxy Kiszolgáló? + +A Proxy Kiszolgáló egy bejövő végpont egy olyan webszolgáltatáshoz, amelyet továbbítani szeretnél. + +Opcionális SSL lezárást biztosít a szolgáltatásodhoz, amelyben esetleg nincs beépített SSL támogatás. + +A Proxy Kiszolgálók az Nginx Proxy Manager leggyakoribb felhasználási módjai. diff --git a/frontend/src/locale/src/HelpDoc/hu/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/hu/RedirectionHosts.md new file mode 100644 index 0000000..576989a --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Mi az az Átirányító Kiszolgáló? + +Az Átirányító Kiszolgáló a bejövő domainre érkező kéréseket átirányítja, és a látogatót egy másik domainre küldi. + +Ennek a kiszolgálótípusnak a leggyakoribb használati oka az, amikor a weboldalad domaint vált, de a keresőkben vagy a hivatkozó oldalakon még mindig a régi domainre mutató linkek vannak. diff --git a/frontend/src/locale/src/HelpDoc/hu/Streams.md b/frontend/src/locale/src/HelpDoc/hu/Streams.md new file mode 100644 index 0000000..1785565 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/Streams.md @@ -0,0 +1,5 @@ +## Mi az a Stream? + +Az Nginx egy viszonylag új funkciója, a Stream arra szolgál, hogy a TCP/UDP forgalmat közvetlenül továbbítsa a hálózat egy másik számítógépére. + +Ha játékszervereket, FTP vagy SSH szervereket futtatsz, ez hasznos lehet. diff --git a/frontend/src/locale/src/HelpDoc/hu/index.ts b/frontend/src/locale/src/HelpDoc/hu/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/hu/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/id/AccessLists.md b/frontend/src/locale/src/HelpDoc/id/AccessLists.md new file mode 100644 index 0000000..33c0b49 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/AccessLists.md @@ -0,0 +1,7 @@ +## Apa itu Daftar Akses? + +Daftar Akses menyediakan daftar hitam atau daftar putih alamat IP klien tertentu beserta autentikasi untuk Host Proxy melalui Autentikasi HTTP Basic. + +Anda dapat mengonfigurasi beberapa aturan klien, nama pengguna, dan kata sandi untuk satu Daftar Akses lalu menerapkannya ke satu atau lebih _Host Proxy_. + +Ini paling berguna untuk layanan web yang diteruskan yang tidak memiliki mekanisme autentikasi bawaan atau ketika Anda ingin melindungi dari klien yang tidak dikenal. diff --git a/frontend/src/locale/src/HelpDoc/id/Certificates.md b/frontend/src/locale/src/HelpDoc/id/Certificates.md new file mode 100644 index 0000000..d58c922 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/Certificates.md @@ -0,0 +1,32 @@ +## Bantuan Sertifikat + +### Sertifikat HTTP + +Sertifikat yang divalidasi HTTP berarti server Let's Encrypt akan +mencoba menjangkau domain Anda melalui HTTP (bukan HTTPS!) dan jika berhasil, mereka +akan menerbitkan sertifikat Anda. + +Untuk metode ini, Anda harus membuat _Host Proxy_ untuk domain Anda yang +dapat diakses dengan HTTP dan mengarah ke instalasi Nginx ini. Setelah sertifikat +diberikan, Anda dapat mengubah _Host Proxy_ agar juga menggunakan sertifikat ini untuk HTTPS +koneksi. Namun, _Host Proxy_ tetap perlu dikonfigurasi untuk akses HTTP +agar sertifikat dapat diperpanjang. + +Proses ini _tidak_ mendukung domain wildcard. + +### Sertifikat DNS + +Sertifikat yang divalidasi DNS mengharuskan Anda menggunakan plugin Penyedia DNS. Penyedia DNS ini +akan digunakan untuk membuat record sementara pada domain Anda dan kemudian Let's +Encrypt akan menanyakan record tersebut untuk memastikan Anda pemiliknya dan jika berhasil, mereka +akan menerbitkan sertifikat Anda. + +Anda tidak perlu membuat _Host Proxy_ sebelum meminta jenis sertifikat ini. +Anda juga tidak perlu mengonfigurasi _Host Proxy_ untuk akses HTTP. + +Proses ini _mendukung_ domain wildcard. + +### Sertifikat Kustom + +Gunakan opsi ini untuk mengunggah Sertifikat SSL Anda sendiri, sebagaimana disediakan oleh +Certificate Authority Anda. diff --git a/frontend/src/locale/src/HelpDoc/id/DeadHosts.md b/frontend/src/locale/src/HelpDoc/id/DeadHosts.md new file mode 100644 index 0000000..44b92bf --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/DeadHosts.md @@ -0,0 +1,10 @@ +## Apa itu Host 404? + +Host 404 adalah konfigurasi host yang menampilkan halaman 404. + +Ini dapat berguna ketika domain Anda terindeks di mesin pencari dan Anda ingin +menyediakan halaman error yang lebih baik atau secara khusus memberi tahu pengindeks pencarian bahwa +halaman domain tersebut sudah tidak ada. + +Manfaat lain memiliki host ini adalah melacak log untuk akses ke host tersebut dan +melihat perujuk. diff --git a/frontend/src/locale/src/HelpDoc/id/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/id/ProxyHosts.md new file mode 100644 index 0000000..b05939a --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/ProxyHosts.md @@ -0,0 +1,7 @@ +## Apa itu Host Proxy? + +Host Proxy adalah endpoint masuk untuk layanan web yang ingin Anda teruskan. + +Host ini menyediakan terminasi SSL opsional untuk layanan Anda yang mungkin tidak memiliki dukungan SSL bawaan. + +Host Proxy adalah penggunaan paling umum untuk Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md new file mode 100644 index 0000000..7e31619 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Apa itu Host Pengalihan? + +Host Pengalihan akan mengalihkan permintaan dari domain masuk dan mengarahkan pengunjung ke domain lain. + +Alasan paling umum menggunakan jenis host ini adalah ketika situs Anda berpindah domain tetapi masih ada tautan mesin pencari atau perujuk yang mengarah ke domain lama. diff --git a/frontend/src/locale/src/HelpDoc/id/Streams.md b/frontend/src/locale/src/HelpDoc/id/Streams.md new file mode 100644 index 0000000..e74f6b4 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/Streams.md @@ -0,0 +1,6 @@ +## Apa itu Stream? + +Fitur yang relatif baru untuk Nginx, Stream berfungsi untuk meneruskan trafik TCP/UDP +langsung ke komputer lain di jaringan. + +Jika Anda menjalankan server game, FTP, atau SSH, ini bisa sangat membantu. diff --git a/frontend/src/locale/src/HelpDoc/id/index.ts b/frontend/src/locale/src/HelpDoc/id/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/id/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/index.ts b/frontend/src/locale/src/HelpDoc/index.ts new file mode 100644 index 0000000..7d4af5f --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/index.ts @@ -0,0 +1,39 @@ +import * as bg from "./bg/index"; +import * as de from "./de/index"; +import * as pt from "./pt/index"; +import * as en from "./en/index"; +import * as es from "./es/index"; +import * as et from "./et/index"; +import * as fr from "./fr/index"; +import * as ga from "./ga/index"; +import * as id from "./id/index"; +import * as it from "./it/index"; +import * as ja from "./ja/index"; +import * as ko from "./ko/index"; +import * as nl from "./nl/index"; +import * as pl from "./pl/index"; +import * as ru from "./ru/index"; +import * as sk from "./sk/index"; +import * as cs from "./cs/index"; +import * as vi from "./vi/index"; +import * as zh from "./zh/index"; +import * as tr from "./tr/index"; +import * as hu from "./hu/index"; + +const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu }; + + +const fallbackLang = "en"; + +export const getHelpFile = (lang: string, section: string): string => { + if (typeof items[lang] !== "undefined" && typeof items[lang][section] !== "undefined") { + return items[lang][section].default; + } + // Fallback to English + if (typeof items[fallbackLang] !== "undefined" && typeof items[fallbackLang][section] !== "undefined") { + return items[fallbackLang][section].default; + } + throw new Error(`Cannot load help doc for ${lang}-${section}`); +}; + +export default items; diff --git a/frontend/src/locale/src/HelpDoc/it/AccessLists.md b/frontend/src/locale/src/HelpDoc/it/AccessLists.md new file mode 100644 index 0000000..fa45ba5 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/AccessLists.md @@ -0,0 +1,7 @@ +## Che cos'è una Lista di Accesso? + +La Lista di Accesso fornisce una blacklist o una whitelist di indirizzi IP specifici dei client insieme all'autenticazione per gli host proxy tramite autenticazione HTTP di base. + +È possibile configurare più regole client, nomi utente e password per un singolo lista di accesso e quindi applicarlo a uno o più host proxy. + +Ciò è particolarmente utile per i servizi web inoltrati che non dispongono di meccanismi di autenticazione integrati o quando si desidera proteggersi da client sconosciuti. diff --git a/frontend/src/locale/src/HelpDoc/it/Certificates.md b/frontend/src/locale/src/HelpDoc/it/Certificates.md new file mode 100644 index 0000000..18cec7f --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/Certificates.md @@ -0,0 +1,24 @@ +## Guida sui Certificati + +### Certificato HTTP + +Un certificato convalidato HTTP significa che i server Let's Encrypttenteranno di raggiungere i tuoi domini tramite HTTP (non HTTPS!) e, in caso di esito positivo, emetteranno il tuo certificato. + +Per questo metodo, dovrai creare un _Proxy Host_ per i tuoi domini chesia accessibile con HTTP e che punti a questa installazione Nginx. +Dopo che il certificato è stato rilasciato, puoi modificare il _Proxy Host_ per utilizzare questo certificato anche per le connessioni HTTPS. +Tuttavia, il _Proxy Host_ dovrà comunque essere configurato per l'accesso HTTP affinché il certificato possa essere rinnovato. + +Questo processo _non_ supporta i domini wildcard. + +### Certificato DNS + +Un certificato convalidato dal DNS richiede l'uso di un plugin DNS Provider. Questo DNS Provider verrà utilizzato per creare record temporanei sul tuo dominio, +quindi Let's Encrypt interrogherà tali record per assicurarsi che tu sia il proprietario e, in caso di esito positivo,rilascerà il tuo certificato. + +Non è necessario creare un _Proxy Host_ prima di richiedere questo tipo di certificato. Non è nemmeno necessario configurare il tuo _proxy host_ per l'accesso HTTP. + +Questo processo _supporta_ i domini wildcard. + +### Certificato personalizzato + +Utilizza questa opzione per caricare il tuo certificato SSL, fornito dalla tua autorità di certificazione. diff --git a/frontend/src/locale/src/HelpDoc/it/DeadHosts.md b/frontend/src/locale/src/HelpDoc/it/DeadHosts.md new file mode 100644 index 0000000..c5be4b0 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/DeadHosts.md @@ -0,0 +1,9 @@ +## Che cos'è un Host 404? + +Un Host 404 è semplicemente una configurazione host che mostra una pagina 404. + +Questo può essere utile quando il tuo dominio è elencato nei motori di ricerca e desideri fornire una pagina di errore più gradevole o specificare agli +indicizzatori di ricerca che le pagine del dominio non esistono più. + +Un altro vantaggio di avere questo host è quello di tracciare i log degli accessi e +visualizzare i referrer. diff --git a/frontend/src/locale/src/HelpDoc/it/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/it/ProxyHosts.md new file mode 100644 index 0000000..bc1dd53 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/ProxyHosts.md @@ -0,0 +1,7 @@ +## Che cos'è un Proxy Host? + +Un host proxy è l'endpoint in entrata per un servizio web che si desidera inoltrare. + +Fornisce la terminazione SSL opzionale per il servizio che potrebbe non avere il supporto SSL integrato. + +Gli host proxy sono l'uso più comune per Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/it/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/it/RedirectionHosts.md new file mode 100644 index 0000000..9971774 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Che cos'è un Host di reindirizzamento? + +Un Host di reindirizzamento reindirizza le richieste provenienti dal dominio in entrata e indirizza il +visitatore verso un altro dominio. + +Il motivo più comune per utilizzare questo tipo di host è quando il tuo sito web cambia +dominio, ma hai ancora link di motori di ricerca o referrer che puntano al vecchio dominio. diff --git a/frontend/src/locale/src/HelpDoc/it/Streams.md b/frontend/src/locale/src/HelpDoc/it/Streams.md new file mode 100644 index 0000000..aa95969 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/Streams.md @@ -0,0 +1,6 @@ +## Che cos'è uno Stream? + +Una funzionalità relativamente nuova per Nginx, uno Stream serve a inoltrare il traffico TCP/UDP +direttamente a un altro computer sulla rete. + +Se gestisci server di gioco, FTP o SSH, questa funzionalità può rivelarsi molto utile. diff --git a/frontend/src/locale/src/HelpDoc/it/index.ts b/frontend/src/locale/src/HelpDoc/it/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/it/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/ja/AccessLists.md b/frontend/src/locale/src/HelpDoc/ja/AccessLists.md new file mode 100644 index 0000000..aaeea87 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/AccessLists.md @@ -0,0 +1,8 @@ +## アクセスリストとは + +アクセスリストは特定のクライアントIPへのブラックリストとホワイトリストを提供し、ベーシック認証によるプロキシホストへの認証を可能にします。 + +複数のクライアントルールやユーザー名とパスワードを一つのアクセスリストに設定し、一つ以上の _プロキシホスト_ に適応することができます。 + +これは認証システムを持たないサービスや不明なクライアントからの保護が必要な場合に有効です。 + diff --git a/frontend/src/locale/src/HelpDoc/ja/Certificates.md b/frontend/src/locale/src/HelpDoc/ja/Certificates.md new file mode 100644 index 0000000..dbe2cfb --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/Certificates.md @@ -0,0 +1,21 @@ +## 証明書 + +### HTTP 証明書 + +HTTPによって検証された証明書はLet's EncryptサーバーがHTTPでドメインにアクセスを試みサーバーを管理していることを確認できた場合に発行される証明書です。 + +この方法では、HTTPアクセス可能でこのNginxを指しているドメインに対して _プロキシホスト_ を作成する必要があります。証明書が発行された後は、 _プロキシホスト_ を編集してこの証明書をHTTPS接続に使用するように設定できます。ただし、証明書の更新には、_プロキシホスト_ がHTTP接続用に設定された状態を維持する必要があります。 + +この方法はワイルドカードのドメインをサポート _していません_ 。 + +### DNS 証明書 + +DNSによって検証された証明書にはDNSプロバイダープラグインが必要です。このプロバイダーはドメイン上に一時レコードを作成するために使用されます。その後Let's Encryptサーバーがそのレコードを参照し、あなたが所有していることを確認できると証明書が発行されます。 + +このタイプの証明書を作成する際に、 _プロキシホスト_ を作成する必要はありません。また、_プロキシホスト_ をHTTPアクセス用に設定する必要もありません。 + +この方法はワイルドカードのドメインをサポート _します_ 。 + +### カスタム証明書 + +このオプションでは、あなたの証明書認証局によって提供された自身の証明書をアップロードして使用できます。 diff --git a/frontend/src/locale/src/HelpDoc/ja/DeadHosts.md b/frontend/src/locale/src/HelpDoc/ja/DeadHosts.md new file mode 100644 index 0000000..4c6aad9 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/DeadHosts.md @@ -0,0 +1,7 @@ +## 404ホストとはなんですか? + +404ホストとは、単に404ページを表示するよう設定されたホストです。 + +これは、検索エンジンに登録されたドメインに分かりやすいエラーページを提供したい場合や、検索エンジンのインデクサーにドメインページがもう存在しないことを伝えたい場合に便利です。 + +このホストを持つもう一つの利点は、アクセスログを追跡し、参照元を確認できることです。 diff --git a/frontend/src/locale/src/HelpDoc/ja/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/ja/ProxyHosts.md new file mode 100644 index 0000000..e01d0a6 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/ProxyHosts.md @@ -0,0 +1,7 @@ +## プロキシホストとは何ですか? + +プロキシホストは転送したいwebサービスの受信エンドポイントです。 + +サービスにSSLサーバーが組み込まれていない場合でも、オプションでSSL終端機能を提供します。 + +プロキシホストはNginx Proxy Managerのもっとも一般的な使用方法です。 \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/ja/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/ja/RedirectionHosts.md new file mode 100644 index 0000000..e1fb588 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/RedirectionHosts.md @@ -0,0 +1,5 @@ +## リダイレクトホストとは何ですか? + +リダイレクトホストは受信したリクエストを別のドメインにリダイレクトして訪問者に表示します。 + +このタイプのもっとも一般的な使用理由は、webサイトのドメインが変更されたが検索エンジンやリンクが古いドメインを指し続けている場合です。 diff --git a/frontend/src/locale/src/HelpDoc/ja/Streams.md b/frontend/src/locale/src/HelpDoc/ja/Streams.md new file mode 100644 index 0000000..8fc38b7 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/Streams.md @@ -0,0 +1,5 @@ +## ストリームとは何ですか? + +Nginxの比較的新しい機能であるストリームは、TCP/UDPトラフィックをネットワーク上の別のコンピュータに直接転送します。 + +ゲームサーバー、FTPサーバー、SSHサーバーを運用している場合に便利です。 diff --git a/frontend/src/locale/src/HelpDoc/ja/index.ts b/frontend/src/locale/src/HelpDoc/ja/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ja/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/ko/AccessLists.md b/frontend/src/locale/src/HelpDoc/ko/AccessLists.md new file mode 100644 index 0000000..8e96701 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/AccessLists.md @@ -0,0 +1,11 @@ +## 접근 정책이란? + +접근 정책은 특정 클라이언트 IP 주소를 허용하거나 거부할 수 있으며, +프록시 호스트에 기본 HTTP 인증(Basic Auth) 을 적용할 수 있는 기능입니다. + +하나의 접근 목록에 여러 클라이언트 규칙과 사용자 이름, 비밀번호를 추가한 뒤 +이를 하나 이상의 프록시 호스트에 적용할 수 있습니다. + +이 기능은 인증 기능이 없는 웹 서비스에 인증을 추가하거나, +알 수 없는 클라이언트로부터 서비스를 보호할 때 유용합니다. + diff --git a/frontend/src/locale/src/HelpDoc/ko/Certificates.md b/frontend/src/locale/src/HelpDoc/ko/Certificates.md new file mode 100644 index 0000000..660f396 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/Certificates.md @@ -0,0 +1,28 @@ +## 인증서 도움말 + +### HTTP 인증서 + +HTTP 검증 방식의 인증서는 Let's Encrypt 서버가 **HTTPS가 아닌 HTTP로** 해당 도메인에 접속을 시도해 응답이 확인되면 인증서를 발급하는 방식입니다. + +이 방식을 사용하려면 도메인에 대한 **프록시 호스트가 미리 생성되어 있어야 하며**, HTTP로 접근할 수 있어야 하고 Nginx Proxy Manager가 설치된 서버를 가리켜야 합니다. 인증서가 발급된 이후에는 해당 프록시 호스트에 HTTPS용 인증서를 적용할 수 있습니다. + +다만, **인증서 자동 갱신을 위해서는 HTTP 접근이 계속 필요합니다.** + +이 방식은 **와일드카드 도메인을 지원하지 않습니다.** + +--- + +### DNS 인증서 + +DNS 검증 방식의 인증서는 DNS 공급자 플러그인을 사용해야 합니다. 이 플러그인은 도메인에 임시 DNS 레코드를 생성하며, Let's Encrypt는 해당 레코드를 조회해 도메인 소유 여부를 확인합니다. 검증이 성공하면 인증서가 발급됩니다. + +이 방식은 인증서를 요청하기 전에 **프록시 호스트를 생성할 필요가 없으며**, 프록시 호스트에 HTTP 접근을 설정할 필요도 없습니다. + +이 방식은 **와일드카드 도메인을 지원합니다.** + +--- + +### 사용자 지정 인증서 + +이 옵션을 사용하면 직접 보유한 인증 기관(CA)에서 발급한 SSL 인증서를 직접 업로드하여 사용할 수 있습니다. + diff --git a/frontend/src/locale/src/HelpDoc/ko/DeadHosts.md b/frontend/src/locale/src/HelpDoc/ko/DeadHosts.md new file mode 100644 index 0000000..032a23c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/DeadHosts.md @@ -0,0 +1,10 @@ +## 404 호스트란? + +404 호스트는 404 오류 페이지를 표시하도록 구성된 호스트입니다. + +이 기능은 도메인이 검색 엔진에 이미 색인되어 있을 때, +더 깔끔한 오류 페이지를 제공하거나 해당 페이지가 더 이상 존재하지 않음을 +검색 엔진에게 명확하게 알려야 할 때 유용합니다. + +또한 404 호스트를 사용하면 접근 로그를 확인하고, 어떤 경로(Referrer)를 통해 들어왔는지 추적할 수 있다는 장점도 있습니다. + diff --git a/frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md new file mode 100644 index 0000000..3212fde --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/ProxyHosts.md @@ -0,0 +1,8 @@ +## 프록시 호스트란? + +프록시 호스트는 외부에서 들어오는 웹 요청을 받아 지정한 전달 대상으로 전달하는 역할을 합니다. + +원래 SSL을 지원하지 않는 대상이라도, 프록시 호스트를 통해 SSL(HTTPS) 연결을 적용할 수 있습니다. + +프록시 호스트는 Nginx Proxy Manager에서 가장 일반적으로 사용되는 기능입니다. + diff --git a/frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md new file mode 100644 index 0000000..7494885 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/RedirectionHosts.md @@ -0,0 +1,7 @@ +## 리다이렉션 호스트란? + +리다이렉션 호스트는 외부에서 들어오는 도메인 요청을 다른 도메인으로 자동 이동(리다이렉트)시키는 역할을 합니다. + +이 유형의 호스트는 주로 웹사이트의 도메인이 변경되었지만, +검색 엔진이나 다른 사이트에 이전 도메인 링크가 남아 있을 때 사용하면 가장 효과적입니다. + diff --git a/frontend/src/locale/src/HelpDoc/ko/Streams.md b/frontend/src/locale/src/HelpDoc/ko/Streams.md new file mode 100644 index 0000000..45107b2 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/Streams.md @@ -0,0 +1,7 @@ +## 호스트 스트림이란? + +호스트 스트림은 비교적 최근에 Nginx에 추가된 기능으로, +TCP/UDP 트래픽을 네트워크 내의 다른 컴퓨터로 직접 전달하는 데 사용됩니다. + +게임 서버나 FTP, SSH 서버 등을 운영할 때 유용하게 사용할 수 있습니다. + diff --git a/frontend/src/locale/src/HelpDoc/ko/index.ts b/frontend/src/locale/src/HelpDoc/ko/index.ts new file mode 100644 index 0000000..33e015c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ko/index.ts @@ -0,0 +1,7 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; + diff --git a/frontend/src/locale/src/HelpDoc/nl/AccessLists.md b/frontend/src/locale/src/HelpDoc/nl/AccessLists.md new file mode 100644 index 0000000..875627d --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/AccessLists.md @@ -0,0 +1,7 @@ +## Wat is een Toegangslijst? + +Toeganslijsten bieden een zwarte- of witte lijst van specifieke client IP-adressen samen met authenticatie voor de Proxy Hosts via Basic HTTP Authenticatie. + +Je kan meerdere client regels, gebruikersnamen en wachtwoorden voor een enkele Toegangslijst configureren en toepassen op één of meerdere _Proxy Hosts_. + +Dit is het meest nuttig voor doorgestuurde webdiensten die geen authenticatiemechanismen hebben of wanneer je wilt beveiligen tegen onbekende bezoekers. diff --git a/frontend/src/locale/src/HelpDoc/nl/Certificates.md b/frontend/src/locale/src/HelpDoc/nl/Certificates.md new file mode 100644 index 0000000..4477217 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/Certificates.md @@ -0,0 +1,31 @@ +## Certificaten Hulp + +### HTTP Certificaat + +Een HTTP gevalideerd certificaat betekent dat Let's Encrypt servers +zullen proberen om over HTTP te bereiken (niet HTTPS!) en als dat gelukt is, zal +jouw certificaat worden uitgegeven. + +Voor deze zal je een _Proxy Host_ moeten hebben die is toegankelijk via HTTP en +die naar deze Nginx installatie wijst. Nadat een certificaat is uitgegeven kan je +de _Proxy Host_ wijzigen om ook HTTPS toegang te geven. Maar de _Proxy Host_ zal +nog moeten worden geconfigureerd voor HTTP toegang om het certificaat te verlengen. + +Dit proces ondersteunt geen domeinen met wildcards. + +### DNS Certificaat + +Een DNS gevalideerd certificaat zal gebruik maken van een DNS Provider plugin. De +DNS Provider zal tijdelijke records op jouw domein maken en Let's Encrypt zal deze +records opvragen om te controleren of je de eigenaar bent. Als dat is gecontroleerd +is zal Let's Encrypt het certificaat uitgeven. + +Je hebt geen _Proxy Host_ nodig om dit soort certificaat aan te vragen. Je hebt dus +geen HTTP _Proxy Host_ nodig. + +Dit proces ondersteunt _wel_ domeinen met wildcards. + +### Aangepast Certificaat + +Gebruik deze optie om jouw eigen SSL Certificaat te uploaden, zoals +geleverd door jouw eigen Certificate Authority. diff --git a/frontend/src/locale/src/HelpDoc/nl/DeadHosts.md b/frontend/src/locale/src/HelpDoc/nl/DeadHosts.md new file mode 100644 index 0000000..7e26fd5 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/DeadHosts.md @@ -0,0 +1,10 @@ +## Wat is een 404 Host? + +Simpel gezegd is een 404 Host een host setup die een 404 pagina weergeeft. + +Dit kan nuttig zijn wanneer jouw domein is opgegeven in zoekmachines en je wil +een betere foutpagina leveren of specifiek om te zeggen tegen de zoekmachines dat +het domein niet langer bestaat. + +Een ander voordeel van het hebben van een 404 Host is om de logs voor bezoeken +te volgen en de referenties te bekijken. diff --git a/frontend/src/locale/src/HelpDoc/nl/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/nl/ProxyHosts.md new file mode 100644 index 0000000..68f9f4c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/ProxyHosts.md @@ -0,0 +1,7 @@ +## Wat is een Proxy Host? + +Een Proxy Host is de inkomende endpoint voor een webdienst dat je wilt doorsturen. + +Het biedt optionele SSL voor je dienst die mogelijk geen SSL ondersteuning heeft. + +Proxy Hosts worden het meest gebruikt in Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/nl/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/nl/RedirectionHosts.md new file mode 100644 index 0000000..6b4e1ab --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Wat is een Redirection Host? + +Een Redirection Host zal verzoeken van de inkomende domeinnaam doorsturen, en de bezoeker +omleiden naar een andere domeinnaam. + +Het gebruik van een Redirection Host is vooral handig wanneer je jouw website verandert +maar je nog zoekmachines of referenties naar de oude domeinnaam hebben. diff --git a/frontend/src/locale/src/HelpDoc/nl/Streams.md b/frontend/src/locale/src/HelpDoc/nl/Streams.md new file mode 100644 index 0000000..7305754 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/Streams.md @@ -0,0 +1,6 @@ +## Wat is een Stream? + +Streams zijn een nieuwe toevoeging aan Nginx, die toelaat om TCP/UDP +verkeer naar een ander computer op het netwerk te sturen. + +Als je game servers, FTP of SSH servers draait kan dit handig zijn. diff --git a/frontend/src/locale/src/HelpDoc/nl/index.ts b/frontend/src/locale/src/HelpDoc/nl/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/nl/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/no/AccessLists.md b/frontend/src/locale/src/HelpDoc/no/AccessLists.md new file mode 100644 index 0000000..f8ff729 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/AccessLists.md @@ -0,0 +1,7 @@ +## Hva er en tilgangsliste? + +Tilgangslister gir en svarteliste eller hviteliste over spesifikke klient‑IP‑adresser, sammen med autentisering for `Proxy‑hosts` via Basic HTTP‑autentisering. + +Du kan konfigurere flere klientregler, brukernavn og passord for én tilgangsliste og deretter bruke denne på én eller flere `Proxy‑hosts`. + +Dette er spesielt nyttig for videresendte webtjenester som ikke har innebygd autentisering, eller når du ønsker å beskytte mot ukjente klienter. diff --git a/frontend/src/locale/src/HelpDoc/no/Certificates.md b/frontend/src/locale/src/HelpDoc/no/Certificates.md new file mode 100644 index 0000000..f336844 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/Certificates.md @@ -0,0 +1,29 @@ +## Hjelp om sertifikater + +### HTTP‑sertifikat + +Et HTTP‑validert sertifikat betyr at Let's Encrypt‑serverne vil forsøke å nå +domenene dine over HTTP (ikke HTTPS!) og hvis det lykkes, vil de utstede sertifikatet. + +For denne metoden må du ha en `Proxy‑host` opprettet for domenet/domenene dine som +er tilgjengelig over HTTP og peker til denne Nginx‑installasjonen. Etter at et sertifikat +er utstedt, kan du endre `Proxy‑host` til også å bruke dette sertifikatet for HTTPS‑tilkoblinger. +Proxy‑hosten må imidlertid fortsatt være konfigurert for HTTP‑tilgang for at sertifikatet skal kunne fornyes. + +Denne prosessen _støtter ikke_ wildcard‑domener. + +### DNS‑sertifikat + +Et DNS‑validert sertifikat krever at du bruker en DNS‑leverandør‑plugin. Denne leverandøren +vil opprette midlertidige DNS‑poster på domenet ditt, og Let's Encrypt vil deretter spørre +disse postene for å bekrefte at du eier domenet. Hvis valideringen lykkes, utstedes sertifikatet. + +Du trenger ikke å ha en `Proxy‑host` opprettet før du ber om denne typen sertifikat. Du trenger heller +ikke at `Proxy‑host` er konfigurert for HTTP‑tilgang. + +Denne prosessen _støtter_ wildcard‑domener. + +### Egendefinert sertifikat + +Bruk dette alternativet for å laste opp ditt eget SSL‑sertifikat, levert av din +egen sertifikatmyndighet (CA). diff --git a/frontend/src/locale/src/HelpDoc/no/DeadHosts.md b/frontend/src/locale/src/HelpDoc/no/DeadHosts.md new file mode 100644 index 0000000..300a752 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/DeadHosts.md @@ -0,0 +1,10 @@ +## Hva er en 404‑host? + +En 404‑host er enkelt og greit en host‑oppsett som viser en 404‑side. + +Dette kan være nyttig når domenet ditt er oppført i søkemotorer og du ønsker å +vise en penere feilmelding, eller for å fortelle søkeindekser at sidene på domenet +ikke lenger eksisterer. + +En annen fordel med å ha denne hosten er å kunne spore treff i loggene og +se hvilke henvisere som kommer til den. diff --git a/frontend/src/locale/src/HelpDoc/no/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/no/ProxyHosts.md new file mode 100644 index 0000000..8092e73 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/ProxyHosts.md @@ -0,0 +1,7 @@ +## Hva er en Proxy‑host? + +En Proxy‑host er inngangspunktet (innkommende endepunkt) for en webtjeneste du ønsker å videresende. + +Den tilbyr valgfri SSL‑terminering for tjenesten din hvis tjenesten ikke har innebygd støtte for SSL. + +Proxy‑hosts er den vanligste bruken av Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/no/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/no/RedirectionHosts.md new file mode 100644 index 0000000..db5c541 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Hva er en omdirigerings‑host? + +En omdirigerings‑host omdirigerer forespørsler fra det innkommende domenet og videresender +brukeren til et annet domene. + +Den vanligste årsaken til å bruke denne typen host er når nettstedet ditt har byttet +domene, men søkemotorer eller henvisningslenker fortsatt peker til det gamle domenet. diff --git a/frontend/src/locale/src/HelpDoc/no/Streams.md b/frontend/src/locale/src/HelpDoc/no/Streams.md new file mode 100644 index 0000000..370bec3 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/Streams.md @@ -0,0 +1,6 @@ +## Hva er en Stream? + +En relativt ny funksjon i Nginx. En Stream brukes til å videresende TCP/UDP‑trafikk +direkte til en annen maskin i nettverket. + +Dette er nyttig hvis du kjører spillservere, FTP‑ eller SSH‑servere. diff --git a/frontend/src/locale/src/HelpDoc/no/index.ts b/frontend/src/locale/src/HelpDoc/no/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/no/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/pl/AccessLists.md b/frontend/src/locale/src/HelpDoc/pl/AccessLists.md new file mode 100644 index 0000000..9cfe975 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/AccessLists.md @@ -0,0 +1,7 @@ +## Czym jest lista dostępu? + +Listy dostępu zapewniają czarną lub białą listę określonych adresów IP klientów wraz z uwierzytelnianiem dla hostów proxy za pomocą podstawowego uwierzytelniania HTTP. + +Możesz skonfigurować wiele reguł klienta, nazw użytkowników i haseł dla pojedynczej listy dostępu, a następnie zastosować ją do jednego lub więcej hostów proxy. + +Jest to najbardziej przydatne w przypadku przekierowywanych usług internetowych, które nie mają wbudowanych mechanizmów uwierzytelniania lub gdy chcesz zabezpieczyć się przed nieznanymi klientami. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/pl/Certificates.md b/frontend/src/locale/src/HelpDoc/pl/Certificates.md new file mode 100644 index 0000000..261dc32 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/Certificates.md @@ -0,0 +1,22 @@ +## Pomoc dotycząca certyfikatów + +### Certyfikat HTTP + +Certyfikat weryfikowany przez HTTP oznacza, że serwery Let's Encrypt będą próbowały połączyć się z twoimi domenami przez HTTP (nie HTTPS!) i jeśli się to powiedzie, wydadzą twój certyfikat. + +W przypadku tej metody musisz mieć utworzony Host proxy dla swoich domen, który jest dostępny przez HTTP i wskazuje na tę instalację Nginx. +Po otrzymaniu certyfikatu możesz zmodyfikować Host proxy, aby używał również tego certyfikatu do połączeń HTTPS. Jednak Host proxy nadal będzie musiał być skonfigurowany do dostępu przez HTTP, aby certyfikat mógł być odnawiany. + +Ten proces nie obsługuje domen wieloznacznych (wildcard). + +### Certyfikat DNS + +Certyfikat weryfikowany przez DNS wymaga użycia wtyczki dostawcy DNS. Ten dostawca DNS zostanie użyty do utworzenia tymczasowych rekordów w twojej domenie, a następnie Let's Encrypt sprawdzi te rekordy, aby upewnić się, że jesteś właścicielem i jeśli się powiedzie, wydadzą twój certyfikat. + +Nie musisz mieć utworzonego Hosta proxy przed wystąpieniem o ten typ certyfikatu. Nie musisz również mieć skonfigurowanego Hosta proxy do dostępu przez HTTP. + +Ten proces obsługuje domeny wieloznaczne (wildcard). + +### Własny certyfikat + +Użyj tej opcji, aby przesłać własny certyfikat SSL, dostarczony przez twój własny urząd certyfikacji. diff --git a/frontend/src/locale/src/HelpDoc/pl/DeadHosts.md b/frontend/src/locale/src/HelpDoc/pl/DeadHosts.md new file mode 100644 index 0000000..ec3d4fd --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/DeadHosts.md @@ -0,0 +1,7 @@ +## Czym jest host 404? + +Host 404 to po prostu konfiguracja hosta, która wyświetla stronę 404. + +Może to być przydatne, gdy twoja domena jest indeksowana w wyszukiwarkach i chcesz zapewnić ładniejszą stronę błędu lub konkretnie poinformować roboty indeksujące, że strony domeny już nie istnieją. + +Kolejną zaletą posiadania tego hosta jest możliwość śledzenia logów dla odwiedzin oraz przeglądania źródeł ruchu (referrerów). \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/pl/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/pl/ProxyHosts.md new file mode 100644 index 0000000..43e94ef --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/ProxyHosts.md @@ -0,0 +1,7 @@ +## Czym jest host proxy? + +Host proxy to punkt wejściowy dla usługi internetowej, którą chcesz przekierować. + +Zapewnia opcjonalne zakończenie SSL dla twojej usługi, która może nie mieć wbudowanej obsługi SSL. + +Hosty proxy są najpopularniejszym zastosowaniem Nginx Proxy Manager \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/pl/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/pl/RedirectionHosts.md new file mode 100644 index 0000000..22854df --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Czym jest host przekierowania? + +Host przekierowania przekierowuje żądania z domeny przychodzącej i przenosi odwiedzającego na inną domenę. + +Najczęstszym powodem używania tego typu hosta jest sytuacja, gdy twoja strona internetowa zmienia domeny, ale nadal masz linki z wyszukiwarek lub odnośniki wskazujące na starą domenę. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/pl/Streams.md b/frontend/src/locale/src/HelpDoc/pl/Streams.md new file mode 100644 index 0000000..793cada --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/Streams.md @@ -0,0 +1,5 @@ +## Czym jest strumień? + +Stosunkowo nowa funkcja dla Nginx, strumień służy do przekazywania ruchu TCP/UDP bezpośrednio na inny komputer/serwer w sieci. + +Jeśli prowadzisz serwery gier, FTP lub SSH, może się to okazać przydatne \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/pl/index.ts b/frontend/src/locale/src/HelpDoc/pl/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pl/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/pt/AccessLists.md b/frontend/src/locale/src/HelpDoc/pt/AccessLists.md new file mode 100644 index 0000000..fcedb8a --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/AccessLists.md @@ -0,0 +1,11 @@ +## O que é uma Access List? + +As *Access Lists* fornecem uma lista de permissões (whitelist) ou bloqueios (blacklist) +de endereços IP específicos de clientes, juntamente com autenticação para os *Proxy Hosts* +via Autenticação HTTP Básica (*Basic Auth*). + +Podes configurar múltiplas regras de cliente, nomes de utilizador e palavras-passe +para uma única *Access List*, e depois aplicá-la a um ou mais *Proxy Hosts*. + +Isto é especialmente útil para serviços web encaminhados que não têm mecanismos +de autenticação integrados ou quando pretendes proteger o acesso contra clientes desconhecidos. diff --git a/frontend/src/locale/src/HelpDoc/pt/Certificates.md b/frontend/src/locale/src/HelpDoc/pt/Certificates.md new file mode 100644 index 0000000..da92f80 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/Certificates.md @@ -0,0 +1,31 @@ +## Ajuda de Certificados + +### Certificado HTTP + +Um certificado validado por HTTP significa que os servidores do Let's Encrypt irão +tentar aceder aos teus domínios via HTTP (não HTTPS!) e, se a ligação for bem-sucedida, +emitirão o certificado. + +Para este método, é necessário ter um *Proxy Host* criado para o(s) teu(s) domínio(s), +acessível via HTTP e a apontar para esta instalação do Nginx. Depois de o certificado ser +emitido, podes modificar o *Proxy Host* para também utilizar esse certificado em ligações HTTPS. +No entanto, o *Proxy Host* deve continuar configurado para acesso HTTP para que a renovação +funcione corretamente. + +Este processo **não** suporta domínios wildcard. + +### Certificado DNS + +Um certificado validado por DNS requer que uses um plugin de fornecedor DNS (*DNS Provider*). +Este fornecedor será usado para criar registos temporários no teu domínio, que serão consultados +pelo Let's Encrypt para confirmar que és o proprietário. Se tudo correr bem, o certificado será emitido. + +Não é necessário ter um *Proxy Host* criado antes de pedir este tipo de certificado. +Também não é necessário que o *Proxy Host* tenha acesso HTTP configurado. + +Este processo **suporta** domínios wildcard. + +### Certificado Personalizado + +Usa esta opção para carregar o teu próprio Certificado SSL, fornecido pela +tua Autoridade Certificadora. diff --git a/frontend/src/locale/src/HelpDoc/pt/DeadHosts.md b/frontend/src/locale/src/HelpDoc/pt/DeadHosts.md new file mode 100644 index 0000000..c9874a1 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/DeadHosts.md @@ -0,0 +1,9 @@ +## O que é um 404 Host? + +Um *404 Host* é simplesmente um host configurado para apresentar uma página 404. + +Isto pode ser útil quando o teu domínio aparece em motores de busca e queres fornecer +uma página de erro mais agradável ou indicar especificamente aos indexadores de pesquisa +que as páginas desse domínio já não existem. + +Outra vantagem é permitir consultar os registos de acessos a este host e ver os referenciadores. diff --git a/frontend/src/locale/src/HelpDoc/pt/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/pt/ProxyHosts.md new file mode 100644 index 0000000..4c221fa --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/ProxyHosts.md @@ -0,0 +1,7 @@ +## O que é um Proxy Host? + +Um *Proxy Host* é o ponto de entrada para um serviço web que pretendes encaminhar. + +Permite, opcionalmente, fazer terminação SSL para um serviço que possa não ter suporte SSL nativo. + +Os *Proxy Hosts* são a utilização mais comum do Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/pt/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/pt/RedirectionHosts.md new file mode 100644 index 0000000..10418ba --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/RedirectionHosts.md @@ -0,0 +1,7 @@ +## O que é um Redirection Host? + +Um *Redirection Host* redireciona pedidos recebidos no domínio de entrada e envia +o utilizador para outro domínio. + +A razão mais comum para usar este tipo de host é quando o teu site muda de domínio +mas ainda tens motores de busca ou links de referência a apontar para o domínio antigo. diff --git a/frontend/src/locale/src/HelpDoc/pt/Streams.md b/frontend/src/locale/src/HelpDoc/pt/Streams.md new file mode 100644 index 0000000..83857c6 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/Streams.md @@ -0,0 +1,6 @@ +## O que é um Stream? + +Uma funcionalidade relativamente recente no Nginx, um *Stream* serve para encaminhar +tráfego TCP/UDP diretamente para outro computador na rede. + +Se estiveres a executar servidores de jogos, FTP ou SSH, isto pode ser bastante útil. diff --git a/frontend/src/locale/src/HelpDoc/pt/index.ts b/frontend/src/locale/src/HelpDoc/pt/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/pt/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/ru/AccessLists.md b/frontend/src/locale/src/HelpDoc/ru/AccessLists.md new file mode 100644 index 0000000..5fba383 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/AccessLists.md @@ -0,0 +1,7 @@ +## Что такое список доступа? + +Списки доступа позволяют задавать белый/чёрный список IP‑адресов клиентов и настраивать аутентификацию для прокси‑хостов через базовую HTTP‑аутентификацию. + +Для одного списка доступа можно настроить несколько правил клиентов, логины и пароли, а затем применить его к одному или нескольким _прокси‑хостам_. + +Это особенно полезно для проксируемых веб‑сервисов без встроенной аутентификации или когда нужно защититься от неизвестных клиентов. diff --git a/frontend/src/locale/src/HelpDoc/ru/Certificates.md b/frontend/src/locale/src/HelpDoc/ru/Certificates.md new file mode 100644 index 0000000..5cd8960 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/Certificates.md @@ -0,0 +1,21 @@ +## Справка по сертификатам + +### HTTP-сертификат + +Сертификат, подтверждённый по HTTP, означает, что серверы Let's Encrypt попытаются обратиться к вашим доменам по HTTP (не HTTPS!) и при успехе выпустят сертификат. + +Для этого метода должен существовать _прокси‑хост_ для ваших доменов, доступный по HTTP и указывающий на эту установку Nginx. После выдачи сертификата вы можете настроить _прокси‑хост_ на использование этого сертификата для HTTPS‑подключений. Однако доступ по HTTP должен сохраняться, чтобы сертификат мог обновляться. + +Этот способ _не_ поддерживает wildcard‑домены. + +### DNS-сертификат + +Сертификат, подтверждённый по DNS, требует использования плагина DNS‑провайдера. Такой провайдер создаст временные записи в вашем домене, затем Let's Encrypt проверит эти записи, чтобы убедиться, что вы владелец домена, и при успехе выпустит сертификат. + +Для запроса такого сертификата предварительно создавать _прокси‑хост_ не требуется. Также не нужен доступ по HTTP для вашего _прокси‑хоста_. + +Этот способ _поддерживает_ wildcard‑домены. + +### Свой сертификат + +Используйте этот вариант, чтобы загрузить собственный SSL‑сертификат, выданный вашим удостоверяющим центром (CA). diff --git a/frontend/src/locale/src/HelpDoc/ru/DeadHosts.md b/frontend/src/locale/src/HelpDoc/ru/DeadHosts.md new file mode 100644 index 0000000..9fafe5f --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/DeadHosts.md @@ -0,0 +1,7 @@ +## Что такое 404‑хост? + +404‑хост — это конфигурация, которая показывает страницу 404. + +Это полезно, когда ваш домен присутствует в поисковых системах и вы хотите показать более дружелюбную страницу ошибки или явно сообщить индексаторам, что страницы домена больше не существуют. + +Ещё одно преимущество — можно отдельно отслеживать обращения в журналах и смотреть источники переходов. diff --git a/frontend/src/locale/src/HelpDoc/ru/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/ru/ProxyHosts.md new file mode 100644 index 0000000..34e5497 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/ProxyHosts.md @@ -0,0 +1,7 @@ +## Что такое прокси‑хост? + +Прокси‑хост — это входная точка веб‑сервиса, который вы проксируете. + +Он может выполнять терминaцию SSL для сервиса, у которого нет собственной поддержки SSL. + +Прокси‑хосты — самый распространённый сценарий использования Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/ru/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/ru/RedirectionHosts.md new file mode 100644 index 0000000..3ccdbdc --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Что такое редирект‑хост? + +Редирект‑хост перенаправляет запросы, поступающие на входящий домен, на другой домен. + +Чаще всего это используют, когда сайт сменил домен, а в поиске или на сторонних ресурсах всё ещё остаются ссылки на старый домен. diff --git a/frontend/src/locale/src/HelpDoc/ru/Streams.md b/frontend/src/locale/src/HelpDoc/ru/Streams.md new file mode 100644 index 0000000..a3951a2 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/Streams.md @@ -0,0 +1,5 @@ +## Что такое поток? + +Относительно новая возможность Nginx: поток позволяет напрямую проксировать TCP/UDP‑трафик на другой компьютер в сети. + +Полезно для игровых серверов, FTP или SSH‑серверов. diff --git a/frontend/src/locale/src/HelpDoc/ru/index.ts b/frontend/src/locale/src/HelpDoc/ru/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/ru/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/sk/AccessLists.md b/frontend/src/locale/src/HelpDoc/sk/AccessLists.md new file mode 100644 index 0000000..8f4eea3 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/AccessLists.md @@ -0,0 +1,7 @@ +## Čo je zoznam prístupov? + +Zoznamy prístupov poskytujú čiernu alebo bielu listinu konkrétnych IP adries klientov spolu s overovaním pre proxy hostiteľov prostredníctvom základného overovania HTTP. + +Môžete nakonfigurovať viacero pravidiel pre klientov, používateľských mien a hesiel pre jeden zoznam prístupov a potom ho použiť na jeden alebo viacero proxy hostiteľov. + +Toto je najužitočnejšie pre presmerované webové služby, ktoré nemajú zabudované overovacie mechanizmy, alebo ak sa chcete chrániť pred neznámymi klientmi. diff --git a/frontend/src/locale/src/HelpDoc/sk/Certificates.md b/frontend/src/locale/src/HelpDoc/sk/Certificates.md new file mode 100644 index 0000000..a2e5797 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/Certificates.md @@ -0,0 +1,32 @@ +## Pomoc s certifikátmi + +### Certifikát HTTP + +Certifikát overený protokolom HTTP znamená, že servery Let's Encrypt sa +pokúsia pripojiť k vašim doménam cez protokol HTTP (nie HTTPS!) a v prípade úspechu +vydajú váš certifikát. + +Pre túto metódu budete musieť mať pre svoje domény vytvorený _Proxy Host_, ktorý +je prístupný cez HTTP a smeruje na túto inštaláciu Nginx. Po vydaní certifikátu +môžete zmeniť _Proxy Host_ tak, aby tento certifikát používal aj pre HTTPS +pripojenia. _Proxy Host_ však bude stále potrebné nakonfigurovať pre prístup cez HTTP, +aby sa certifikát mohol obnoviť. + +Tento proces _nepodporuje_ domény s divokými kartami. + +### Certifikát DNS + +Certifikát overený DNS vyžaduje použitie pluginu DNS Provider. Tento DNS +Provider sa použije na vytvorenie dočasných záznamov vo vašej doméne a potom Let's +Encrypt overí tieto záznamy, aby sa uistil, že ste vlastníkom, a ak bude úspešný, +vydá váš certifikát. + +Pred požiadaním o tento typ certifikátu nie je potrebné vytvoriť _Proxy Host_. +Tiež nie je potrebné mať _Proxy Host_ nakonfigurovaný pre prístup HTTP. + +Tento proces _podporuje_ domény s divokými kartami. + +### Vlastný certifikát + +Túto možnosť použite na nahratie vlastného SSL certifikátu, ktorý vám poskytla vaša +certifikačná autorita. diff --git a/frontend/src/locale/src/HelpDoc/sk/DeadHosts.md b/frontend/src/locale/src/HelpDoc/sk/DeadHosts.md new file mode 100644 index 0000000..a232d23 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/DeadHosts.md @@ -0,0 +1,10 @@ +## Čo je to 404 Hostiteľ? + +404 Hostiteľ je jednoducho nastavenie hostiteľa, ktoré zobrazuje stránku 404. + +To môže byť užitočné, ak je vaša doména uvedená vo vyhľadávačoch a chcete +poskytnúť krajšiu stránku s chybou alebo konkrétne oznámiť vyhľadávačom, že +stránky domény už neexistujú. + +Ďalšou výhodou tohto hostiteľa je sledovanie protokolov o návštevách a +zobrazenie odkazov. diff --git a/frontend/src/locale/src/HelpDoc/sk/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/sk/ProxyHosts.md new file mode 100644 index 0000000..631a1b4 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/ProxyHosts.md @@ -0,0 +1,7 @@ +## Čo je proxy hostiteľ? + +Proxy hostiteľ je prichádzajúci koncový bod pre webovú službu, ktorú chcete presmerovať. + +Poskytuje voliteľné ukončenie SSL pre vašu službu, ktorá nemusí mať zabudovanú podporu SSL. + +Proxy hostitelia sú najbežnejším použitím pre Nginx Proxy Manager. diff --git a/frontend/src/locale/src/HelpDoc/sk/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/sk/RedirectionHosts.md new file mode 100644 index 0000000..92b5e54 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/RedirectionHosts.md @@ -0,0 +1,7 @@ +## Čo je presmerovací hostiteľ? + +Presmerovací hostiteľ presmeruje požiadavky z prichádzajúcej domény a presmeruje +návštevníka na inú doménu. + +Najčastejším dôvodom na použitie tohto typu hostiteľa je situácia, keď vaša webová stránka zmení +doménu, ale stále máte odkazy vo vyhľadávačoch alebo referenčné odkazy smerujúce na starú doménu. diff --git a/frontend/src/locale/src/HelpDoc/sk/Streams.md b/frontend/src/locale/src/HelpDoc/sk/Streams.md new file mode 100644 index 0000000..39b3b1c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/Streams.md @@ -0,0 +1,6 @@ +## Čo je stream? + +Stream je relatívne nová funkcia pre Nginx, ktorá slúži na presmerovanie TCP/UDP +dátového toku priamo do iného počítača v sieti. + +Ak prevádzkujete herné servery, FTP alebo SSH servery, táto funkcia sa vám môže hodiť. diff --git a/frontend/src/locale/src/HelpDoc/sk/index.ts b/frontend/src/locale/src/HelpDoc/sk/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/sk/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/tr/AccessLists.md b/frontend/src/locale/src/HelpDoc/tr/AccessLists.md new file mode 100644 index 0000000..7c02322 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/AccessLists.md @@ -0,0 +1,8 @@ +## Erişim Listesi Nedir? + +Erişim Listeleri, Temel HTTP Kimlik Doğrulama aracılığıyla Proxy Host'lar için belirli istemci IP adreslerinin kara listesi veya beyaz listesini ve kimlik doğrulamasını sağlar. + +Tek bir Erişim Listesi için birden fazla istemci kuralı, kullanıcı adı ve şifre yapılandırabilir ve bunu bir veya daha fazla _Proxy Host_'a uygulayabilirsiniz. + +Bu, yerleşik kimlik doğrulama mekanizmaları olmayan veya bilinmeyen istemcilerden korunmak istediğinizde iletilen web hizmetleri için en kullanışlıdır. + diff --git a/frontend/src/locale/src/HelpDoc/tr/Certificates.md b/frontend/src/locale/src/HelpDoc/tr/Certificates.md new file mode 100644 index 0000000..8817d98 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/Certificates.md @@ -0,0 +1,29 @@ +## Sertifika Yardımı + +### HTTP Sertifikası + +Bir HTTP doğrulanmış sertifika, Let's Encrypt sunucularının +alan adlarınıza HTTP (HTTPS değil!) üzerinden ulaşmaya çalışacağı ve başarılı olursa, +sertifikanızı verecekleri anlamına gelir. + +Bu yöntem için, alan adlarınız için HTTP ile erişilebilir ve bu Nginx kurulumuna işaret eden bir _Proxy Host_ oluşturulmuş olmalıdır. Bir sertifika +verildikten sonra, _Proxy Host_'u HTTPS +bağlantıları için de bu sertifikayı kullanacak şekilde değiştirebilirsiniz. Ancak, sertifikanın yenilenmesi için _Proxy Host_'un hala HTTP erişimi için yapılandırılmış olması gerekecektir. + +Bu işlem joker karakter alan adlarını _desteklemez_. + +### DNS Sertifikası + +Bir DNS doğrulanmış sertifika, bir DNS Sağlayıcı eklentisi kullanmanızı gerektirir. Bu DNS +Sağlayıcı, alan adınızda geçici kayıtlar oluşturmak için kullanılacak ve ardından Let's +Encrypt bu kayıtları sorgulayarak sahibi olduğunuzdan emin olacak ve başarılı olursa, +sertifikanızı verecektir. + +Bu tür bir sertifika talep etmeden önce bir _Proxy Host_ oluşturulmasına gerek yoktur. Ayrıca _Proxy Host_'unuzun HTTP erişimi için yapılandırılmasına da gerek yoktur. + +Bu işlem joker karakter alan adlarını _destekler_. + +### Özel Sertifika + +Kendi Sertifika Otoriteniz tarafından sağlanan kendi SSL Sertifikanızı yüklemek için bu seçeneği kullanın. + diff --git a/frontend/src/locale/src/HelpDoc/tr/DeadHosts.md b/frontend/src/locale/src/HelpDoc/tr/DeadHosts.md new file mode 100644 index 0000000..6d8d433 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/DeadHosts.md @@ -0,0 +1,10 @@ +## 404 Host Nedir? + +404 Host, basitçe bir 404 sayfası gösteren bir host kurulumudur. + +Bu, alan adınız arama motorlarında listelendiğinde ve daha güzel bir hata sayfası sağlamak veya özellikle arama dizinleyicilerine +alan adı sayfalarının artık mevcut olmadığını söylemek istediğinizde yararlı olabilir. + +Bu host'un bir başka faydası da, ona yapılan isteklerin loglarını takip etmek ve +referansları görüntülemektir. + diff --git a/frontend/src/locale/src/HelpDoc/tr/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/tr/ProxyHosts.md new file mode 100644 index 0000000..224a76a --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/ProxyHosts.md @@ -0,0 +1,8 @@ +## Proxy Host Nedir? + +Proxy Host, iletilmek istediğiniz bir web hizmeti için gelen uç noktadır. + +SSL desteği yerleşik olmayan hizmetiniz için isteğe bağlı SSL sonlandırma sağlar. + +Proxy Host'lar, Nginx Proxy Manager'ın en yaygın kullanımıdır. + diff --git a/frontend/src/locale/src/HelpDoc/tr/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/tr/RedirectionHosts.md new file mode 100644 index 0000000..01e3bf2 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/RedirectionHosts.md @@ -0,0 +1,8 @@ +## Yönlendirme Host'u Nedir? + +Yönlendirme Host'u, gelen alan adından gelen istekleri yönlendirir ve +görüntüleyiciyi başka bir alan adına yönlendirir. + +Bu tür bir host kullanmanın en yaygın nedeni, web sitenizin alan adı değiştiğinde +ancak hala eski alan adına işaret eden arama motoru veya referans bağlantılarınız olduğunda ortaya çıkar. + diff --git a/frontend/src/locale/src/HelpDoc/tr/Streams.md b/frontend/src/locale/src/HelpDoc/tr/Streams.md new file mode 100644 index 0000000..696c27b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/Streams.md @@ -0,0 +1,7 @@ +## Akış Nedir? + +Nginx için nispeten yeni bir özellik olan Akış, TCP/UDP +trafiğini doğrudan ağdaki başka bir bilgisayara iletmek için hizmet edecektir. + +Oyun sunucuları, FTP veya SSH sunucuları çalıştırıyorsanız bu işinize yarayabilir. + diff --git a/frontend/src/locale/src/HelpDoc/tr/index.ts b/frontend/src/locale/src/HelpDoc/tr/index.ts new file mode 100644 index 0000000..33e015c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/tr/index.ts @@ -0,0 +1,7 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; + diff --git a/frontend/src/locale/src/HelpDoc/vi/AccessLists.md b/frontend/src/locale/src/HelpDoc/vi/AccessLists.md new file mode 100644 index 0000000..50e5375 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/AccessLists.md @@ -0,0 +1,11 @@ +## Khái niệm Access List là gì? + +Access List (Danh sách truy cập) cung cấp cơ chế chặn (blacklist) hoặc cho phép (whitelist) các địa chỉ IP của client, đồng thời hỗ trợ xác thực Basic HTTP Authentication cho các Proxy Host. + +Bạn có thể cấu hình nhiều quy tắc client, nhiều tên người dùng và mật khẩu trong một Access List duy nhất, sau đó áp dụng Access List đó cho một hoặc nhiều Proxy Host. + +Tính năng này đặc biệt hữu ích đối với: + +các dịch vụ web được forward mà không có cơ chế xác thực tích hợp, hoặc + +khi bạn muốn bảo vệ tài nguyên khỏi những client không xác định. diff --git a/frontend/src/locale/src/HelpDoc/vi/Certificates.md b/frontend/src/locale/src/HelpDoc/vi/Certificates.md new file mode 100644 index 0000000..d25ef00 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/Certificates.md @@ -0,0 +1,25 @@ +## Hỗ trợ Chứng chỉ + +### Chứng chỉ HTTP (HTTP Certificate) + +Chứng chỉ được xác thực qua HTTP nghĩa là máy chủ của Let's Encrypt sẽ cố gắng truy cập vào tên miền của bạn thông qua HTTP (không phải HTTPS!). Nếu kiểm tra thành công, chứng chỉ sẽ được cấp. + +Với phương thức này, bạn phải tạo trước một Proxy Host cho tên miền, có thể truy cập qua HTTP và trỏ về đúng cài đặt Nginx này. +Sau khi chứng chỉ được cấp, bạn có thể chỉnh sửa Proxy Host để sử dụng chứng chỉ đó cho kết nối HTTPS. + +Tuy nhiên, Proxy Host vẫn phải hỗ trợ truy cập HTTP để việc gia hạn chứng chỉ diễn ra bình thường. + +Phương thức này _không hỗ trợ_ wildcard domain. + +### Chứng chỉ DNS (DNS Certificate) + +Chứng chỉ được xác thực qua DNS yêu cầu bạn sử dụng plugin của DNS Provider. +Plugin này sẽ tạo các bản ghi tạm thời trong DNS của bạn để Let's Encrypt kiểm tra quyền sở hữu tên miền. Nếu hợp lệ, chứng chỉ sẽ được cấp. + +Khi dùng phương thức này: Bạn không cần tạo sẵn Proxy Host trước và bạn không cần mở HTTP cho Proxy Host. + +Phương thức DNS _có hỗ trợ_ wildcard domain. + +### Chứng chỉ tùy chỉnh (Custom Certificate) + +Tùy chọn này cho phép bạn tải lên chứng chỉ SSL của riêng mình, được cung cấp bởi Certificate Authority (CA) mà bạn tự chọn. diff --git a/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md b/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md new file mode 100644 index 0000000..4c0aae8 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/DeadHosts.md @@ -0,0 +1,8 @@ +## 404 Host là gì? + +404 Host đơn giản là một host được thiết lập để hiển thị trang 404. + +Điều này có thể hữu ích khi tên miền của bạn vẫn xuất hiện trên các công cụ tìm kiếm và bạn muốn hiển thị một trang lỗi đẹp hơn, hoặc muốn thông báo rõ ràng cho các trình thu thập dữ liệu tìm kiếm rằng các trang thuộc tên miền đó không còn tồn tại. + +Một lợi ích khác của việc có 404 Host là bạn có thể theo dõi nhật ký truy cập vào nó và +xem các nguồn giới thiệu (referrers). \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md new file mode 100644 index 0000000..969f8a7 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/ProxyHosts.md @@ -0,0 +1,7 @@ +## Proxy Host là gì? + +Proxy Host là điểm truy cập đầu vào cho một dịch vụ web mà bạn muốn chuyển tiếp. + +Nó cung cấp khả năng kết thúc SSL (SSL termination) tùy chọn cho các dịch vụ vốn không hỗ trợ SSL tích hợp. + +Proxy Host là loại cấu hình phổ biến nhất trong Nginx Proxy Manager. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md new file mode 100644 index 0000000..4339923 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/RedirectionHosts.md @@ -0,0 +1,5 @@ +## Redirection Host là gì? + +Redirection Host sẽ chuyển hướng các yêu cầu từ tên miền truy cập vào và đưa người xem sang một tên miền khác + +Lý do phổ biến nhất để sử dụng loại host này là khi trang web của bạn đổi sang tên miền mới nhưng vẫn còn các liên kết từ công cụ tìm kiếm hoặc nguồn giới thiệu trỏ về tên miền cũ. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/vi/Streams.md b/frontend/src/locale/src/HelpDoc/vi/Streams.md new file mode 100644 index 0000000..43cf97b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/Streams.md @@ -0,0 +1,6 @@ +## Stream là gì? + +Stream là một tính năng tương đối mới của Nginx, dùng để chuyển tiếp lưu lượng +TCP/UDP trực tiếp tới một máy khác trong mạng. + +Nếu bạn đang vận hành các máy chủ game, FTP hoặc SSH thì tính năng này sẽ rất hữu ích. \ No newline at end of file diff --git a/frontend/src/locale/src/HelpDoc/vi/index.ts b/frontend/src/locale/src/HelpDoc/vi/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/vi/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/HelpDoc/zh/AccessLists.md b/frontend/src/locale/src/HelpDoc/zh/AccessLists.md new file mode 100644 index 0000000..5d50836 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/AccessLists.md @@ -0,0 +1,7 @@ +## 什么是通信规则? + +通信规则提供了一个特定客户IP地址的黑名单或白名单,以及通过基本HTTP认证对代理服务的认证。 + +你可以为一个通信规则配置多个客户规则、用户名和密码,然后将其应用于代理服务。 + +这对那些没有内置认证机制的转发网络服务或你想保护其免受未知客户的访问是最有用的。 diff --git a/frontend/src/locale/src/HelpDoc/zh/Certificates.md b/frontend/src/locale/src/HelpDoc/zh/Certificates.md new file mode 100644 index 0000000..1c51c6e --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/Certificates.md @@ -0,0 +1,21 @@ +## 证书帮助 + +### HTTP 证书 + +HTTP 验证的证书表示 Let's Encrypt 服务器将尝试通过 HTTP(而非 HTTPS!)访问您的域名,如果成功,它们将为您颁发证书。 + +使用此方法时,您必须为您的域名创建一个可通过 HTTP 访问并指向此 Nginx 安装的 代理主机。在获得证书后,您可以修改该 代理主机,使其也使用此证书处理 HTTPS 连接。然而,为了证书能够续期,该 代理主机 仍需配置为支持 HTTP 访问。 + +此过程_不支持_通配符域名。 + +### DNS 证书 + +DNS 验证的证书要求您使用一个 DNS 服务商插件。该 DNS 服务商将用于在您的域名下创建临时记录,随后 Let's Encrypt 将查询这些记录以确认您是域名所有者,如果成功,它们将为您颁发证书。 + +请求此类证书前,您无需预先创建 代理主机,也无需将您的 代理主机 配置为支持 HTTP 访问。 + +此过程_支持_通配符域名。 + +### 自定义证书 + +使用此选项上传您自己的 SSL 证书,该证书由您自己的证书颁发机构提供。 diff --git a/frontend/src/locale/src/HelpDoc/zh/DeadHosts.md b/frontend/src/locale/src/HelpDoc/zh/DeadHosts.md new file mode 100644 index 0000000..9d16602 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/DeadHosts.md @@ -0,0 +1,7 @@ +## 什么是错误页面? + +错误页面是一个简单的主机设置,显示错误页面。 + +当你的域名被列入搜索引擎,而你想提供一个更好的错误页面或特别是告诉搜索索引者域名页面不再存在时,这可能是有用的。 + +拥有这种主机的另一个好处是可以跟踪点击它的日志并查看访问来源。 diff --git a/frontend/src/locale/src/HelpDoc/zh/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/zh/ProxyHosts.md new file mode 100644 index 0000000..36da09e --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/ProxyHosts.md @@ -0,0 +1,7 @@ +## 什么是代理服务? + +代理服务是你想转发网络应用的主机。 + +代理服务可以为没有SSL服务的网络应用提供SSL服务(可选)。 + +代理服务是Nginx代理管理器的最常见用途之一。 diff --git a/frontend/src/locale/src/HelpDoc/zh/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/zh/RedirectionHosts.md new file mode 100644 index 0000000..ef3a4d4 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/RedirectionHosts.md @@ -0,0 +1,5 @@ +## 什么是重定向? + +重定向是将接入域名的请求推送到另一个域名。 + +使用这种类型的主机最常见的原因是当你的网站改变了域名,但你仍然有链接指向旧域名的应用。 diff --git a/frontend/src/locale/src/HelpDoc/zh/Streams.md b/frontend/src/locale/src/HelpDoc/zh/Streams.md new file mode 100644 index 0000000..24cac7c --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/Streams.md @@ -0,0 +1,5 @@ +## 什么是端口转发? + +端口转发是Nginx的一个相对较新的功能,可以直接转发 TCP/UDP 流量到网络上的另一台计算机。 + +如果你正在运行游戏服务器、FTP或SSH服务器,这个功能就会很有用。 diff --git a/frontend/src/locale/src/HelpDoc/zh/index.ts b/frontend/src/locale/src/HelpDoc/zh/index.ts new file mode 100644 index 0000000..a9bb46b --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/zh/index.ts @@ -0,0 +1,6 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as DeadHosts from "./DeadHosts.md"; +export * as ProxyHosts from "./ProxyHosts.md"; +export * as RedirectionHosts from "./RedirectionHosts.md"; +export * as Streams from "./Streams.md"; diff --git a/frontend/src/locale/src/bg.json b/frontend/src/locale/src/bg.json new file mode 100644 index 0000000..5183fe3 --- /dev/null +++ b/frontend/src/locale/src/bg.json @@ -0,0 +1,695 @@ +{ + "access-list": { + "defaultMessage": "Списък за достъп" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {правило} other {правила}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {потребител} other {потребители}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Когато съществува поне 1 правило, това правило за отказ се добавя последно" + }, + "access-list.help.rules-order": { + "defaultMessage": "Обърнете внимание, че правилата Позволяване и Отказване се прилагат в реда, в който са зададени." + }, + "access-list.pass-auth": { + "defaultMessage": "Предаване на автентикация към Upstream" + }, + "access-list.public": { + "defaultMessage": "Публичен достъп" + }, + "access-list.public.subtitle": { + "defaultMessage": "Без базова автентикация" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 или 192.168.1.0/24 или 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Удовлетворяване на което и да е" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {потребител} other {потребители}}, {rules} {rules, plural, one {правило} other {правила}} - Създадено: {date}" + }, + "access-lists": { + "defaultMessage": "Списъци за достъп" + }, + "action.add": { + "defaultMessage": "Добавяне" + }, + "action.add-location": { + "defaultMessage": "Добавяне на маршрут" + }, + "action.allow": { + "defaultMessage": "Разрешаване" + }, + "action.close": { + "defaultMessage": "Затваряне" + }, + "action.delete": { + "defaultMessage": "Изтриване" + }, + "action.deny": { + "defaultMessage": "Отказване" + }, + "action.disable": { + "defaultMessage": "Деактивиране" + }, + "action.download": { + "defaultMessage": "Изтегляне" + }, + "action.edit": { + "defaultMessage": "Редактиране" + }, + "action.enable": { + "defaultMessage": "Активиране" + }, + "action.permissions": { + "defaultMessage": "Права" + }, + "action.renew": { + "defaultMessage": "Подновяване" + }, + "action.view-details": { + "defaultMessage": "Преглед на детайли" + }, + "auditlogs": { + "defaultMessage": "Журнали за одит" + }, + "auto": { + "defaultMessage": "Автоматично" + }, + "cancel": { + "defaultMessage": "Отказ" + }, + "certificate": { + "defaultMessage": "Сертификат" + }, + "certificate.custom-certificate": { + "defaultMessage": "Сертификат" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Ключ на сертификата" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Междинен сертификат" + }, + "certificate.in-use": { + "defaultMessage": "Използва се" + }, + "certificate.none.subtitle": { + "defaultMessage": "Не е назначен сертификат" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Този хост няма да използва HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Без сертификат" + }, + "certificate.not-in-use": { + "defaultMessage": "Не се използва" + }, + "certificate.renew": { + "defaultMessage": "Подновяване на сертификат" + }, + "certificates": { + "defaultMessage": "Сертификати" + }, + "certificates.custom": { + "defaultMessage": "Потребителски сертификат" + }, + "certificates.custom.warning": { + "defaultMessage": "Ключове, защитени с парола, не се поддържат." + }, + "certificates.dns.credentials": { + "defaultMessage": "Съдържание на файл с удостоверения" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Този плъгин изисква конфигурационен файл с API токен или други идентификационни данни." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Тези данни ще бъдат съхранени като обикновен текст в базата и във файл!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Секунди за разпространение" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Оставете празно, за да се използва стойността по подразбиране. Брой секунди за изчакване на DNS разпространение." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS доставчик" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Изберете доставчик..." + }, + "certificates.dns.warning": { + "defaultMessage": "Този раздел изисква познания за Certbot и неговите DNS плъгини. Моля, консултирайте се с документацията." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Сървър е намерен на този домейн, но не изглежда да е Nginx Proxy Manager. Уверете се, че домейнът сочи към IP адреса, където работи NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Неуспешна проверка поради грешка в комуникацията със site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Няма достъпен сървър на този домейн. Проверете, че домейнът съществува и сочи към IP-та, където се изпълнява NPM, и ако е необходимо, че порт 80 е пренасочен." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Вашият сървър е достъпен и създаването на сертификати е възможно." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Намерен е сървър, но върна неочакван код {code}. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Намерен е сървър, но върна неочаквани данни. Това NPM ли е? Уверете се, че домейнът сочи към вашия NPM сървър." + }, + "certificates.http.test-results": { + "defaultMessage": "Резултати от теста" + }, + "certificates.http.warning": { + "defaultMessage": "Тези домейни трябва вече да сочат към тази инсталация." + }, + "certificates.key-type": { + "defaultMessage": "Тип ключ" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA е широко съвместим, ECDSA е по-бърз и по-сигурен, но може да не се поддържа от по-стари системи" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "с Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Заявка за нов сертификат" + }, + "column.access": { + "defaultMessage": "Достъп" + }, + "column.authorization": { + "defaultMessage": "Автентикация" + }, + "column.authorizations": { + "defaultMessage": "Автентикации" + }, + "column.custom-locations": { + "defaultMessage": "Персонализирани маршрути" + }, + "column.destination": { + "defaultMessage": "Дестинация" + }, + "column.details": { + "defaultMessage": "Детайли" + }, + "column.email": { + "defaultMessage": "Имейл" + }, + "column.event": { + "defaultMessage": "Събитие" + }, + "column.expires": { + "defaultMessage": "Изтича" + }, + "column.http-code": { + "defaultMessage": "HTTP код" + }, + "column.incoming-port": { + "defaultMessage": "Входящ порт" + }, + "column.name": { + "defaultMessage": "Име" + }, + "column.protocol": { + "defaultMessage": "Протокол" + }, + "column.provider": { + "defaultMessage": "Доставчик" + }, + "column.roles": { + "defaultMessage": "Роли" + }, + "column.rules": { + "defaultMessage": "Правила" + }, + "column.satisfy": { + "defaultMessage": "Удовлетворяване" + }, + "column.satisfy-all": { + "defaultMessage": "Всички" + }, + "column.satisfy-any": { + "defaultMessage": "Кое и да е" + }, + "column.scheme": { + "defaultMessage": "Схема" + }, + "column.source": { + "defaultMessage": "Източник" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Статус" + }, + "created-on": { + "defaultMessage": "Създадено: {date}" + }, + "dashboard": { + "defaultMessage": "Табло" + }, + "dead-host": { + "defaultMessage": "404 хост" + }, + "dead-hosts": { + "defaultMessage": "404 хостове" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 хост} other {404 хостове}}" + }, + "disabled": { + "defaultMessage": "Деактивиран" + }, + "domain-names": { + "defaultMessage": "Домейн имена" + }, + "domain-names.max": { + "defaultMessage": "Максимум {count} домейна" + }, + "domain-names.placeholder": { + "defaultMessage": "Започнете да въвеждате, за да добавите домейн..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcard не е разрешен за този тип" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcard не се поддържа от това CA" + }, + "domains.force-ssl": { + "defaultMessage": "Принудително SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS активирано" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS за поддомейни" + }, + "domains.http2-support": { + "defaultMessage": "Поддръжка на HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Използване на DNS Challenge" + }, + "email-address": { + "defaultMessage": "Имейл адрес" + }, + "empty-search": { + "defaultMessage": "Няма резултати" + }, + "empty-subtitle": { + "defaultMessage": "Защо не създадете един?" + }, + "enabled": { + "defaultMessage": "Активиран" + }, + "error.access.at-least-one": { + "defaultMessage": "Необходимо е поне една Автентикация или едно Правило за достъп" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Потребителските имена за достъп трябва да са уникални" + }, + "error.invalid-auth": { + "defaultMessage": "Невалиден имейл или парола" + }, + "error.invalid-domain": { + "defaultMessage": "Невалиден домейн: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Невалиден имейл адрес" + }, + "error.max-character-length": { + "defaultMessage": "Максималната дължина е {max} знак{max, plural, one {} other {а}}" + }, + "error.max-domains": { + "defaultMessage": "Твърде много домейни, максимум {max}" + }, + "error.maximum": { + "defaultMessage": "Максимум {max}" + }, + "error.min-character-length": { + "defaultMessage": "Минималната дължина е {min} знак{min, plural, one {} other {а}}" + }, + "error.minimum": { + "defaultMessage": "Минимум e {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Паролите трябва да съвпадат" + }, + "error.required": { + "defaultMessage": "Това поле е задължително" + }, + "expires.on": { + "defaultMessage": "Изтича: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork в GitHub" + }, + "host.flags.block-exploits": { + "defaultMessage": "Блокиране на често срещани експлойти" + }, + "host.flags.cache-assets": { + "defaultMessage": "Кеширане на ресурси" + }, + "host.flags.preserve-path": { + "defaultMessage": "Запазване на пътя" + }, + "host.flags.protocols": { + "defaultMessage": "Протоколи" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Поддръжка на WebSockets" + }, + "host.forward-port": { + "defaultMessage": "Порт" + }, + "host.forward-scheme": { + "defaultMessage": "Схема" + }, + "hosts": { + "defaultMessage": "Хостове" + }, + "http-only": { + "defaultMessage": "Само HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt чрез DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt чрез HTTP" + }, + "loading": { + "defaultMessage": "Зареждане…" + }, + "login.title": { + "defaultMessage": "Вход в акаунта" + }, + "nginx-config.label": { + "defaultMessage": "Персонализирана Nginx конфигурация" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Въведете вашата персонализирана Nginx конфигурация на собствен риск!" + }, + "no-permission-error": { + "defaultMessage": "Нямате достъп до тази страница." + }, + "notfound.action": { + "defaultMessage": "Към началната страница" + }, + "notfound.content": { + "defaultMessage": "Страницата, която търсите, не беше намерена" + }, + "notfound.title": { + "defaultMessage": "Упс… Намерихте грешка" + }, + "notification.error": { + "defaultMessage": "Грешка" + }, + "notification.object-deleted": { + "defaultMessage": "{object} беше изтрит" + }, + "notification.object-disabled": { + "defaultMessage": "{object} беше деактивиран" + }, + "notification.object-enabled": { + "defaultMessage": "{object} беше активиран" + }, + "notification.object-renewed": { + "defaultMessage": "{object} беше подновен" + }, + "notification.object-saved": { + "defaultMessage": "{object} беше запазен" + }, + "notification.success": { + "defaultMessage": "Успех" + }, + "object.actions-title": { + "defaultMessage": "{object} №{id}" + }, + "object.add": { + "defaultMessage": "Добавяне: {object}" + }, + "object.delete": { + "defaultMessage": "Изтриване: {object}" + }, + "object.delete.content": { + "defaultMessage": "Сигурни ли сте, че искате да изтриете {object}?" + }, + "object.edit": { + "defaultMessage": "Редактиране: {object}" + }, + "object.empty": { + "defaultMessage": "Няма налични {objects}" + }, + "object.event.created": { + "defaultMessage": "Създаден {object}" + }, + "object.event.deleted": { + "defaultMessage": "Изтрит {object}" + }, + "object.event.disabled": { + "defaultMessage": "Деактивиран {object}" + }, + "object.event.enabled": { + "defaultMessage": "Активиран {object}" + }, + "object.event.renewed": { + "defaultMessage": "Подновен {object}" + }, + "object.event.updated": { + "defaultMessage": "Актуализиран {object}" + }, + "offline": { + "defaultMessage": "Офлайн" + }, + "online": { + "defaultMessage": "Онлайн" + }, + "options": { + "defaultMessage": "Опции" + }, + "password": { + "defaultMessage": "Парола" + }, + "password.generate": { + "defaultMessage": "Генериране на случайна парола" + }, + "password.hide": { + "defaultMessage": "Скриване на паролата" + }, + "password.show": { + "defaultMessage": "Показване на паролата" + }, + "permissions.hidden": { + "defaultMessage": "Скрито" + }, + "permissions.manage": { + "defaultMessage": "Управление" + }, + "permissions.view": { + "defaultMessage": "Само преглед" + }, + "permissions.visibility.all": { + "defaultMessage": "Всички елементи" + }, + "permissions.visibility.title": { + "defaultMessage": "Видимост на елементите" + }, + "permissions.visibility.user": { + "defaultMessage": "Само създадените от потребителя" + }, + "proxy-host": { + "defaultMessage": "Прокси хост" + }, + "proxy-host.forward-host": { + "defaultMessage": "Хост/IP за препращане" + }, + "proxy-hosts": { + "defaultMessage": "Прокси хостове" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {прокси хост} other {прокси хостове}}" + }, + "public": { + "defaultMessage": "Публичен" + }, + "redirection-host": { + "defaultMessage": "Хост за пренасочване" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Домейн за пренасочване" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP код" + }, + "redirection-hosts": { + "defaultMessage": "Хостове за пренасочване" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {хост за пренасочване} other {хостове за пренасочване}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Multiple Choices" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Преместено постоянно" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Преместено временно" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 See other" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Временно пренасочване" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Постоянно пренасочване" + }, + "role.admin": { + "defaultMessage": "Администратор" + }, + "role.standard-user": { + "defaultMessage": "Обикновен потребител" + }, + "save": { + "defaultMessage": "Запазване" + }, + "setting": { + "defaultMessage": "Настройка" + }, + "settings": { + "defaultMessage": "Настройки" + }, + "settings.default-site": { + "defaultMessage": "Сайт по подразбиране" + }, + "settings.default-site.404": { + "defaultMessage": "404 страница" + }, + "settings.default-site.444": { + "defaultMessage": "Без отговор (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Страница поздравление" + }, + "settings.default-site.description": { + "defaultMessage": "Какво да се показва при заявка към неизвестен хост" + }, + "settings.default-site.html": { + "defaultMessage": "Персонализиран HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Пренасочване" + }, + "setup.preamble": { + "defaultMessage": "Започнете, като създадете администраторски акаунт." + }, + "setup.title": { + "defaultMessage": "Добре дошли!" + }, + "sign-in": { + "defaultMessage": "Вход" + }, + "ssl-certificate": { + "defaultMessage": "SSL сертификат" + }, + "stream": { + "defaultMessage": "Поток" + }, + "stream.forward-host": { + "defaultMessage": "Хост за препращане" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com или 10.0.0.1 или 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Входящ порт" + }, + "streams": { + "defaultMessage": "Потоци" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {поток} other {потоци}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Тест" + }, + "update-available": { + "defaultMessage": "Налична актуализация: {latestVersion}" + }, + "user": { + "defaultMessage": "Потребител" + }, + "user.change-password": { + "defaultMessage": "Смяна на парола" + }, + "user.confirm-password": { + "defaultMessage": "Потвърждение на парола" + }, + "user.current-password": { + "defaultMessage": "Текуща парола" + }, + "user.edit-profile": { + "defaultMessage": "Редактиране на профил" + }, + "user.full-name": { + "defaultMessage": "Пълно име" + }, + "user.login-as": { + "defaultMessage": "Вход като {name}" + }, + "user.logout": { + "defaultMessage": "Изход" + }, + "user.new-password": { + "defaultMessage": "Нова парола" + }, + "user.nickname": { + "defaultMessage": "Псевдоним" + }, + "user.set-password": { + "defaultMessage": "Задаване на парола" + }, + "user.set-permissions": { + "defaultMessage": "Настройка на права за {name}" + }, + "user.switch-dark": { + "defaultMessage": "Тъмна тема" + }, + "user.switch-light": { + "defaultMessage": "Светла тема" + }, + "username": { + "defaultMessage": "Потребителско име" + }, + "users": { + "defaultMessage": "Потребители" + } +} diff --git a/frontend/src/locale/src/cs.json b/frontend/src/locale/src/cs.json new file mode 100644 index 0000000..cd86b67 --- /dev/null +++ b/frontend/src/locale/src/cs.json @@ -0,0 +1,770 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Počet zbývajících záložních kódů: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Tyto záložní kódy si uložte na bezpečném místě. Každý kód lze použít pouze jednou." + }, + "2fa.disable": { + "defaultMessage": "Vypnout dvoufaktorové ověřování" + }, + "2fa.disable-confirm": { + "defaultMessage": "Vypnout 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Vypnutím dvoufaktorového ověřování snížíte bezpečnost svého účtu." + }, + "2fa.disabled": { + "defaultMessage": "Vypnuto" + }, + "2fa.done": { + "defaultMessage": "Uložil jsem si své záložní kódy." + }, + "2fa.enable": { + "defaultMessage": "Zapnout dvoufaktorové ověřování" + }, + "2fa.enabled": { + "defaultMessage": "Zapnuto" + }, + "2fa.enter-code": { + "defaultMessage": "Zadejte ověřovací kód" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Zadejte ověřovací kód pro vypnutí" + }, + "2fa.regenerate": { + "defaultMessage": "Znovu vytvořit" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Znovu vytvořit záložní kódy" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Zadejte ověřovací kód pro vytvoření nových záložních kódů. Vaše staré kódy budou neplatné." + }, + "2fa.secret-key": { + "defaultMessage": "Tajný klíč" + }, + "2fa.setup-instructions": { + "defaultMessage": "Naskenujte tento QR kód pomocí své ověřovací aplikace nebo zadejte tajný klíč ručně." + }, + "2fa.status": { + "defaultMessage": "Stav" + }, + "2fa.title": { + "defaultMessage": "Dvoufaktorové ověření" + }, + "2fa.verify-enable": { + "defaultMessage": "Ověřit a zapnout" + }, + "access-list": { + "defaultMessage": "seznam přístupů" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {pravidlo} few {pravidla} other {pravidel}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {uživatel} few {uživatelé} other {uživatelů}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Když existuje alespoň jedno pravidlo, toto pravidlo „zamítnout vše“ bude přidáno jako poslední" + }, + "access-list.help.rules-order": { + "defaultMessage": "Upozornění: pravidla povolit a zamítnout budou uplatňována v pořadí, v jakém jsou definována." + }, + "access-list.pass-auth": { + "defaultMessage": "Odeslat ověření na Upstream" + }, + "access-list.public": { + "defaultMessage": "Veřejně přístupné" + }, + "access-list.public.subtitle": { + "defaultMessage": "Není potřeba základní ověření" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 nebo 192.168.1.0/24 nebo 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Splnit kterékoliv" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {uživatel} few {uživatelé} other {uživatelů}}, {rules} {rules, plural, one {pravidlo} few {pravidla} other {pravidel}} - Vytvořeno: {date}" + }, + "access-lists": { + "defaultMessage": "Seznamy přístupů" + }, + "action.add": { + "defaultMessage": "Přidat" + }, + "action.add-location": { + "defaultMessage": "Přidat umístění" + }, + "action.allow": { + "defaultMessage": "Povolit" + }, + "action.close": { + "defaultMessage": "Zavřít" + }, + "action.delete": { + "defaultMessage": "Smazat" + }, + "action.deny": { + "defaultMessage": "Zamítnout" + }, + "action.disable": { + "defaultMessage": "Deaktivovat" + }, + "action.download": { + "defaultMessage": "Stáhnout" + }, + "action.edit": { + "defaultMessage": "Upravit" + }, + "action.enable": { + "defaultMessage": "Aktivovat" + }, + "action.permissions": { + "defaultMessage": "Oprávnění" + }, + "action.renew": { + "defaultMessage": "Obnovit" + }, + "action.view-details": { + "defaultMessage": "Zobrazit podrobnosti" + }, + "auditlogs": { + "defaultMessage": "Záznamy auditu" + }, + "auto": { + "defaultMessage": "Automaticky" + }, + "cancel": { + "defaultMessage": "Zrušit" + }, + "certificate": { + "defaultMessage": "certifikát" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certifikát" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Klíč certifikátu" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Zprostředkovatelský certifikát" + }, + "certificate.in-use": { + "defaultMessage": "Používá se" + }, + "certificate.none.subtitle": { + "defaultMessage": "Není přiřazen žádný certifikát" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Tento hostitel nebude používat HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Žádný" + }, + "certificate.not-in-use": { + "defaultMessage": "Nepoužívá se" + }, + "certificate.renew": { + "defaultMessage": "Obnovit certifikát" + }, + "certificates": { + "defaultMessage": "Certifikáty" + }, + "certificates.custom": { + "defaultMessage": "Vlastní certifikát" + }, + "certificates.custom.warning": { + "defaultMessage": "Soubory klíčů chráněné heslem nejsou podporovány." + }, + "certificates.dns.credentials": { + "defaultMessage": "Obsah souboru s přihlašovacími údaji" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Tento doplněk vyžaduje konfigurační soubor obsahující API token nebo jiné přihlašovací údaje vašeho poskytovatele" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Tyto údaje budou uloženy v databázi a v souboru jako obyčejný text!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagace v sekundách" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Nechte prázdné pro výchozí hodnotu doplňku. Počet sekund, po které se čeká na propagaci DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS poskytovatel" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Vyberte poskytovatele..." + }, + "certificates.dns.warning": { + "defaultMessage": "Tato sekce vyžaduje znalost Certbotu a jeho DNS doplňků. Prosím, podívejte se do dokumentace příslušného doplňku." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Na této doméně byl nalezen server, ale nezdá se, že jde o Nginx Proxy Manager. Ujistěte se, že vaše doména směřuje na IP, kde běží vaše instance NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Nepodařilo se ověřit dostupnost kvůli chybě komunikace se službou site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Na této doméně není dostupný žádný server. Ujistěte se, že doména existuje a směřuje na IP adresu s NPM a pokud je to potřeba, port 80 je přesměrován ve vašem routeru." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Váš server je dostupný a vytvoření certifikátu by mělo být možné." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Na této doméně byl nalezen server, ale vrátil neočekávaný stavový kód {code}. Je to NPM server? Ujistěte se prosím, že doména směřuje na IP, kde běží vaše instance NPM." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Na této doméně byl nalezen server, ale vrátil neočekávaná data. Je to NPM server? Ujistěte se, že doména směřuje na IP, kde běží vaše instance NPM." + }, + "certificates.http.test-results": { + "defaultMessage": "Výsledky testu" + }, + "certificates.http.warning": { + "defaultMessage": "Tyto domény musí být již nakonfigurovány tak, aby směřovaly na tuto instalaci." + }, + "certificates.key-type": { + "defaultMessage": "Typ klíče" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA je široce kompatibilní, ECDSA je rychlejší a bezpečnější, ale nemusí být podporován staršími systémy" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "pomocí Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Vyžádat nový certifikát" + }, + "column.access": { + "defaultMessage": "Přístup" + }, + "column.authorization": { + "defaultMessage": "Autorizace" + }, + "column.authorizations": { + "defaultMessage": "Autorizace" + }, + "column.custom-locations": { + "defaultMessage": "Vlastní umístění" + }, + "column.destination": { + "defaultMessage": "Cíl" + }, + "column.details": { + "defaultMessage": "Podrobnosti" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Událost" + }, + "column.expires": { + "defaultMessage": "Platnost do" + }, + "column.http-code": { + "defaultMessage": "Přístup" + }, + "column.incoming-port": { + "defaultMessage": "Vstupní port" + }, + "column.name": { + "defaultMessage": "Název" + }, + "column.protocol": { + "defaultMessage": "Protokol" + }, + "column.provider": { + "defaultMessage": "Poskytovatel" + }, + "column.roles": { + "defaultMessage": "Role" + }, + "column.rules": { + "defaultMessage": "Pravidla" + }, + "column.satisfy": { + "defaultMessage": "Splnit" + }, + "column.satisfy-all": { + "defaultMessage": "Všechny" + }, + "column.satisfy-any": { + "defaultMessage": "Kterékoliv" + }, + "column.scheme": { + "defaultMessage": "Schéma" + }, + "column.source": { + "defaultMessage": "Zdroj" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Stav" + }, + "created-on": { + "defaultMessage": "Vytvořeno: {date}" + }, + "dashboard": { + "defaultMessage": "Panel" + }, + "dead-host": { + "defaultMessage": "404 hostitel" + }, + "dead-hosts": { + "defaultMessage": "404 Hostitelé" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 hostitel} few {404 hostitelé} other {404 hostitelů}}" + }, + "disabled": { + "defaultMessage": "Deaktivováno" + }, + "domain-names": { + "defaultMessage": "Doménová jména" + }, + "domain-names.max": { + "defaultMessage": "Maximálně {count} doménových jmen" + }, + "domain-names.placeholder": { + "defaultMessage": "Začněte psát pro přidání domény..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards nejsou pro tento typ povoleny" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards nejsou podporovány pro tuto certifikační autoritu" + }, + "domains.force-ssl": { + "defaultMessage": "Vynutit SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS povoleno" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS pro subdomény" + }, + "domains.http2-support": { + "defaultMessage": "Podpora HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Použít DNS výzvu" + }, + "email-address": { + "defaultMessage": "Emailová adresa" + }, + "empty-search": { + "defaultMessage": "Nebyly nalezeny žádné výsledky" + }, + "empty-subtitle": { + "defaultMessage": "Proč nevytvoříte nějaký?" + }, + "enabled": { + "defaultMessage": "Aktivováno" + }, + "error.access.at-least-one": { + "defaultMessage": "Je vyžadována alespoň jedna autorizace nebo jedno přístupové pravidlo" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Uživatelská jména pro autorizaci musí být jedinečná" + }, + "error.invalid-auth": { + "defaultMessage": "Neplatný email nebo heslo" + }, + "error.invalid-domain": { + "defaultMessage": "Neplatná doména: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Neplatná emailová adresa" + }, + "error.max-character-length": { + "defaultMessage": "Maximální délka je {max} znak{max, plural, one {} few {y} other {ů}}" + }, + "error.max-domains": { + "defaultMessage": "Příliš mnoho domén, maximum je {max}" + }, + "error.maximum": { + "defaultMessage": "Maximum je {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimální délka je {min} znak{min, plural, one {} few {y} other {ů}}" + }, + "error.minimum": { + "defaultMessage": "Minimum je {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Hesla se musí shodovat" + }, + "error.required": { + "defaultMessage": "Toto pole je povinné" + }, + "expires.on": { + "defaultMessage": "Platnost do: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forkněte mě na GitHubu" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blokovat běžné exploity" + }, + "host.flags.cache-assets": { + "defaultMessage": "Uložit zdroje do mezipaměti" + }, + "host.flags.preserve-path": { + "defaultMessage": "Zachovat cestu" + }, + "host.flags.protocols": { + "defaultMessage": "Protokoly" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Podpora WebSockets" + }, + "host.forward-port": { + "defaultMessage": "Port přesměrování" + }, + "host.forward-scheme": { + "defaultMessage": "Schéma" + }, + "hosts": { + "defaultMessage": "Hostitelé" + }, + "http-only": { + "defaultMessage": "Pouze HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt přes DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt přes HTTP" + }, + "loading": { + "defaultMessage": "Načítá se…" + }, + "login.2fa-code": { + "defaultMessage": "Ověřovací kód" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Vložit kód" + }, + "login.2fa-description": { + "defaultMessage": "Vložte kód z vaší ověřovací aplikace" + }, + "login.2fa-title": { + "defaultMessage": "Dvoufaktorové ověření" + }, + "login.2fa-verify": { + "defaultMessage": "Ověřit" + }, + "login.title": { + "defaultMessage": "Přihlaste se ke svému účtu" + }, + "nginx-config.label": { + "defaultMessage": "Vlastní Nginx konfigurace" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Zadejte vlastní Nginx konfiguraci na vlastní riziko!" + }, + "no-permission-error": { + "defaultMessage": "Nemáte oprávnění k zobrazení tohoto obsahu." + }, + "notfound.action": { + "defaultMessage": "Zpět na hlavní stránku" + }, + "notfound.content": { + "defaultMessage": "Omlouváme se, stránka, kterou hledáte, nebyla nalezena" + }, + "notfound.title": { + "defaultMessage": "Ups… Našli jste chybovou stránku" + }, + "notification.error": { + "defaultMessage": "Chyba" + }, + "notification.object-deleted": { + "defaultMessage": "{object} byl odstraněn" + }, + "notification.object-disabled": { + "defaultMessage": "{object} byl deaktivován" + }, + "notification.object-enabled": { + "defaultMessage": "{object} byl aktivován" + }, + "notification.object-renewed": { + "defaultMessage": "{object} byl obnoven" + }, + "notification.object-saved": { + "defaultMessage": "{object} byl uložen" + }, + "notification.success": { + "defaultMessage": "Úspěch" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Přidat {object}" + }, + "object.delete": { + "defaultMessage": "Smazat {object}" + }, + "object.delete.content": { + "defaultMessage": "Opravdu chcete smazat tento {object}?" + }, + "object.edit": { + "defaultMessage": "Upravit {object}" + }, + "object.empty": { + "defaultMessage": "Nejsou {objects}" + }, + "object.event.created": { + "defaultMessage": "Vytvořen {object}" + }, + "object.event.deleted": { + "defaultMessage": "Smazán {object}" + }, + "object.event.disabled": { + "defaultMessage": "Deaktivován {object}" + }, + "object.event.enabled": { + "defaultMessage": "Aktivován {object}" + }, + "object.event.renewed": { + "defaultMessage": "Obnoven {object}" + }, + "object.event.updated": { + "defaultMessage": "Aktualizován {object}" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Možnosti" + }, + "password": { + "defaultMessage": "Heslo" + }, + "password.generate": { + "defaultMessage": "Vygenerovat náhodné heslo" + }, + "password.hide": { + "defaultMessage": "Skrýt heslo" + }, + "password.show": { + "defaultMessage": "Zobrazit heslo" + }, + "permissions.hidden": { + "defaultMessage": "Skryté" + }, + "permissions.manage": { + "defaultMessage": "Spravovat" + }, + "permissions.view": { + "defaultMessage": "Pouze pro zobrazení" + }, + "permissions.visibility.all": { + "defaultMessage": "Všechny položky" + }, + "permissions.visibility.title": { + "defaultMessage": "Viditelnost položky" + }, + "permissions.visibility.user": { + "defaultMessage": "Pouze vytvořené položky" + }, + "proxy-host": { + "defaultMessage": "proxy hostitele" + }, + "proxy-host.forward-host": { + "defaultMessage": "Cílový název hostitele / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy hostitelé" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {proxy hostitel} few {proxy hostitelé} other {proxy hostitelů}}" + }, + "public": { + "defaultMessage": "Veřejné" + }, + "redirection-host": { + "defaultMessage": "přesměrovacího hostitele" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Cílová doména" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP kód" + }, + "redirection-hosts": { + "defaultMessage": "Přesměrovací hostitelé" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {přesměrovací hostitel} few {přesměrovací hostitelé} other {přesměrovacích hostitelů}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Více možností" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Trvale přesunuto" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Dočasně přesunuto" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Podívat se na jiné" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Dočasné přesměrování" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Trvalé přesměrování" + }, + "role.admin": { + "defaultMessage": "Administrátor" + }, + "role.standard-user": { + "defaultMessage": "Běžný uživatel" + }, + "save": { + "defaultMessage": "Uložit" + }, + "setting": { + "defaultMessage": "Nastavení" + }, + "settings": { + "defaultMessage": "Nastavení" + }, + "settings.default-site": { + "defaultMessage": "Výchozí stránka" + }, + "settings.default-site.404": { + "defaultMessage": "Stránka 404" + }, + "settings.default-site.444": { + "defaultMessage": "Bez odpovědi (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Gratulační stránka" + }, + "settings.default-site.description": { + "defaultMessage": "Co zobrazit, když Nginx zachytí neznámého hostitele" + }, + "settings.default-site.html": { + "defaultMessage": "Vlastní HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Přesměrovat" + }, + "setup.preamble": { + "defaultMessage": "Začněte vytvořením administrátorského účtu." + }, + "setup.title": { + "defaultMessage": "Vítejte!" + }, + "sign-in": { + "defaultMessage": "Přihlásit se" + }, + "ssl-certificate": { + "defaultMessage": "SSL certifikát" + }, + "stream": { + "defaultMessage": "stream" + }, + "stream.forward-host": { + "defaultMessage": "Cílový hostitel" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "napriklad.cz nebo 10.0.0.1 nebo 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Vstupní port" + }, + "streams": { + "defaultMessage": "Streamy" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {stream} few {streamy} other {streamů}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Dostupná aktualizace: {latestVersion}" + }, + "user": { + "defaultMessage": "uživatele" + }, + "user.change-password": { + "defaultMessage": "Změnit heslo" + }, + "user.confirm-password": { + "defaultMessage": "Potvrdit heslo" + }, + "user.current-password": { + "defaultMessage": "Aktuální heslo" + }, + "user.edit-profile": { + "defaultMessage": "Upravit profil" + }, + "user.full-name": { + "defaultMessage": "Celé jméno" + }, + "user.login-as": { + "defaultMessage": "Přihlásit se jako {name}" + }, + "user.logout": { + "defaultMessage": "Odhlásit se" + }, + "user.new-password": { + "defaultMessage": "Nové heslo" + }, + "user.nickname": { + "defaultMessage": "Přezdívka" + }, + "user.set-password": { + "defaultMessage": "Nastavit heslo" + }, + "user.set-permissions": { + "defaultMessage": "Nastavit oprávnění pro {name}" + }, + "user.switch-dark": { + "defaultMessage": "Přepnout na tmavý režim" + }, + "user.switch-light": { + "defaultMessage": "Přepnout na světlý režim" + }, + "user.two-factor": { + "defaultMessage": "Dvoufaktorové ověření" + }, + "username": { + "defaultMessage": "Uživatelské jméno" + }, + "users": { + "defaultMessage": "Uživatelé" + } +} diff --git a/frontend/src/locale/src/de.json b/frontend/src/locale/src/de.json new file mode 100644 index 0000000..f654e10 --- /dev/null +++ b/frontend/src/locale/src/de.json @@ -0,0 +1,656 @@ +{ + "access-list": { + "defaultMessage": "Zugriffsliste" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Regel} other {Regeln}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {User} other {Users}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Wenn mindestens eine Regel vorhanden ist, wird diese Regel zum Ablehnen aller Anfragen als letzte hinzugefügt." + }, + "access-list.help.rules-order": { + "defaultMessage": "Beachten Sie, dass die Anweisungen „Erlauben“ und „Verbieten“ in der Reihenfolge ihrer Definition angewendet werden." + }, + "access-list.pass-auth": { + "defaultMessage": "Authentifizierung an Upstream weiterleiten" + }, + "access-list.public": { + "defaultMessage": "Öffentlich" + }, + "access-list.public.subtitle": { + "defaultMessage": "Keine Authentifizierung erforderlich" + }, + "access-list.satisfy-any": { + "defaultMessage": "Satisfy Any" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Regel} other {Regeln}} - Erstellt: {date}" + }, + "access-lists": { + "defaultMessage": "Zugriffslisten" + }, + "action.add": { + "defaultMessage": "Hinzufügen" + }, + "action.add-location": { + "defaultMessage": "Pfad hinzufügen" + }, + "action.close": { + "defaultMessage": "Schließen" + }, + "action.delete": { + "defaultMessage": "Löschen" + }, + "action.disable": { + "defaultMessage": "Deaktivieren" + }, + "action.download": { + "defaultMessage": "Herunterladen" + }, + "action.edit": { + "defaultMessage": "Bearbeiten" + }, + "action.enable": { + "defaultMessage": "Aktivieren" + }, + "action.permissions": { + "defaultMessage": "Berechtigungen" + }, + "action.renew": { + "defaultMessage": "Erneuern" + }, + "action.view-details": { + "defaultMessage": "Details anzeigen" + }, + "auditlogs": { + "defaultMessage": "Protokolle" + }, + "cancel": { + "defaultMessage": "Abbrechen" + }, + "certificate": { + "defaultMessage": "Zertifikat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Zertifikat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Privater Schlüssel" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Zwischenzertifikat" + }, + "certificate.in-use": { + "defaultMessage": "In Benutzung" + }, + "certificate.none.subtitle": { + "defaultMessage": "Kein Zertifikat zugewiesen" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Dieser Host verwendet kein HTTPS." + }, + "certificate.none.title": { + "defaultMessage": "Kein" + }, + "certificate.not-in-use": { + "defaultMessage": "Nicht in Benutzung" + }, + "certificate.renew": { + "defaultMessage": "Zertifikat erneuern" + }, + "certificates": { + "defaultMessage": "Zertifikate" + }, + "certificates.custom": { + "defaultMessage": "Benutzerdefiniertes Zertifikat" + }, + "certificates.custom.warning": { + "defaultMessage": "Mit einem Passwort geschützte Schlüsseldateien werden nicht unterstützt." + }, + "certificates.dns.credentials": { + "defaultMessage": "Inhalt der Anmeldedaten-Datei" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Dieses Plugin erfordert eine Konfigurationsdatei, die einen API-Token oder andere Anmeldedaten für Ihren Anbieter enthält." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Diese Daten werden als Klartext in der Datenbank und in einer Datei gespeichert!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Wartezeit in Sekunden" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Leer lassen um die Standardwartezeit des Plugins zu nutzen" + }, + "certificates.dns.provider": { + "defaultMessage": "DNS Provider" + }, + "certificates.dns.warning": { + "defaultMessage": "Dieser Abschnitt erfordert einige Kenntnisse über Certbot und seine DNS-Plugins. Bitte konsultieren Sie die jeweilige Plugin-Dokumentation." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Unter dieser Domain wurde ein Server gefunden, aber es scheint sich nicht um Nginx Proxy Manager zu handeln. Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Die Erreichbarkeit konnte aufgrund eines Kommunikationsfehlers mit site24x7.com nicht überprüft werden." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Unter dieser Domain ist kein Server verfügbar. Bitte stellen Sie sicher, dass Ihre Domain existiert und auf die IP-Adresse verweist, unter der Ihre NPM-Instanz läuft, und dass gegebenenfalls Port 80 in Ihrem Router weitergeleitet wird." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Ihr Server ist erreichbar und die Erstellung von Zertifikaten sollte möglich sein." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Unter dieser Domain wurde ein Server gefunden, der jedoch einen unerwarteten Statuscode {code} zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Unter dieser Domain wurde ein Server gefunden, der jedoch unerwartete Daten zurückgegeben hat. Handelt es sich um den NPM-Server? Bitte stellen Sie sicher, dass Ihre Domain auf die IP-Adresse verweist, unter der Ihre NPM-Instanz ausgeführt wird." + }, + "certificates.http.test-results": { + "defaultMessage": "Testergebnisse" + }, + "certificates.http.warning": { + "defaultMessage": "Diese Domänen müssen bereits so konfiguriert sein, dass sie auf diese Installation verweisen." + }, + "certificates.key-type": { + "defaultMessage": "Schlüsseltyp" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA ist weit verbreitet, ECDSA ist schneller und sicherer, wird aber möglicherweise von älteren Systemen nicht unterstützt" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "Über Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Anfordern eines neuen Zertifikates" + }, + "column.access": { + "defaultMessage": "Zugriff" + }, + "column.authorization": { + "defaultMessage": "Genehmigung" + }, + "column.authorizations": { + "defaultMessage": "Genehmigungen" + }, + "column.custom-locations": { + "defaultMessage": "Benutzerdefinierte Pfade" + }, + "column.destination": { + "defaultMessage": "Ziel" + }, + "column.details": { + "defaultMessage": "Details" + }, + "column.email": { + "defaultMessage": "E-Mail" + }, + "column.event": { + "defaultMessage": "Ereignis" + }, + "column.expires": { + "defaultMessage": "Verfällt am" + }, + "column.http-code": { + "defaultMessage": "HTTP Code" + }, + "column.incoming-port": { + "defaultMessage": "Eingehender Port" + }, + "column.name": { + "defaultMessage": "Name" + }, + "column.protocol": { + "defaultMessage": "Protokoll" + }, + "column.provider": { + "defaultMessage": "Provider" + }, + "column.roles": { + "defaultMessage": "Rollen" + }, + "column.rules": { + "defaultMessage": "Regeln" + }, + "column.satisfy": { + "defaultMessage": "Satisfy" + }, + "column.satisfy-all": { + "defaultMessage": "Alle" + }, + "column.satisfy-any": { + "defaultMessage": "Jeder" + }, + "column.scheme": { + "defaultMessage": "Schema" + }, + "column.source": { + "defaultMessage": "Quelle" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Erstelldatum: {date}" + }, + "dashboard": { + "defaultMessage": "Dashboard" + }, + "dead-host": { + "defaultMessage": "404 Host" + }, + "dead-hosts": { + "defaultMessage": "404 Hosts" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}" + }, + "disabled": { + "defaultMessage": "Deaktiviert" + }, + "domain-names": { + "defaultMessage": "Domain Names" + }, + "domain-names.max": { + "defaultMessage": "{count} Maximale Anzahl von Domainnamen" + }, + "domain-names.placeholder": { + "defaultMessage": "Eintragen der Domain..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards sind für diesen Typ nicht zulässig." + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards werden für diese Zertifizierungsstelle nicht unterstützt." + }, + "domains.force-ssl": { + "defaultMessage": "Erzwinge SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS aktiviert" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Sub-domains" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 Support" + }, + "domains.use-dns": { + "defaultMessage": "Nutze DNS Challenge" + }, + "email-address": { + "defaultMessage": "E-Mail-Adresse" + }, + "empty-search": { + "defaultMessage": "Keine Ergebnisse gefunden" + }, + "empty-subtitle": { + "defaultMessage": "Warum erstellen Sie nicht eine?" + }, + "enabled": { + "defaultMessage": "aktiviert" + }, + "error.access.at-least-one": { + "defaultMessage": "Entweder eine Genehmigung oder eine Zugriffsregel ist erforderlich." + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Autorisierung Benutzernamen müssen eindeutig sein" + }, + "error.invalid-auth": { + "defaultMessage": "Ungültige E-Mail-Adresse oder Passwort" + }, + "error.invalid-domain": { + "defaultMessage": "Ungültige Domain: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Ungültige E-Mail-Adresse" + }, + "error.max-character-length": { + "defaultMessage": "Die maximale Länge beträgt {max} Zeichen{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Zu viele Domains, maximal sind {max}" + }, + "error.maximum": { + "defaultMessage": "Maximum ist {max}" + }, + "error.min-character-length": { + "defaultMessage": "Die minimale Länge beträgt {min} Zeichen{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Minimum ist {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Passwörter müssen übereinstimmen" + }, + "error.required": { + "defaultMessage": "Dies ist erforderlich." + }, + "expires.on": { + "defaultMessage": "Ablauf am: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork me on Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Gängige Exploits blockieren" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache Assets" + }, + "host.flags.preserve-path": { + "defaultMessage": "Pfad beibehalten" + }, + "host.flags.protocols": { + "defaultMessage": "Protokole" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets Support" + }, + "host.forward-port": { + "defaultMessage": "Forward Port" + }, + "host.forward-scheme": { + "defaultMessage": "Schema" + }, + "hosts": { + "defaultMessage": "Hosts" + }, + "http-only": { + "defaultMessage": "HTTP Only" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Laden…" + }, + "login.title": { + "defaultMessage": "Anmelden" + }, + "nginx-config.label": { + "defaultMessage": "Benutzerdefinierte Nginx Konfiguration" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Geben Sie hier Ihre benutzerdefinierte Nginx-Konfiguration auf eigene Gefahr ein!" + }, + "no-permission-error": { + "defaultMessage": "Sie haben keinen Zugriff, um dies anzuzeigen." + }, + "notfound.action": { + "defaultMessage": "Take me home" + }, + "notfound.content": { + "defaultMessage": "Es tut uns leid, aber die gesuchte Seite wurde nicht gefunden" + }, + "notfound.title": { + "defaultMessage": "Oops… You just found an error page" + }, + "notification.error": { + "defaultMessage": "Error" + }, + "notification.object-deleted": { + "defaultMessage": "{object} wurde gelöscht" + }, + "notification.object-disabled": { + "defaultMessage": "{object} wurde deaktiviert" + }, + "notification.object-enabled": { + "defaultMessage": "{object} wurde aktiviert" + }, + "notification.object-renewed": { + "defaultMessage": "{object} wurde erneuert" + }, + "notification.object-saved": { + "defaultMessage": "{object} wurde gespeichert" + }, + "notification.success": { + "defaultMessage": "Erfolgreich" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "{object} hinzufügen" + }, + "object.delete": { + "defaultMessage": "{object} löschen" + }, + "object.delete.content": { + "defaultMessage": "{object} wirklich löschen?" + }, + "object.edit": { + "defaultMessage": "{object} bearbeiten" + }, + "object.empty": { + "defaultMessage": "Keine {objects} vorhanden" + }, + "object.event.created": { + "defaultMessage": "{object} erstellt" + }, + "object.event.deleted": { + "defaultMessage": "{object} gelöscht" + }, + "object.event.disabled": { + "defaultMessage": "{object} deaktiviert" + }, + "object.event.enabled": { + "defaultMessage": "{object} aktiviert" + }, + "object.event.renewed": { + "defaultMessage": "{object} erneuert" + }, + "object.event.updated": { + "defaultMessage": "{object} aktualisiert" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Optionen" + }, + "password": { + "defaultMessage": "Passwort" + }, + "password.generate": { + "defaultMessage": "Zufälliges Passwort generieren" + }, + "password.hide": { + "defaultMessage": "Passwort verstecken" + }, + "password.show": { + "defaultMessage": "Passwort anzeigen" + }, + "permissions.hidden": { + "defaultMessage": "Versteckt" + }, + "permissions.manage": { + "defaultMessage": "Verwalten" + }, + "permissions.view": { + "defaultMessage": "Nur anzeigen" + }, + "permissions.visibility.all": { + "defaultMessage": "Alle Elemente" + }, + "permissions.visibility.title": { + "defaultMessage": "Objektsichtbarkeit" + }, + "permissions.visibility.user": { + "defaultMessage": "Nur erstellte Elemente" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Forward Hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Hosts" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}" + }, + "public": { + "defaultMessage": "Öffentlich" + }, + "redirection-host": { + "defaultMessage": "Redirection Host" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Forward Domain" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP Code" + }, + "redirection-hosts": { + "defaultMessage": "Redirection Hosts" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Standardbenutzer" + }, + "save": { + "defaultMessage": "Speichern" + }, + "setting": { + "defaultMessage": "Einstellung" + }, + "settings": { + "defaultMessage": "Einstellungen" + }, + "settings.default-site": { + "defaultMessage": "Standardseite" + }, + "settings.default-site.404": { + "defaultMessage": "404 Page" + }, + "settings.default-site.444": { + "defaultMessage": "No Response (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Willkommensseite" + }, + "settings.default-site.description": { + "defaultMessage": "Was angezeigt wird, wenn der Nginx eine unbekannte Webseitenanfrage bekommt" + }, + "settings.default-site.html": { + "defaultMessage": "Benutzerdefinierte HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Weiterleitung" + }, + "setup.preamble": { + "defaultMessage": "Beginnen Sie mit der Erstellung Ihres Administratorkontos." + }, + "setup.title": { + "defaultMessage": "Willkommen!" + }, + "sign-in": { + "defaultMessage": "Login" + }, + "ssl-certificate": { + "defaultMessage": "SSL-Zertifikate" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Forward Host" + }, + "stream.incoming-port": { + "defaultMessage": "Incoming Port" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "user": { + "defaultMessage": "User" + }, + "user.change-password": { + "defaultMessage": "Passwort ändern" + }, + "user.confirm-password": { + "defaultMessage": "Passwort wiederholen" + }, + "user.current-password": { + "defaultMessage": "Aktuelles Passwort" + }, + "user.edit-profile": { + "defaultMessage": "Profil bearbeiten" + }, + "user.full-name": { + "defaultMessage": "Name" + }, + "user.login-as": { + "defaultMessage": "Einloggen als {name}" + }, + "user.logout": { + "defaultMessage": "Ausloggen" + }, + "user.new-password": { + "defaultMessage": "Neues Password" + }, + "user.nickname": { + "defaultMessage": "Nickname" + }, + "user.set-password": { + "defaultMessage": "Passwort setzen" + }, + "user.set-permissions": { + "defaultMessage": "Berechtigungen für {name} setzen" + }, + "user.switch-dark": { + "defaultMessage": "Zum Dark Mode wechseln" + }, + "user.switch-light": { + "defaultMessage": "Zum Light Mode wechseln" + }, + "username": { + "defaultMessage": "Benutzername" + }, + "users": { + "defaultMessage": "Benutzer" + } +} diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json new file mode 100644 index 0000000..bb00ac3 --- /dev/null +++ b/frontend/src/locale/src/en.json @@ -0,0 +1,776 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Backup codes remaining: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Save these backup codes in a secure place. Each code can only be used once." + }, + "2fa.disable": { + "defaultMessage": "Disable Two-Factor Authentication" + }, + "2fa.disable-confirm": { + "defaultMessage": "Disable 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Disabling two-factor authentication will make your account less secure." + }, + "2fa.disabled": { + "defaultMessage": "Disabled" + }, + "2fa.done": { + "defaultMessage": "I have saved my backup codes" + }, + "2fa.enable": { + "defaultMessage": "Enable Two-Factor Authentication" + }, + "2fa.enabled": { + "defaultMessage": "Enabled" + }, + "2fa.enter-code": { + "defaultMessage": "Enter verification code" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Enter verification code to disable" + }, + "2fa.regenerate": { + "defaultMessage": "Regenerate" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Regenerate Backup Codes" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Enter a verification code to generate new backup codes. Your old codes will be invalidated." + }, + "2fa.secret-key": { + "defaultMessage": "Secret Key" + }, + "2fa.setup-instructions": { + "defaultMessage": "Scan this QR code with your authenticator app, or enter the secret manually." + }, + "2fa.status": { + "defaultMessage": "Status" + }, + "2fa.title": { + "defaultMessage": "Two-Factor Authentication" + }, + "2fa.verify-enable": { + "defaultMessage": "Verify and Enable" + }, + "access-list": { + "defaultMessage": "Access List" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {User} other {Users}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "When at least 1 rule exists, this deny all rule will be added last" + }, + "access-list.help.rules-order": { + "defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined." + }, + "access-list.pass-auth": { + "defaultMessage": "Pass Auth to Upstream" + }, + "access-list.public": { + "defaultMessage": "Publicly Accessible" + }, + "access-list.public.subtitle": { + "defaultMessage": "No basic auth required" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Satisfy Any" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}" + }, + "access-lists": { + "defaultMessage": "Access Lists" + }, + "action.add": { + "defaultMessage": "Add" + }, + "action.add-location": { + "defaultMessage": "Add Location" + }, + "action.allow": { + "defaultMessage": "Allow" + }, + "action.close": { + "defaultMessage": "Close" + }, + "action.delete": { + "defaultMessage": "Delete" + }, + "action.deny": { + "defaultMessage": "Deny" + }, + "action.disable": { + "defaultMessage": "Disable" + }, + "action.download": { + "defaultMessage": "Download" + }, + "action.edit": { + "defaultMessage": "Edit" + }, + "action.enable": { + "defaultMessage": "Enable" + }, + "action.permissions": { + "defaultMessage": "Permissions" + }, + "action.renew": { + "defaultMessage": "Renew" + }, + "action.view-details": { + "defaultMessage": "View Details" + }, + "auditlogs": { + "defaultMessage": "Audit Logs" + }, + "auto": { + "defaultMessage": "Auto" + }, + "cancel": { + "defaultMessage": "Cancel" + }, + "certificate": { + "defaultMessage": "Certificate" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificate" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Certificate Key" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Intermediate Certificate" + }, + "certificate.in-use": { + "defaultMessage": "In Use" + }, + "certificate.none.subtitle": { + "defaultMessage": "No certificate assigned" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "This host will not use HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "None" + }, + "certificate.not-in-use": { + "defaultMessage": "Not Used" + }, + "certificate.renew": { + "defaultMessage": "Renew Certificate" + }, + "certificates": { + "defaultMessage": "Certificates" + }, + "certificates.custom": { + "defaultMessage": "Custom Certificate" + }, + "certificates.custom.warning": { + "defaultMessage": "Key files protected with a passphrase are not supported." + }, + "certificates.dns.credentials": { + "defaultMessage": "Credentials File Content" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "This plugin requires a configuration file containing an API token or other credentials for your provider" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "This data will be stored as plaintext in the database and in a file!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagation Seconds" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS Provider" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Select a Provider..." + }, + "certificates.dns.warning": { + "defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation." + }, + "certificates.http.reachability-404": { + "defaultMessage": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Your server is reachable and creating certificates should be possible." + }, + "certificates.http.reachability-other": { + "defaultMessage": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running." + }, + "certificates.http.test-results": { + "defaultMessage": "Test Results" + }, + "certificates.http.warning": { + "defaultMessage": "These domains must be already configured to point to this installation." + }, + "certificates.key-type": { + "defaultMessage": "Key Type" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "with Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Request a new Certificate" + }, + "column.access": { + "defaultMessage": "Access" + }, + "column.authorization": { + "defaultMessage": "Authorization" + }, + "column.authorizations": { + "defaultMessage": "Authorizations" + }, + "column.custom-locations": { + "defaultMessage": "Custom Locations" + }, + "column.destination": { + "defaultMessage": "Destination" + }, + "column.details": { + "defaultMessage": "Details" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Event" + }, + "column.expires": { + "defaultMessage": "Expires" + }, + "column.http-code": { + "defaultMessage": "HTTP Code" + }, + "column.incoming-port": { + "defaultMessage": "Incoming Port" + }, + "column.name": { + "defaultMessage": "Name" + }, + "column.protocol": { + "defaultMessage": "Protocol" + }, + "column.provider": { + "defaultMessage": "Provider" + }, + "column.roles": { + "defaultMessage": "Roles" + }, + "column.rules": { + "defaultMessage": "Rules" + }, + "column.satisfy": { + "defaultMessage": "Satisfy" + }, + "column.satisfy-all": { + "defaultMessage": "All" + }, + "column.satisfy-any": { + "defaultMessage": "Any" + }, + "column.scheme": { + "defaultMessage": "Scheme" + }, + "column.source": { + "defaultMessage": "Source" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Created: {date}" + }, + "dashboard": { + "defaultMessage": "Dashboard" + }, + "dead-host": { + "defaultMessage": "404 Host" + }, + "dead-hosts": { + "defaultMessage": "404 Hosts" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}" + }, + "disabled": { + "defaultMessage": "Disabled" + }, + "domain-names": { + "defaultMessage": "Domain Names" + }, + "domain-names.max": { + "defaultMessage": "{count} domain names maximum" + }, + "domain-names.placeholder": { + "defaultMessage": "Start typing to add domain..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards not permitted for this type" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards not supported for this CA" + }, + "domains.advanced": { + "defaultMessage": "Advanced" + }, + "domains.force-ssl": { + "defaultMessage": "Force SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Enabled" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Sub-domains" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 Support" + }, + "domains.trust-forwarded-proto": { + "defaultMessage": "Trust Upstream Forwarded Proto Headers" + }, + "domains.use-dns": { + "defaultMessage": "Use DNS Challenge" + }, + "email-address": { + "defaultMessage": "Email address" + }, + "empty-search": { + "defaultMessage": "No results found" + }, + "empty-subtitle": { + "defaultMessage": "Why don't you create one?" + }, + "enabled": { + "defaultMessage": "Enabled" + }, + "error.access.at-least-one": { + "defaultMessage": "Either one Authorization or one Access Rule is required" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Authorization Usernames must be unique" + }, + "error.invalid-auth": { + "defaultMessage": "Invalid email or password" + }, + "error.invalid-domain": { + "defaultMessage": "Invalid domain: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Invalid email address" + }, + "error.max-character-length": { + "defaultMessage": "Maximum length is {max} character{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Too many domains, max is {max}" + }, + "error.maximum": { + "defaultMessage": "Maximum is {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimum length is {min} character{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Minimum is {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Passwords must match" + }, + "error.required": { + "defaultMessage": "This is required" + }, + "expires.on": { + "defaultMessage": "Expires: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork me on Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Block Common Exploits" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache Assets" + }, + "host.flags.preserve-path": { + "defaultMessage": "Preserve Path" + }, + "host.flags.protocols": { + "defaultMessage": "Protocols" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets Support" + }, + "host.forward-port": { + "defaultMessage": "Forward Port" + }, + "host.forward-scheme": { + "defaultMessage": "Scheme" + }, + "hosts": { + "defaultMessage": "Hosts" + }, + "http-only": { + "defaultMessage": "HTTP Only" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Loading…" + }, + "login.2fa-code": { + "defaultMessage": "Verification Code" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Enter code" + }, + "login.2fa-description": { + "defaultMessage": "Enter the code from your authenticator app" + }, + "login.2fa-title": { + "defaultMessage": "Two-Factor Authentication" + }, + "login.2fa-verify": { + "defaultMessage": "Verify" + }, + "login.title": { + "defaultMessage": "Login to your account" + }, + "nginx-config.label": { + "defaultMessage": "Custom Nginx Configuration" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Enter your custom Nginx configuration here at your own risk!" + }, + "no-permission-error": { + "defaultMessage": "You do not have access to view this." + }, + "notfound.action": { + "defaultMessage": "Take me home" + }, + "notfound.content": { + "defaultMessage": "We are sorry but the page you are looking for was not found" + }, + "notfound.title": { + "defaultMessage": "Oops… You just found an error page" + }, + "notification.error": { + "defaultMessage": "Error" + }, + "notification.object-deleted": { + "defaultMessage": "{object} has been deleted" + }, + "notification.object-disabled": { + "defaultMessage": "{object} has been disabled" + }, + "notification.object-enabled": { + "defaultMessage": "{object} has been enabled" + }, + "notification.object-renewed": { + "defaultMessage": "{object} has been renewed" + }, + "notification.object-saved": { + "defaultMessage": "{object} has been saved" + }, + "notification.success": { + "defaultMessage": "Success" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Add {object}" + }, + "object.delete": { + "defaultMessage": "Delete {object}" + }, + "object.delete.content": { + "defaultMessage": "Are you sure you want to delete this {object}?" + }, + "object.edit": { + "defaultMessage": "Edit {object}" + }, + "object.empty": { + "defaultMessage": "There are no {objects}" + }, + "object.event.created": { + "defaultMessage": "Created {object}" + }, + "object.event.deleted": { + "defaultMessage": "Deleted {object}" + }, + "object.event.disabled": { + "defaultMessage": "Disabled {object}" + }, + "object.event.enabled": { + "defaultMessage": "Enabled {object}" + }, + "object.event.renewed": { + "defaultMessage": "Renewed {object}" + }, + "object.event.updated": { + "defaultMessage": "Updated {object}" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Options" + }, + "password": { + "defaultMessage": "Password" + }, + "password.generate": { + "defaultMessage": "Generate random password" + }, + "password.hide": { + "defaultMessage": "Hide Password" + }, + "password.show": { + "defaultMessage": "Show Password" + }, + "permissions.hidden": { + "defaultMessage": "Hidden" + }, + "permissions.manage": { + "defaultMessage": "Manage" + }, + "permissions.view": { + "defaultMessage": "View Only" + }, + "permissions.visibility.all": { + "defaultMessage": "All Items" + }, + "permissions.visibility.title": { + "defaultMessage": "Item Visibility" + }, + "permissions.visibility.user": { + "defaultMessage": "Created Items Only" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Forward Hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Hosts" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}" + }, + "public": { + "defaultMessage": "Public" + }, + "redirection-host": { + "defaultMessage": "Redirection Host" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Forward Domain" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP Code" + }, + "redirection-hosts": { + "defaultMessage": "Redirection Hosts" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Multiple Choices" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Moved permanently" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Moved temporarily" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 See other" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Temporary redirect" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Permanent redirect" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Standard User" + }, + "save": { + "defaultMessage": "Save" + }, + "setting": { + "defaultMessage": "Setting" + }, + "settings": { + "defaultMessage": "Settings" + }, + "settings.default-site": { + "defaultMessage": "Default Site" + }, + "settings.default-site.404": { + "defaultMessage": "404 Page" + }, + "settings.default-site.444": { + "defaultMessage": "No Response (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Congratulations Page" + }, + "settings.default-site.description": { + "defaultMessage": "What to show when Nginx is hit with an unknown Host" + }, + "settings.default-site.html": { + "defaultMessage": "Custom HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirect" + }, + "setup.preamble": { + "defaultMessage": "Get started by creating your admin account." + }, + "setup.title": { + "defaultMessage": "Welcome!" + }, + "sign-in": { + "defaultMessage": "Sign in" + }, + "ssl-certificate": { + "defaultMessage": "SSL Certificate" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Forward Host" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Incoming Port" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Update Available: {latestVersion}" + }, + "user": { + "defaultMessage": "User" + }, + "user.change-password": { + "defaultMessage": "Change Password" + }, + "user.confirm-password": { + "defaultMessage": "Confirm Password" + }, + "user.current-password": { + "defaultMessage": "Current Password" + }, + "user.edit-profile": { + "defaultMessage": "Edit Profile" + }, + "user.full-name": { + "defaultMessage": "Full Name" + }, + "user.login-as": { + "defaultMessage": "Sign in as {name}" + }, + "user.logout": { + "defaultMessage": "Logout" + }, + "user.new-password": { + "defaultMessage": "New Password" + }, + "user.nickname": { + "defaultMessage": "Nickname" + }, + "user.set-password": { + "defaultMessage": "Set Password" + }, + "user.set-permissions": { + "defaultMessage": "Set Permissions for {name}" + }, + "user.switch-dark": { + "defaultMessage": "Switch to Dark mode" + }, + "user.switch-light": { + "defaultMessage": "Switch to Light mode" + }, + "user.two-factor": { + "defaultMessage": "Two-Factor Auth" + }, + "username": { + "defaultMessage": "Username" + }, + "users": { + "defaultMessage": "Users" + } +} diff --git a/frontend/src/locale/src/es.json b/frontend/src/locale/src/es.json new file mode 100644 index 0000000..c8b1edb --- /dev/null +++ b/frontend/src/locale/src/es.json @@ -0,0 +1,692 @@ +{ + "access-list": { + "defaultMessage": "Lista de Acceso" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Regla} other {Reglas}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Usuario} other {Usuarios}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Cuando exista al menos 1 regla, esta regla de denegar todo se añadirá al final" + }, + "access-list.help.rules-order": { + "defaultMessage": "Ten en cuenta que las directivas de permitir y denegar se aplicarán en el orden en que estén definidas." + }, + "access-list.pass-auth": { + "defaultMessage": "Pasar Autenticación al Upstream" + }, + "access-list.public": { + "defaultMessage": "Accesible Públicamente" + }, + "access-list.public.subtitle": { + "defaultMessage": "No se requiere autenticación básica" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 o 192.168.1.0/24 o 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Satisfacer Cualquiera" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Usuario} other {Usuarios}}, {rules} {rules, plural, one {Regla} other {Reglas}} - Creado: {date}" + }, + "access-lists": { + "defaultMessage": "Listas de Acceso" + }, + "action.add": { + "defaultMessage": "Añadir" + }, + "action.add-location": { + "defaultMessage": "Añadir Ubicación" + }, + "action.allow": { + "defaultMessage": "Permitir" + }, + "action.close": { + "defaultMessage": "Cerrar" + }, + "action.delete": { + "defaultMessage": "Eliminar" + }, + "action.deny": { + "defaultMessage": "Denegar" + }, + "action.disable": { + "defaultMessage": "Deshabilitar" + }, + "action.download": { + "defaultMessage": "Descargar" + }, + "action.edit": { + "defaultMessage": "Editar" + }, + "action.enable": { + "defaultMessage": "Habilitar" + }, + "action.permissions": { + "defaultMessage": "Permisos" + }, + "action.renew": { + "defaultMessage": "Renovar" + }, + "action.view-details": { + "defaultMessage": "Ver Detalles" + }, + "auditlogs": { + "defaultMessage": "Registros de Auditoría" + }, + "auto": { + "defaultMessage": "Auto" + }, + "cancel": { + "defaultMessage": "Cancelar" + }, + "certificate": { + "defaultMessage": "Certificado" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificado" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Clave del Certificado" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certificado Intermedio" + }, + "certificate.in-use": { + "defaultMessage": "En Uso" + }, + "certificate.none.subtitle": { + "defaultMessage": "Sin certificado asignado" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Este host no usará HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Ninguno" + }, + "certificate.not-in-use": { + "defaultMessage": "Sin Usar" + }, + "certificate.renew": { + "defaultMessage": "Renovar Certificado" + }, + "certificates": { + "defaultMessage": "Certificados" + }, + "certificates.custom": { + "defaultMessage": "Certificado Personalizado" + }, + "certificates.custom.warning": { + "defaultMessage": "No se admiten archivos de claves protegidos con contraseña." + }, + "certificates.dns.credentials": { + "defaultMessage": "Contenido del Archivo de Credenciales" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Este plugin requiere un archivo de configuración que contenga un token de API u otras credenciales para tu proveedor" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "¡Estos datos se almacenarán como texto plano en la base de datos y en un archivo!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Segundos de Propagación" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Dejar vacío para usar el valor predeterminado del plugin. Número de segundos a esperar para la propagación DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Proveedor DNS" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Selecciona un Proveedor..." + }, + "certificates.dns.warning": { + "defaultMessage": "Esta sección requiere algunos conocimientos sobre Certbot y sus plugins DNS. Consulta la documentación de los plugins respectivos." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Se encontró un servidor en este dominio pero no parece ser Nginx Proxy Manager. Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "No se pudo verificar la accesibilidad debido a un error de comunicación con site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "No hay ningún servidor disponible en este dominio. Asegúrate de que tu dominio existe y apunta a la IP donde se está ejecutando tu instancia de NPM y, si es necesario, que el puerto 80 esté redirigido en tu router." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Tu servidor es accesible y debería ser posible crear certificados." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Se encontró un servidor en este dominio pero devolvió un código de estado inesperado {code}. ¿Es el servidor NPM? Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Se encontró un servidor en este dominio pero devolvió datos inesperados. ¿Es el servidor NPM? Asegúrate de que tu dominio apunte a la IP donde se está ejecutando tu instancia de NPM." + }, + "certificates.http.test-results": { + "defaultMessage": "Resultados de la Prueba" + }, + "certificates.http.warning": { + "defaultMessage": "Estos dominios ya deben estar configurados para apuntar a esta instalación." + }, + "certificates.key-type": { + "defaultMessage": "Tipo de Clave" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA es ampliamente compatible, ECDSA es más rápido y seguro pero puede no ser compatible con sistemas antiguos" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "con Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Solicitar un nuevo Certificado" + }, + "column.access": { + "defaultMessage": "Acceso" + }, + "column.authorization": { + "defaultMessage": "Autorización" + }, + "column.authorizations": { + "defaultMessage": "Autorizaciones" + }, + "column.custom-locations": { + "defaultMessage": "Ubicaciones Personalizadas" + }, + "column.destination": { + "defaultMessage": "Destino" + }, + "column.details": { + "defaultMessage": "Detalles" + }, + "column.email": { + "defaultMessage": "Correo Electrónico" + }, + "column.event": { + "defaultMessage": "Evento" + }, + "column.expires": { + "defaultMessage": "Expira" + }, + "column.http-code": { + "defaultMessage": "Código HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Puerto de Entrada" + }, + "column.name": { + "defaultMessage": "Nombre" + }, + "column.protocol": { + "defaultMessage": "Protocolo" + }, + "column.provider": { + "defaultMessage": "Proveedor" + }, + "column.roles": { + "defaultMessage": "Roles" + }, + "column.rules": { + "defaultMessage": "Reglas" + }, + "column.satisfy": { + "defaultMessage": "Satisfacer" + }, + "column.satisfy-all": { + "defaultMessage": "Todo" + }, + "column.satisfy-any": { + "defaultMessage": "Cualquiera" + }, + "column.scheme": { + "defaultMessage": "Esquema" + }, + "column.source": { + "defaultMessage": "Origen" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Estado" + }, + "created-on": { + "defaultMessage": "Creado: {date}" + }, + "dashboard": { + "defaultMessage": "Panel de Control" + }, + "dead-host": { + "defaultMessage": "Host 404" + }, + "dead-hosts": { + "defaultMessage": "Hosts 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host 404} other {Hosts 404}}" + }, + "disabled": { + "defaultMessage": "Deshabilitado" + }, + "domain-names": { + "defaultMessage": "Nombres de Dominio" + }, + "domain-names.max": { + "defaultMessage": "{count} nombres de dominio como máximo" + }, + "domain-names.placeholder": { + "defaultMessage": "Comienza a escribir para añadir dominio..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "No se permiten comodines para este tipo" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "No se admiten comodines para esta CA" + }, + "domains.force-ssl": { + "defaultMessage": "Forzar SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Habilitado" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS en Subdominios" + }, + "domains.http2-support": { + "defaultMessage": "Soporte HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Usar Desafío DNS" + }, + "email-address": { + "defaultMessage": "Dirección de correo electrónico" + }, + "empty-search": { + "defaultMessage": "No se encontraron resultados" + }, + "empty-subtitle": { + "defaultMessage": "¿Por qué no creas uno?" + }, + "enabled": { + "defaultMessage": "Habilitado" + }, + "error.access.at-least-one": { + "defaultMessage": "Se requiere al menos una Autorización o una Regla de Acceso" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Los nombres de usuario de autorización deben ser únicos" + }, + "error.invalid-auth": { + "defaultMessage": "Correo electrónico o contraseña no válidos" + }, + "error.invalid-domain": { + "defaultMessage": "Dominio no válido: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Dirección de correo electrónico no válida" + }, + "error.max-character-length": { + "defaultMessage": "La longitud máxima es {max} caracter{max, plural, one {} other {es}}" + }, + "error.max-domains": { + "defaultMessage": "Demasiados dominios, el máximo es {max}" + }, + "error.maximum": { + "defaultMessage": "El máximo es {max}" + }, + "error.min-character-length": { + "defaultMessage": "La longitud mínima es {min} caracter{min, plural, one {} other {es}}" + }, + "error.minimum": { + "defaultMessage": "El mínimo es {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Las contraseñas deben coincidir" + }, + "error.required": { + "defaultMessage": "Este campo es obligatorio" + }, + "expires.on": { + "defaultMessage": "Expira: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Bifúrcame en Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Bloquear Exploits Comunes" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cachear Recursos" + }, + "host.flags.preserve-path": { + "defaultMessage": "Preservar Ruta" + }, + "host.flags.protocols": { + "defaultMessage": "Protocolos" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Soporte de Websockets" + }, + "host.forward-port": { + "defaultMessage": "Puerto" + }, + "host.forward-scheme": { + "defaultMessage": "Esquema" + }, + "hosts": { + "defaultMessage": "Hosts" + }, + "http-only": { + "defaultMessage": "Solo HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt vía DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt vía HTTP" + }, + "loading": { + "defaultMessage": "Cargando…" + }, + "login.title": { + "defaultMessage": "Inicia sesión en tu cuenta" + }, + "nginx-config.label": { + "defaultMessage": "Configuración Personalizada de Nginx" + }, + "nginx-config.placeholder": { + "defaultMessage": "# ¡Introduce aquí tu configuración personalizada de Nginx bajo tu propio riesgo!" + }, + "no-permission-error": { + "defaultMessage": "No tienes acceso para ver esto." + }, + "notfound.action": { + "defaultMessage": "Llévame al inicio" + }, + "notfound.content": { + "defaultMessage": "Lo sentimos, pero la página que buscas no fue encontrada" + }, + "notfound.title": { + "defaultMessage": "Ups… Has encontrado una página de error" + }, + "notification.error": { + "defaultMessage": "Error" + }, + "notification.object-deleted": { + "defaultMessage": "{object} ha sido eliminado" + }, + "notification.object-disabled": { + "defaultMessage": "{object} ha sido deshabilitado" + }, + "notification.object-enabled": { + "defaultMessage": "{object} ha sido habilitado" + }, + "notification.object-renewed": { + "defaultMessage": "{object} ha sido renovado" + }, + "notification.object-saved": { + "defaultMessage": "{object} ha sido guardado" + }, + "notification.success": { + "defaultMessage": "Éxito" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Añadir {object}" + }, + "object.delete": { + "defaultMessage": "Eliminar {object}" + }, + "object.delete.content": { + "defaultMessage": "¿Estás seguro de que quieres eliminar este {object}?" + }, + "object.edit": { + "defaultMessage": "Editar {object}" + }, + "object.empty": { + "defaultMessage": "No hay {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} Creado" + }, + "object.event.deleted": { + "defaultMessage": "{object} Eliminado" + }, + "object.event.disabled": { + "defaultMessage": "{object} Deshabilitado" + }, + "object.event.enabled": { + "defaultMessage": "{object} Habilitado" + }, + "object.event.renewed": { + "defaultMessage": "{object} Renovado" + }, + "object.event.updated": { + "defaultMessage": "{object} Actualizado" + }, + "offline": { + "defaultMessage": "Desconectado" + }, + "online": { + "defaultMessage": "Conectado" + }, + "options": { + "defaultMessage": "Opciones" + }, + "password": { + "defaultMessage": "Contraseña" + }, + "password.generate": { + "defaultMessage": "Generar contraseña aleatoria" + }, + "password.hide": { + "defaultMessage": "Ocultar Contraseña" + }, + "password.show": { + "defaultMessage": "Mostrar Contraseña" + }, + "permissions.hidden": { + "defaultMessage": "Oculto" + }, + "permissions.manage": { + "defaultMessage": "Gestionar" + }, + "permissions.view": { + "defaultMessage": "Solo Ver" + }, + "permissions.visibility.all": { + "defaultMessage": "Todos los Elementos" + }, + "permissions.visibility.title": { + "defaultMessage": "Visibilidad de Elementos" + }, + "permissions.visibility.user": { + "defaultMessage": "Solo Elementos Creados" + }, + "proxy-host": { + "defaultMessage": "Host Proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Nombre de Host / IP de Reenvío" + }, + "proxy-hosts": { + "defaultMessage": "Hosts Proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host Proxy} other {Hosts Proxy}}" + }, + "public": { + "defaultMessage": "Público" + }, + "redirection-host": { + "defaultMessage": "Host de Redirección" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Dominio de Reenvío" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Código HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Hosts de Redirección" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host de Redirección} other {Hosts de Redirección}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Multiples Opciones" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Movido permanentemente" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Movido temporalmente" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Ver otro" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Redirección temporal" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Redirección permanente" + }, + "role.admin": { + "defaultMessage": "Administrador" + }, + "role.standard-user": { + "defaultMessage": "Usuario Estándar" + }, + "save": { + "defaultMessage": "Guardar" + }, + "setting": { + "defaultMessage": "Configuración" + }, + "settings": { + "defaultMessage": "Configuración" + }, + "settings.default-site": { + "defaultMessage": "Sitio Predeterminado" + }, + "settings.default-site.404": { + "defaultMessage": "Página 404" + }, + "settings.default-site.444": { + "defaultMessage": "Sin Respuesta (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Página de Felicitaciones" + }, + "settings.default-site.description": { + "defaultMessage": "Qué mostrar cuando Nginx recibe un Host desconocido" + }, + "settings.default-site.html": { + "defaultMessage": "HTML Personalizado" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirigir" + }, + "setup.preamble": { + "defaultMessage": "Comienza creando tu cuenta de administrador." + }, + "setup.title": { + "defaultMessage": "¡Bienvenido!" + }, + "sign-in": { + "defaultMessage": "Iniciar Sesión" + }, + "ssl-certificate": { + "defaultMessage": "Certificado SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Host de Reenvío" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com o 10.0.0.1 o 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Puerto de Entrada" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Probar" + }, + "user": { + "defaultMessage": "Usuario" + }, + "user.change-password": { + "defaultMessage": "Cambiar Contraseña" + }, + "user.confirm-password": { + "defaultMessage": "Confirmar Contraseña" + }, + "user.current-password": { + "defaultMessage": "Contraseña Actual" + }, + "user.edit-profile": { + "defaultMessage": "Editar Perfil" + }, + "user.full-name": { + "defaultMessage": "Nombre Completo" + }, + "user.login-as": { + "defaultMessage": "Iniciar sesión como {name}" + }, + "user.logout": { + "defaultMessage": "Cerrar Sesión" + }, + "user.new-password": { + "defaultMessage": "Nueva Contraseña" + }, + "user.nickname": { + "defaultMessage": "Apodo" + }, + "user.set-password": { + "defaultMessage": "Establecer Contraseña" + }, + "user.set-permissions": { + "defaultMessage": "Establecer Permisos para {name}" + }, + "user.switch-dark": { + "defaultMessage": "Cambiar a modo Oscuro" + }, + "user.switch-light": { + "defaultMessage": "Cambiar a modo Claro" + }, + "username": { + "defaultMessage": "Nombre de Usuario" + }, + "users": { + "defaultMessage": "Usuarios" + } +} diff --git a/frontend/src/locale/src/et.json b/frontend/src/locale/src/et.json new file mode 100644 index 0000000..bb00ac3 --- /dev/null +++ b/frontend/src/locale/src/et.json @@ -0,0 +1,776 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Backup codes remaining: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Save these backup codes in a secure place. Each code can only be used once." + }, + "2fa.disable": { + "defaultMessage": "Disable Two-Factor Authentication" + }, + "2fa.disable-confirm": { + "defaultMessage": "Disable 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Disabling two-factor authentication will make your account less secure." + }, + "2fa.disabled": { + "defaultMessage": "Disabled" + }, + "2fa.done": { + "defaultMessage": "I have saved my backup codes" + }, + "2fa.enable": { + "defaultMessage": "Enable Two-Factor Authentication" + }, + "2fa.enabled": { + "defaultMessage": "Enabled" + }, + "2fa.enter-code": { + "defaultMessage": "Enter verification code" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Enter verification code to disable" + }, + "2fa.regenerate": { + "defaultMessage": "Regenerate" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Regenerate Backup Codes" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Enter a verification code to generate new backup codes. Your old codes will be invalidated." + }, + "2fa.secret-key": { + "defaultMessage": "Secret Key" + }, + "2fa.setup-instructions": { + "defaultMessage": "Scan this QR code with your authenticator app, or enter the secret manually." + }, + "2fa.status": { + "defaultMessage": "Status" + }, + "2fa.title": { + "defaultMessage": "Two-Factor Authentication" + }, + "2fa.verify-enable": { + "defaultMessage": "Verify and Enable" + }, + "access-list": { + "defaultMessage": "Access List" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {User} other {Users}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "When at least 1 rule exists, this deny all rule will be added last" + }, + "access-list.help.rules-order": { + "defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined." + }, + "access-list.pass-auth": { + "defaultMessage": "Pass Auth to Upstream" + }, + "access-list.public": { + "defaultMessage": "Publicly Accessible" + }, + "access-list.public.subtitle": { + "defaultMessage": "No basic auth required" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Satisfy Any" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}" + }, + "access-lists": { + "defaultMessage": "Access Lists" + }, + "action.add": { + "defaultMessage": "Add" + }, + "action.add-location": { + "defaultMessage": "Add Location" + }, + "action.allow": { + "defaultMessage": "Allow" + }, + "action.close": { + "defaultMessage": "Close" + }, + "action.delete": { + "defaultMessage": "Delete" + }, + "action.deny": { + "defaultMessage": "Deny" + }, + "action.disable": { + "defaultMessage": "Disable" + }, + "action.download": { + "defaultMessage": "Download" + }, + "action.edit": { + "defaultMessage": "Edit" + }, + "action.enable": { + "defaultMessage": "Enable" + }, + "action.permissions": { + "defaultMessage": "Permissions" + }, + "action.renew": { + "defaultMessage": "Renew" + }, + "action.view-details": { + "defaultMessage": "View Details" + }, + "auditlogs": { + "defaultMessage": "Audit Logs" + }, + "auto": { + "defaultMessage": "Auto" + }, + "cancel": { + "defaultMessage": "Cancel" + }, + "certificate": { + "defaultMessage": "Certificate" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificate" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Certificate Key" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Intermediate Certificate" + }, + "certificate.in-use": { + "defaultMessage": "In Use" + }, + "certificate.none.subtitle": { + "defaultMessage": "No certificate assigned" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "This host will not use HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "None" + }, + "certificate.not-in-use": { + "defaultMessage": "Not Used" + }, + "certificate.renew": { + "defaultMessage": "Renew Certificate" + }, + "certificates": { + "defaultMessage": "Certificates" + }, + "certificates.custom": { + "defaultMessage": "Custom Certificate" + }, + "certificates.custom.warning": { + "defaultMessage": "Key files protected with a passphrase are not supported." + }, + "certificates.dns.credentials": { + "defaultMessage": "Credentials File Content" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "This plugin requires a configuration file containing an API token or other credentials for your provider" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "This data will be stored as plaintext in the database and in a file!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagation Seconds" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Leave empty to use the plugins default value. Number of seconds to wait for DNS propagation." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS Provider" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Select a Provider..." + }, + "certificates.dns.warning": { + "defaultMessage": "This section requires some knowledge about Certbot and its DNS plugins. Please consult the respective plugins documentation." + }, + "certificates.http.reachability-404": { + "defaultMessage": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Failed to check the reachability due to a communication error with site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Your server is reachable and creating certificates should be possible." + }, + "certificates.http.reachability-other": { + "defaultMessage": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running." + }, + "certificates.http.test-results": { + "defaultMessage": "Test Results" + }, + "certificates.http.warning": { + "defaultMessage": "These domains must be already configured to point to this installation." + }, + "certificates.key-type": { + "defaultMessage": "Key Type" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA is widely compatible, ECDSA is faster and more secure but may not be supported by older systems" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "with Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Request a new Certificate" + }, + "column.access": { + "defaultMessage": "Access" + }, + "column.authorization": { + "defaultMessage": "Authorization" + }, + "column.authorizations": { + "defaultMessage": "Authorizations" + }, + "column.custom-locations": { + "defaultMessage": "Custom Locations" + }, + "column.destination": { + "defaultMessage": "Destination" + }, + "column.details": { + "defaultMessage": "Details" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Event" + }, + "column.expires": { + "defaultMessage": "Expires" + }, + "column.http-code": { + "defaultMessage": "HTTP Code" + }, + "column.incoming-port": { + "defaultMessage": "Incoming Port" + }, + "column.name": { + "defaultMessage": "Name" + }, + "column.protocol": { + "defaultMessage": "Protocol" + }, + "column.provider": { + "defaultMessage": "Provider" + }, + "column.roles": { + "defaultMessage": "Roles" + }, + "column.rules": { + "defaultMessage": "Rules" + }, + "column.satisfy": { + "defaultMessage": "Satisfy" + }, + "column.satisfy-all": { + "defaultMessage": "All" + }, + "column.satisfy-any": { + "defaultMessage": "Any" + }, + "column.scheme": { + "defaultMessage": "Scheme" + }, + "column.source": { + "defaultMessage": "Source" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Created: {date}" + }, + "dashboard": { + "defaultMessage": "Dashboard" + }, + "dead-host": { + "defaultMessage": "404 Host" + }, + "dead-hosts": { + "defaultMessage": "404 Hosts" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}" + }, + "disabled": { + "defaultMessage": "Disabled" + }, + "domain-names": { + "defaultMessage": "Domain Names" + }, + "domain-names.max": { + "defaultMessage": "{count} domain names maximum" + }, + "domain-names.placeholder": { + "defaultMessage": "Start typing to add domain..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards not permitted for this type" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards not supported for this CA" + }, + "domains.advanced": { + "defaultMessage": "Advanced" + }, + "domains.force-ssl": { + "defaultMessage": "Force SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Enabled" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Sub-domains" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 Support" + }, + "domains.trust-forwarded-proto": { + "defaultMessage": "Trust Upstream Forwarded Proto Headers" + }, + "domains.use-dns": { + "defaultMessage": "Use DNS Challenge" + }, + "email-address": { + "defaultMessage": "Email address" + }, + "empty-search": { + "defaultMessage": "No results found" + }, + "empty-subtitle": { + "defaultMessage": "Why don't you create one?" + }, + "enabled": { + "defaultMessage": "Enabled" + }, + "error.access.at-least-one": { + "defaultMessage": "Either one Authorization or one Access Rule is required" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Authorization Usernames must be unique" + }, + "error.invalid-auth": { + "defaultMessage": "Invalid email or password" + }, + "error.invalid-domain": { + "defaultMessage": "Invalid domain: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Invalid email address" + }, + "error.max-character-length": { + "defaultMessage": "Maximum length is {max} character{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Too many domains, max is {max}" + }, + "error.maximum": { + "defaultMessage": "Maximum is {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimum length is {min} character{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Minimum is {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Passwords must match" + }, + "error.required": { + "defaultMessage": "This is required" + }, + "expires.on": { + "defaultMessage": "Expires: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork me on Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Block Common Exploits" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache Assets" + }, + "host.flags.preserve-path": { + "defaultMessage": "Preserve Path" + }, + "host.flags.protocols": { + "defaultMessage": "Protocols" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets Support" + }, + "host.forward-port": { + "defaultMessage": "Forward Port" + }, + "host.forward-scheme": { + "defaultMessage": "Scheme" + }, + "hosts": { + "defaultMessage": "Hosts" + }, + "http-only": { + "defaultMessage": "HTTP Only" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Loading…" + }, + "login.2fa-code": { + "defaultMessage": "Verification Code" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Enter code" + }, + "login.2fa-description": { + "defaultMessage": "Enter the code from your authenticator app" + }, + "login.2fa-title": { + "defaultMessage": "Two-Factor Authentication" + }, + "login.2fa-verify": { + "defaultMessage": "Verify" + }, + "login.title": { + "defaultMessage": "Login to your account" + }, + "nginx-config.label": { + "defaultMessage": "Custom Nginx Configuration" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Enter your custom Nginx configuration here at your own risk!" + }, + "no-permission-error": { + "defaultMessage": "You do not have access to view this." + }, + "notfound.action": { + "defaultMessage": "Take me home" + }, + "notfound.content": { + "defaultMessage": "We are sorry but the page you are looking for was not found" + }, + "notfound.title": { + "defaultMessage": "Oops… You just found an error page" + }, + "notification.error": { + "defaultMessage": "Error" + }, + "notification.object-deleted": { + "defaultMessage": "{object} has been deleted" + }, + "notification.object-disabled": { + "defaultMessage": "{object} has been disabled" + }, + "notification.object-enabled": { + "defaultMessage": "{object} has been enabled" + }, + "notification.object-renewed": { + "defaultMessage": "{object} has been renewed" + }, + "notification.object-saved": { + "defaultMessage": "{object} has been saved" + }, + "notification.success": { + "defaultMessage": "Success" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Add {object}" + }, + "object.delete": { + "defaultMessage": "Delete {object}" + }, + "object.delete.content": { + "defaultMessage": "Are you sure you want to delete this {object}?" + }, + "object.edit": { + "defaultMessage": "Edit {object}" + }, + "object.empty": { + "defaultMessage": "There are no {objects}" + }, + "object.event.created": { + "defaultMessage": "Created {object}" + }, + "object.event.deleted": { + "defaultMessage": "Deleted {object}" + }, + "object.event.disabled": { + "defaultMessage": "Disabled {object}" + }, + "object.event.enabled": { + "defaultMessage": "Enabled {object}" + }, + "object.event.renewed": { + "defaultMessage": "Renewed {object}" + }, + "object.event.updated": { + "defaultMessage": "Updated {object}" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Options" + }, + "password": { + "defaultMessage": "Password" + }, + "password.generate": { + "defaultMessage": "Generate random password" + }, + "password.hide": { + "defaultMessage": "Hide Password" + }, + "password.show": { + "defaultMessage": "Show Password" + }, + "permissions.hidden": { + "defaultMessage": "Hidden" + }, + "permissions.manage": { + "defaultMessage": "Manage" + }, + "permissions.view": { + "defaultMessage": "View Only" + }, + "permissions.visibility.all": { + "defaultMessage": "All Items" + }, + "permissions.visibility.title": { + "defaultMessage": "Item Visibility" + }, + "permissions.visibility.user": { + "defaultMessage": "Created Items Only" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Forward Hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Hosts" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}" + }, + "public": { + "defaultMessage": "Public" + }, + "redirection-host": { + "defaultMessage": "Redirection Host" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Forward Domain" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP Code" + }, + "redirection-hosts": { + "defaultMessage": "Redirection Hosts" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Multiple Choices" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Moved permanently" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Moved temporarily" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 See other" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Temporary redirect" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Permanent redirect" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Standard User" + }, + "save": { + "defaultMessage": "Save" + }, + "setting": { + "defaultMessage": "Setting" + }, + "settings": { + "defaultMessage": "Settings" + }, + "settings.default-site": { + "defaultMessage": "Default Site" + }, + "settings.default-site.404": { + "defaultMessage": "404 Page" + }, + "settings.default-site.444": { + "defaultMessage": "No Response (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Congratulations Page" + }, + "settings.default-site.description": { + "defaultMessage": "What to show when Nginx is hit with an unknown Host" + }, + "settings.default-site.html": { + "defaultMessage": "Custom HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirect" + }, + "setup.preamble": { + "defaultMessage": "Get started by creating your admin account." + }, + "setup.title": { + "defaultMessage": "Welcome!" + }, + "sign-in": { + "defaultMessage": "Sign in" + }, + "ssl-certificate": { + "defaultMessage": "SSL Certificate" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Forward Host" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com or 10.0.0.1 or 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Incoming Port" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Update Available: {latestVersion}" + }, + "user": { + "defaultMessage": "User" + }, + "user.change-password": { + "defaultMessage": "Change Password" + }, + "user.confirm-password": { + "defaultMessage": "Confirm Password" + }, + "user.current-password": { + "defaultMessage": "Current Password" + }, + "user.edit-profile": { + "defaultMessage": "Edit Profile" + }, + "user.full-name": { + "defaultMessage": "Full Name" + }, + "user.login-as": { + "defaultMessage": "Sign in as {name}" + }, + "user.logout": { + "defaultMessage": "Logout" + }, + "user.new-password": { + "defaultMessage": "New Password" + }, + "user.nickname": { + "defaultMessage": "Nickname" + }, + "user.set-password": { + "defaultMessage": "Set Password" + }, + "user.set-permissions": { + "defaultMessage": "Set Permissions for {name}" + }, + "user.switch-dark": { + "defaultMessage": "Switch to Dark mode" + }, + "user.switch-light": { + "defaultMessage": "Switch to Light mode" + }, + "user.two-factor": { + "defaultMessage": "Two-Factor Auth" + }, + "username": { + "defaultMessage": "Username" + }, + "users": { + "defaultMessage": "Users" + } +} diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json new file mode 100644 index 0000000..c715c02 --- /dev/null +++ b/frontend/src/locale/src/fr.json @@ -0,0 +1,647 @@ +{ + "access-list": { + "defaultMessage": "Liste d'accès" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Règle} other {Règles}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Utilisateur} other {Utilisateurs}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier." + }, + "access-list.help.rules-order": { + "defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies." + }, + "access-list.pass-auth": { + "defaultMessage": "Transmettre l'authentification au serveur en amont" + }, + "access-list.public": { + "defaultMessage": "Accessible au public" + }, + "access-list.public.subtitle": { + "defaultMessage": "Aucune authentification de base requise" + }, + "access-list.satisfy-any": { + "defaultMessage": "Valide n'importe quelle règle" + }, + "access-list.subtitle": { + "defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}" + }, + "access-lists": { + "defaultMessage": "Listes d'accès" + }, + "action.add": { + "defaultMessage": "Ajouter" + }, + "action.add-location": { + "defaultMessage": "Ajouter localisation" + }, + "action.close": { + "defaultMessage": "Fermer" + }, + "action.delete": { + "defaultMessage": "Supprimer" + }, + "action.disable": { + "defaultMessage": "Désactiver" + }, + "action.download": { + "defaultMessage": "Télécharger" + }, + "action.edit": { + "defaultMessage": "Modifier" + }, + "action.enable": { + "defaultMessage": "Activer" + }, + "action.permissions": { + "defaultMessage": "Permissions" + }, + "action.renew": { + "defaultMessage": "Renouveler" + }, + "action.view-details": { + "defaultMessage": "Voir les Détails" + }, + "auditlogs": { + "defaultMessage": "Journaux d'audit" + }, + "cancel": { + "defaultMessage": "Annuler" + }, + "certificate": { + "defaultMessage": "Certificat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Clé du Certificat" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certificat intermédiaire" + }, + "certificate.in-use": { + "defaultMessage": "Utilisé" + }, + "certificate.none.subtitle": { + "defaultMessage": "Aucun certificat assigné" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Cet hôte n'utilisera pas le HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Aucun" + }, + "certificate.not-in-use": { + "defaultMessage": "Non utilisé" + }, + "certificate.renew": { + "defaultMessage": "Renouveler Certificat" + }, + "certificates": { + "defaultMessage": "Certificats" + }, + "certificates.custom": { + "defaultMessage": "Certificat personnalisé" + }, + "certificates.custom.warning": { + "defaultMessage": "Les fichiers de clé protégés par une passphrase ne sont pas acceptés." + }, + "certificates.dns.credentials": { + "defaultMessage": "Contenu du fichier d'identifiants" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagation Seconds" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Fournisseur DNS" + }, + "certificates.dns.warning": { + "defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Aucun serveur n'est disponible pour ce domaine. Veuillez vérifier que votre domaine existe et pointe vers l'adresse IP où votre instance NPM est exécutée. Si nécessaire, le port 80 est ouvert dans votre routeur." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.test-results": { + "defaultMessage": "Résultats du test" + }, + "certificates.http.warning": { + "defaultMessage": "Ces domaines doivent déjà être configurés pour pointer vers cette installation." + }, + "certificates.request.subtitle": { + "defaultMessage": "avec Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Demander un nouveau certificat" + }, + "column.access": { + "defaultMessage": "Accès" + }, + "column.authorization": { + "defaultMessage": "Autorisation" + }, + "column.authorizations": { + "defaultMessage": "Autorisations" + }, + "column.custom-locations": { + "defaultMessage": "Emplacement personnalisé" + }, + "column.destination": { + "defaultMessage": "Destination" + }, + "column.details": { + "defaultMessage": "Détails" + }, + "column.email": { + "defaultMessage": "eMail" + }, + "column.event": { + "defaultMessage": "Évènement" + }, + "column.expires": { + "defaultMessage": "Expire" + }, + "column.http-code": { + "defaultMessage": "Code HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Port entrant" + }, + "column.name": { + "defaultMessage": "Nom" + }, + "column.protocol": { + "defaultMessage": "Protocole" + }, + "column.provider": { + "defaultMessage": "Fournisseur" + }, + "column.roles": { + "defaultMessage": "Rôles" + }, + "column.rules": { + "defaultMessage": "Règles" + }, + "column.satisfy": { + "defaultMessage": "Valide" + }, + "column.satisfy-all": { + "defaultMessage": "All" + }, + "column.satisfy-any": { + "defaultMessage": "Any" + }, + "column.scheme": { + "defaultMessage": "Schéma" + }, + "column.source": { + "defaultMessage": "Source" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Statut" + }, + "created-on": { + "defaultMessage": "Créé : {date}" + }, + "dashboard": { + "defaultMessage": "Tableau de bord" + }, + "dead-host": { + "defaultMessage": "Hôte 404" + }, + "dead-hosts": { + "defaultMessage": "Hôtes 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte 404} other {Hôtes 404}}" + }, + "disabled": { + "defaultMessage": "Désactivé" + }, + "domain-names": { + "defaultMessage": "Noms de domaine" + }, + "domain-names.max": { + "defaultMessage": "{count} noms de domaine au maximum" + }, + "domain-names.placeholder": { + "defaultMessage": "Commencez à écrire pour ajouter un domaine…" + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Les Wildcards ne sont pas permises dans ce cas" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification." + }, + "domains.force-ssl": { + "defaultMessage": "Forcer SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS activé" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Sous-domaines HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Prise en charge de HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Utiliser le challenge DNS" + }, + "email-address": { + "defaultMessage": "Adresse eMail" + }, + "empty-search": { + "defaultMessage": "Aucun résultat trouvé" + }, + "empty-subtitle": { + "defaultMessage": "Pourquoi n'en créez-vous pas un ?" + }, + "enabled": { + "defaultMessage": "Activé" + }, + "error.access.at-least-one": { + "defaultMessage": "Une autorisation ou une règle d'accès est requise." + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Les noms d'utilisateurs autorisés doivent être uniques" + }, + "error.invalid-auth": { + "defaultMessage": "Adresse eMail ou mot de passe invalide" + }, + "error.invalid-domain": { + "defaultMessage": "Domaine invalide : {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Adresse eMail invalide" + }, + "error.max-character-length": { + "defaultMessage": "La longueur maximale est {max} caractère{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Trop de domaines, le maximum est {max}" + }, + "error.maximum": { + "defaultMessage": "Le maximum est {max}" + }, + "error.min-character-length": { + "defaultMessage": "La longueur minimale est {min} caractère{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Le minimum est {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Les mots de passe doivent correspondre" + }, + "error.required": { + "defaultMessage": "Ceci est obligatoire" + }, + "expires.on": { + "defaultMessage": "Expire : {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forkez-moi sur Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Bloquer les exploits courants" + }, + "host.flags.cache-assets": { + "defaultMessage": "Ressources du cache" + }, + "host.flags.preserve-path": { + "defaultMessage": "Préserver le chemin" + }, + "host.flags.protocols": { + "defaultMessage": "Protocoles" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Prise en charge de Websockets" + }, + "host.forward-port": { + "defaultMessage": "Port de redirection" + }, + "host.forward-scheme": { + "defaultMessage": "Schéma" + }, + "hosts": { + "defaultMessage": "Hôtes" + }, + "http-only": { + "defaultMessage": "HTTP uniquement" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Chargement…" + }, + "login.title": { + "defaultMessage": "Connectez-vous à votre compte" + }, + "nginx-config.label": { + "defaultMessage": "Configuration Nginx personnalisée" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !" + }, + "no-permission-error": { + "defaultMessage": "Vous n'avez pas la permission de voir ce contenu." + }, + "notfound.action": { + "defaultMessage": "Ramenez-moi à l'accueil" + }, + "notfound.content": { + "defaultMessage": "Nous sommes désolés, mais la page que vous cherchez est introuvable" + }, + "notfound.title": { + "defaultMessage": "Oops… Vous avez découvert une page d'erreur" + }, + "notification.error": { + "defaultMessage": "Erreur" + }, + "notification.object-deleted": { + "defaultMessage": "{object} a été supprimé" + }, + "notification.object-disabled": { + "defaultMessage": "{object} a été désactivé" + }, + "notification.object-enabled": { + "defaultMessage": "{object} a été activé" + }, + "notification.object-renewed": { + "defaultMessage": "{object} a été renouvelé" + }, + "notification.object-saved": { + "defaultMessage": "{object} a été enregistré" + }, + "notification.success": { + "defaultMessage": "Réussi" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Ajouter {object}" + }, + "object.delete": { + "defaultMessage": "Supprimer {object}" + }, + "object.delete.content": { + "defaultMessage": "Êtes-vous sûr de vouloir supprimer {object} ?" + }, + "object.edit": { + "defaultMessage": "Modifier {object}" + }, + "object.empty": { + "defaultMessage": "Il n'y a aucun {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} créé" + }, + "object.event.deleted": { + "defaultMessage": "{object} supprimé" + }, + "object.event.disabled": { + "defaultMessage": "{object} désactivé" + }, + "object.event.enabled": { + "defaultMessage": "{object} activé" + }, + "object.event.renewed": { + "defaultMessage": "{object} renouvelé" + }, + "object.event.updated": { + "defaultMessage": "{object} mis à jour" + }, + "offline": { + "defaultMessage": "Hors ligne" + }, + "online": { + "defaultMessage": "En ligne" + }, + "options": { + "defaultMessage": "Options" + }, + "password": { + "defaultMessage": "Mot de passe" + }, + "password.generate": { + "defaultMessage": "Générer un mot de passe aléatoire" + }, + "password.hide": { + "defaultMessage": "Masquer le mot de passe" + }, + "password.show": { + "defaultMessage": "Afficher le mot de passe" + }, + "permissions.hidden": { + "defaultMessage": "Masquer" + }, + "permissions.manage": { + "defaultMessage": "Gérer" + }, + "permissions.view": { + "defaultMessage": "Voir uniquement" + }, + "permissions.visibility.all": { + "defaultMessage": "Tous les éléments" + }, + "permissions.visibility.title": { + "defaultMessage": "Éléments visibles" + }, + "permissions.visibility.user": { + "defaultMessage": "Éléments créés uniquement" + }, + "proxy-host": { + "defaultMessage": "Hôte proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Nom d'hôte de redirection / IP" + }, + "proxy-hosts": { + "defaultMessage": "Hôtes proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}" + }, + "public": { + "defaultMessage": "Publique" + }, + "redirection-host": { + "defaultMessage": "Hôte de redirection" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Domaine de redirection" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Code HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Hôtes de redirection" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}" + }, + "role.admin": { + "defaultMessage": "Administrateur" + }, + "role.standard-user": { + "defaultMessage": "Utilisateur standard" + }, + "save": { + "defaultMessage": "Enregistrer" + }, + "setting": { + "defaultMessage": "Paramètre" + }, + "settings": { + "defaultMessage": "Paramètres" + }, + "settings.default-site": { + "defaultMessage": "Site par défaut" + }, + "settings.default-site.404": { + "defaultMessage": "Page 404" + }, + "settings.default-site.444": { + "defaultMessage": "Aucune réponse (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Page de félicitations" + }, + "settings.default-site.description": { + "defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" + }, + "settings.default-site.html": { + "defaultMessage": "HTML personnalisé" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirection" + }, + "setup.preamble": { + "defaultMessage": "Commencez par créer votre compte administrateur." + }, + "setup.title": { + "defaultMessage": "Bienvenue !" + }, + "sign-in": { + "defaultMessage": "Se connecter" + }, + "ssl-certificate": { + "defaultMessage": "Certificat SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Hôte destinataire" + }, + "stream.incoming-port": { + "defaultMessage": "Port d'entrée" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Mise à jour disponible : {latestVersion}" + }, + "user": { + "defaultMessage": "Utilisateur" + }, + "user.change-password": { + "defaultMessage": "Modifier le mot de passe" + }, + "user.confirm-password": { + "defaultMessage": "Confirmer le mot de passe" + }, + "user.current-password": { + "defaultMessage": "Mot de passe actuel" + }, + "user.edit-profile": { + "defaultMessage": "Modifier le profil" + }, + "user.full-name": { + "defaultMessage": "Nom complet" + }, + "user.login-as": { + "defaultMessage": "Se connecter en tant que {name}" + }, + "user.logout": { + "defaultMessage": "Déconnexion" + }, + "user.new-password": { + "defaultMessage": "Nouveau mot de passe" + }, + "user.nickname": { + "defaultMessage": "Pseudonyme" + }, + "user.set-password": { + "defaultMessage": "Définir le mot de passe" + }, + "user.set-permissions": { + "defaultMessage": "Définir les autorisations pour {name}" + }, + "user.switch-dark": { + "defaultMessage": "Passer au mode Sombre" + }, + "user.switch-light": { + "defaultMessage": "Passer au mode Lumineux" + }, + "username": { + "defaultMessage": "Nom d'utilisateur" + }, + "users": { + "defaultMessage": "Utilisateurs" + } +} diff --git a/frontend/src/locale/src/ga.json b/frontend/src/locale/src/ga.json new file mode 100644 index 0000000..719b863 --- /dev/null +++ b/frontend/src/locale/src/ga.json @@ -0,0 +1,683 @@ +{ + "access-list": { + "defaultMessage": "Liosta Rochtana" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Rial} other {Rialacha}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Úsáideoir} other {Úsáideoirí}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Nuair a bhíonn riail amháin ar a laghad ann, cuirfear an riail seo chun gach rud a dhiúltú leis an gceann deireanach." + }, + "access-list.help.rules-order": { + "defaultMessage": "Tabhair faoi deara go gcuirfear na treoracha ceadaigh agus diúltaigh i bhfeidhm san ord a shainmhínítear iad." + }, + "access-list.pass-auth": { + "defaultMessage": "Tabhair Údarú chuig an Sruth Uachtarach" + }, + "access-list.public": { + "defaultMessage": "Inrochtana don Phobal" + }, + "access-list.public.subtitle": { + "defaultMessage": "Níl aon údarú bunúsach ag teastáil" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 nó 192.168.1.0/24 nó 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Sásaigh Aon" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Úsáideoir} other {Úsáideoirí}}, {rules} {rules, plural, one {Riail} other {Rialacha}} - Cruthaithe: {date}" + }, + "access-lists": { + "defaultMessage": "Liostaí Rochtana" + }, + "action.add": { + "defaultMessage": "Cuir leis" + }, + "action.add-location": { + "defaultMessage": "Cuir Suíomh leis" + }, + "action.allow": { + "defaultMessage": "Ceadaigh" + }, + "action.close": { + "defaultMessage": "Dún" + }, + "action.delete": { + "defaultMessage": "Scrios" + }, + "action.deny": { + "defaultMessage": "Diúltaigh" + }, + "action.disable": { + "defaultMessage": "Díchumasaigh" + }, + "action.download": { + "defaultMessage": "Íoslódáil" + }, + "action.edit": { + "defaultMessage": "Cuir in Eagar" + }, + "action.enable": { + "defaultMessage": "Cumasaigh" + }, + "action.permissions": { + "defaultMessage": "Ceadanna" + }, + "action.renew": { + "defaultMessage": "Athnuachan" + }, + "action.view-details": { + "defaultMessage": "Féach Sonraí" + }, + "auditlogs": { + "defaultMessage": "Logaí Iniúchta" + }, + "auto": { + "defaultMessage": "Uath" + }, + "cancel": { + "defaultMessage": "Cealaigh" + }, + "certificate": { + "defaultMessage": "Teastas" + }, + "certificate.custom-certificate": { + "defaultMessage": "Teastas" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Eochair Teastais" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Teastas Idirmheánach" + }, + "certificate.in-use": { + "defaultMessage": "In Úsáid" + }, + "certificate.none.subtitle": { + "defaultMessage": "Níor sannadh aon deimhniú" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Ní úsáidfidh an t-óstach seo HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Dada" + }, + "certificate.not-in-use": { + "defaultMessage": "Níor Úsáideadh" + }, + "certificate.renew": { + "defaultMessage": "Athnuachan an Teastais" + }, + "certificates": { + "defaultMessage": "Teastais" + }, + "certificates.custom": { + "defaultMessage": "Teastas Saincheaptha" + }, + "certificates.custom.warning": { + "defaultMessage": "Ní thacaítear le comhaid eochair atá cosanta le frása faire." + }, + "certificates.dns.credentials": { + "defaultMessage": "Ábhar Comhaid Dintiúir" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Éilíonn an breiseán seo comhad cumraíochta ina bhfuil comhartha API nó dintiúir eile do do sholáthraí." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Stórálfar an fhaisnéis seo mar théacs simplí sa bhunachar sonraí agus i gcomhad!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Soicindí Iolraithe" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Fág folamh chun luach réamhshocraithe na mbreiseán a úsáid. Líon na soicindí le fanacht le haghaidh iomadú DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Soláthraí DNS" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Roghnaigh Soláthraí..." + }, + "certificates.dns.warning": { + "defaultMessage": "Éilíonn an chuid seo roinnt eolais faoi Certbot agus a bhreiseáin DNS. Féach ar dhoiciméadacht na mbreiseán faoi seach, le do thoil." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach ní cosúil gur Bainisteoir Proxy Nginx atá ann. Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Theip ar sheiceáil an inrochtaineachta mar gheall ar earráid chumarsáide le site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Níl aon fhreastalaí ar fáil ag an bhfearann seo. Cinntigh le do thoil go bhfuil do fhearann ann agus go bhfuil sé ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith agus más gá, go bhfuil port 80 curtha ar aghaidh i do ródaire." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Tá rochtain ar do fhreastalaí agus ba cheart go mbeadh sé indéanta deimhnithe a chruthú." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach thug sé cód stádais gan choinne {code} ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach thug sé sonraí gan choinne ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith." + }, + "certificates.http.test-results": { + "defaultMessage": "Torthaí Tástála" + }, + "certificates.http.warning": { + "defaultMessage": "Ní mór na fearainn seo a bheith cumraithe cheana féin chun pointeáil chuig an suiteáil seo." + }, + "certificates.request.subtitle": { + "defaultMessage": "le Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Iarr Teastas nua" + }, + "column.access": { + "defaultMessage": "Rochtain" + }, + "column.authorization": { + "defaultMessage": "Údarú" + }, + "column.authorizations": { + "defaultMessage": "Údaruithe" + }, + "column.custom-locations": { + "defaultMessage": "Suíomhanna Saincheaptha" + }, + "column.destination": { + "defaultMessage": "Ceann Scríbe" + }, + "column.details": { + "defaultMessage": "Sonraí" + }, + "column.email": { + "defaultMessage": "Ríomhphost" + }, + "column.event": { + "defaultMessage": "Imeacht" + }, + "column.expires": { + "defaultMessage": "Éagaíonn" + }, + "column.http-code": { + "defaultMessage": "Cód HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Port Isteach" + }, + "column.name": { + "defaultMessage": "Ainm" + }, + "column.protocol": { + "defaultMessage": "Prótacal" + }, + "column.provider": { + "defaultMessage": "Soláthraí" + }, + "column.roles": { + "defaultMessage": "Róil" + }, + "column.rules": { + "defaultMessage": "Rialacha" + }, + "column.satisfy": { + "defaultMessage": "Sásamh" + }, + "column.satisfy-all": { + "defaultMessage": "Gach" + }, + "column.satisfy-any": { + "defaultMessage": "Aon" + }, + "column.scheme": { + "defaultMessage": "Scéim" + }, + "column.source": { + "defaultMessage": "Foinse" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Stádas" + }, + "created-on": { + "defaultMessage": "Cruthaithe: {date}" + }, + "dashboard": { + "defaultMessage": "Painéal Rialaithe" + }, + "dead-host": { + "defaultMessage": "Óstach 404" + }, + "dead-hosts": { + "defaultMessage": "404 Óstaigh" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Óstach 404} other {Óstaigh 404}}" + }, + "disabled": { + "defaultMessage": "Míchumasaithe" + }, + "domain-names": { + "defaultMessage": "Ainmneacha Fearainn" + }, + "domain-names.max": { + "defaultMessage": "Uasmhéid d'ainmneacha fearainn: {count}" + }, + "domain-names.placeholder": { + "defaultMessage": "Tosaigh ag clóscríobh chun fearann a chur leis..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Ní cheadaítear cártaí fiáine don chineál seo" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Ní thacaítear le cártaí fiáine don ÚD seo" + }, + "domains.force-ssl": { + "defaultMessage": "Fórsáil SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "Cumasaithe HSTS" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Fo-fhearainn HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Tacaíocht HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Úsáid Dúshlán DNS" + }, + "email-address": { + "defaultMessage": "Seoladh ríomhphoist" + }, + "empty-search": { + "defaultMessage": "Níor aimsíodh aon torthaí" + }, + "empty-subtitle": { + "defaultMessage": "Cén fáth nach gcruthaíonn tú ceann?" + }, + "enabled": { + "defaultMessage": "Cumasaithe" + }, + "error.access.at-least-one": { + "defaultMessage": "Tá Údarú amháin nó Riail Rochtana amháin ag teastáil" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Ní mór d’ainmneacha úsáideora údaraithe a bheith uathúil" + }, + "error.invalid-auth": { + "defaultMessage": "Ríomhphost nó pasfhocal neamhbhailí" + }, + "error.invalid-domain": { + "defaultMessage": "Fearann neamhbhailí: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Seoladh ríomhphoist neamhbhailí" + }, + "error.max-character-length": { + "defaultMessage": "Is é an fad uasta ná {max} carachtar{max, plural, one {} other {anna}}" + }, + "error.max-domains": { + "defaultMessage": "An iomarca fearainn, is é {max} an t-uasmhéid" + }, + "error.maximum": { + "defaultMessage": "Is é {max} an t-uasmhéid" + }, + "error.min-character-length": { + "defaultMessage": "Is é an fad íosta ná {min} carachtar{min, plural, one {} other {anna}}" + }, + "error.minimum": { + "defaultMessage": "Is é {min} an t-íosmhéid" + }, + "error.passwords-must-match": { + "defaultMessage": "Ní mór pasfhocail a bheith mar a chéile" + }, + "error.required": { + "defaultMessage": "Tá sé seo riachtanach" + }, + "expires.on": { + "defaultMessage": "Éagaíonn: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forc mé ar Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blocáil Easnaimh Choitianta" + }, + "host.flags.cache-assets": { + "defaultMessage": "Sócmhainní Taisce" + }, + "host.flags.preserve-path": { + "defaultMessage": "Cosán a Chaomhnú" + }, + "host.flags.protocols": { + "defaultMessage": "Prótacail" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Tacaíocht Websockets" + }, + "host.forward-port": { + "defaultMessage": "Port Ar Aghaidh" + }, + "host.forward-scheme": { + "defaultMessage": "Scéim" + }, + "hosts": { + "defaultMessage": "Óstaigh" + }, + "http-only": { + "defaultMessage": "HTTP Amháin" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt trí DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt trí HTTP" + }, + "loading": { + "defaultMessage": "Ag lódáil…" + }, + "login.title": { + "defaultMessage": "Logáil isteach i do chuntas" + }, + "nginx-config.label": { + "defaultMessage": "Cumraíocht Nginx Saincheaptha" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Cuir isteach do chumraíocht saincheaptha Nginx anseo ar do phriacal féin!" + }, + "no-permission-error": { + "defaultMessage": "Níl rochtain agat chun seo a fheiceáil." + }, + "notfound.action": { + "defaultMessage": "Tabhair abhaile mé" + }, + "notfound.content": { + "defaultMessage": "Tá brón orainn ach níor aimsíodh an leathanach atá á lorg agat" + }, + "notfound.title": { + "defaultMessage": "Úps… Fuair tú leathanach earráide díreach anois." + }, + "notification.error": { + "defaultMessage": "Earráid" + }, + "notification.object-deleted": { + "defaultMessage": "Scriosadh {object}" + }, + "notification.object-disabled": { + "defaultMessage": "Tá {object} díchumasaithe" + }, + "notification.object-enabled": { + "defaultMessage": "Tá {object} cumasaithe" + }, + "notification.object-renewed": { + "defaultMessage": "Tá {object} athnuaite" + }, + "notification.object-saved": { + "defaultMessage": "Tá {object} sábháilte" + }, + "notification.success": { + "defaultMessage": "Rath" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Cuir {object} leis" + }, + "object.delete": { + "defaultMessage": "Scrios {object}" + }, + "object.delete.content": { + "defaultMessage": "An bhfuil tú cinnte gur mian leat an {object} seo a scriosadh?" + }, + "object.edit": { + "defaultMessage": "Cuir in eagar {object}" + }, + "object.empty": { + "defaultMessage": "Níl aon {objects} ann" + }, + "object.event.created": { + "defaultMessage": "Cruthaithe {object}" + }, + "object.event.deleted": { + "defaultMessage": "Scriosadh {object}" + }, + "object.event.disabled": { + "defaultMessage": "Díchumasaithe {object}" + }, + "object.event.enabled": { + "defaultMessage": "Cumasaithe {object}" + }, + "object.event.renewed": { + "defaultMessage": "Athnuaite {object}" + }, + "object.event.updated": { + "defaultMessage": "Nuashonraithe {object}" + }, + "offline": { + "defaultMessage": "As líne" + }, + "online": { + "defaultMessage": "Ar líne" + }, + "options": { + "defaultMessage": "Roghanna" + }, + "password": { + "defaultMessage": "Pasfhocal" + }, + "password.generate": { + "defaultMessage": "Gin pasfhocal randamach" + }, + "password.hide": { + "defaultMessage": "Folaigh Pasfhocal" + }, + "password.show": { + "defaultMessage": "Taispeáin Pasfhocal" + }, + "permissions.hidden": { + "defaultMessage": "I bhfolach" + }, + "permissions.manage": { + "defaultMessage": "Bainistigh" + }, + "permissions.view": { + "defaultMessage": "Amharc Amháin" + }, + "permissions.visibility.all": { + "defaultMessage": "Gach Míreanna" + }, + "permissions.visibility.title": { + "defaultMessage": "Infheictheacht Míre" + }, + "permissions.visibility.user": { + "defaultMessage": "Míreanna Cruthaithe Amháin" + }, + "proxy-host": { + "defaultMessage": "Óstach Seachfhreastalaí" + }, + "proxy-host.forward-host": { + "defaultMessage": "Ainm Óstach / IP Ar Aghaidh" + }, + "proxy-hosts": { + "defaultMessage": "Óstaigh Seachfhreastalaí" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Óstach Seachfhreastalaí} other {Óstaigh Seachfhreastalaí}}" + }, + "public": { + "defaultMessage": "Poiblí" + }, + "redirection-host": { + "defaultMessage": "Óstach Athsheolta" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Fearann Ar Aghaidh" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Cód HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Óstaigh Athsheolta" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Athsheoladh Óstach} other {Athsheoladh Óstaigh}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Rogha Ilghnéitheach" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Bogtha go buan" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Bogtha go sealadach" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Féach eile" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Atreorú sealadach" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Athsheoladh buan" + }, + "role.admin": { + "defaultMessage": "Riarthóir" + }, + "role.standard-user": { + "defaultMessage": "Úsáideoir Caighdeánach" + }, + "save": { + "defaultMessage": "Sábháil" + }, + "setting": { + "defaultMessage": "Socrú" + }, + "settings": { + "defaultMessage": "Socruithe" + }, + "settings.default-site": { + "defaultMessage": "Suíomh Réamhshocraithe" + }, + "settings.default-site.404": { + "defaultMessage": "Leathanach 404" + }, + "settings.default-site.444": { + "defaultMessage": "Gan Freagra (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Leathanach Comhghairdeas" + }, + "settings.default-site.description": { + "defaultMessage": "Cad atá le taispeáint nuair a bhuaileann óstach anaithnid Nginx" + }, + "settings.default-site.html": { + "defaultMessage": "HTML saincheaptha" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Atreorú" + }, + "setup.preamble": { + "defaultMessage": "Tosaigh trí do chuntas riarthóra a chruthú." + }, + "setup.title": { + "defaultMessage": "Fáilte!" + }, + "sign-in": { + "defaultMessage": "Sínigh isteach" + }, + "ssl-certificate": { + "defaultMessage": "Teastas SSL" + }, + "stream": { + "defaultMessage": "Sruth" + }, + "stream.forward-host": { + "defaultMessage": "Óstach Ar Aghaidh" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com nó 10.0.0.1 nó 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Port Isteach" + }, + "streams": { + "defaultMessage": "Sruthanna" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Sruth} other {Sruthanna}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Tástáil" + }, + "update-available": { + "defaultMessage": "Nuashonrú ar Fáil: {latestVersion}" + }, + "user": { + "defaultMessage": "Úsáideoir" + }, + "user.change-password": { + "defaultMessage": "Athraigh Pasfhocal" + }, + "user.confirm-password": { + "defaultMessage": "Deimhnigh Pasfhocal" + }, + "user.current-password": { + "defaultMessage": "Pasfhocal Reatha" + }, + "user.edit-profile": { + "defaultMessage": "Cuir Próifíl in Eagar" + }, + "user.full-name": { + "defaultMessage": "Ainm Iomlán" + }, + "user.login-as": { + "defaultMessage": "Sínigh isteach mar {name}" + }, + "user.logout": { + "defaultMessage": "Logáil Amach" + }, + "user.new-password": { + "defaultMessage": "Pasfhocal Nua" + }, + "user.nickname": { + "defaultMessage": "Leasainm" + }, + "user.set-password": { + "defaultMessage": "Socraigh Pasfhocal" + }, + "user.set-permissions": { + "defaultMessage": "Socraigh Ceadanna do {name}" + }, + "user.switch-dark": { + "defaultMessage": "Athraigh go Mód Dorcha" + }, + "user.switch-light": { + "defaultMessage": "Athraigh go mód Solais" + }, + "username": { + "defaultMessage": "Ainm úsáideora" + }, + "users": { + "defaultMessage": "Úsáideoirí" + } +} diff --git a/frontend/src/locale/src/hu.json b/frontend/src/locale/src/hu.json new file mode 100644 index 0000000..4caf058 --- /dev/null +++ b/frontend/src/locale/src/hu.json @@ -0,0 +1,770 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Hátralévő tartalék kódok: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Mentse el ezeket a tartalék kódokat biztonságos helyre. Minden kód csak egyszer használható." + }, + "2fa.disable": { + "defaultMessage": "Kétfaktoros hitelesítés letiltása" + }, + "2fa.disable-confirm": { + "defaultMessage": "2FA letiltása" + }, + "2fa.disable-warning": { + "defaultMessage": "A kétfaktoros hitelesítés letiltása kevésbé teszi biztonságossá a fiókját." + }, + "2fa.disabled": { + "defaultMessage": "Letiltva" + }, + "2fa.done": { + "defaultMessage": "Elmentettem a tartalék kódjaimat" + }, + "2fa.enable": { + "defaultMessage": "Kétfaktoros hitelesítés engedélyezése" + }, + "2fa.enabled": { + "defaultMessage": "Engedélyezve" + }, + "2fa.enter-code": { + "defaultMessage": "Adja meg az ellenőrző kódot" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Adja meg az ellenőrző kódot a letiltáshoz" + }, + "2fa.regenerate": { + "defaultMessage": "Újragenerálás" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Tartalék kódok újragenerálása" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Adjon meg egy ellenőrző kódot az új tartalék kódok generálásához. A régi kódok érvénytelenné válnak." + }, + "2fa.secret-key": { + "defaultMessage": "Titkos kulcs" + }, + "2fa.setup-instructions": { + "defaultMessage": "Olvassa be ezt a QR kódot a hitelesítő alkalmazásával, vagy adja meg a titkot manuálisan." + }, + "2fa.status": { + "defaultMessage": "Állapot" + }, + "2fa.title": { + "defaultMessage": "Kétfaktoros hitelesítés" + }, + "2fa.verify-enable": { + "defaultMessage": "Ellenőrzés és engedélyezés" + }, + "access-list": { + "defaultMessage": "Hozzáférési lista" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {szabály} other {szabály}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {felhasználó} other {felhasználó}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Ha legalább 1 szabály létezik, ez a mindent tiltó szabály utolsóként lesz hozzáadva" + }, + "access-list.help.rules-order": { + "defaultMessage": "Vegye figyelembe, hogy az engedélyező és tiltó direktívák a meghatározásuk sorrendjében lesznek alkalmazva." + }, + "access-list.pass-auth": { + "defaultMessage": "Hitelesítés továbbítása az upstream felé" + }, + "access-list.public": { + "defaultMessage": "Nyilvánosan elérhető" + }, + "access-list.public.subtitle": { + "defaultMessage": "Alapszintű hitelesítés nem szükséges" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 vagy 192.168.1.0/24 vagy 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Bármely teljesítése" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {felhasználó} other {felhasználó}}, {rules} {rules, plural, one {szabály} other {szabály}} - Létrehozva: {date}" + }, + "access-lists": { + "defaultMessage": "Hozzáférési listák" + }, + "action.add": { + "defaultMessage": "Hozzáadás" + }, + "action.add-location": { + "defaultMessage": "Útvonal hozzáadása" + }, + "action.allow": { + "defaultMessage": "Engedélyezés" + }, + "action.close": { + "defaultMessage": "Bezárás" + }, + "action.delete": { + "defaultMessage": "Törlés" + }, + "action.deny": { + "defaultMessage": "Tiltás" + }, + "action.disable": { + "defaultMessage": "Letiltás" + }, + "action.download": { + "defaultMessage": "Letöltés" + }, + "action.edit": { + "defaultMessage": "Szerkesztés" + }, + "action.enable": { + "defaultMessage": "Engedélyezés" + }, + "action.permissions": { + "defaultMessage": "Engedélyek" + }, + "action.renew": { + "defaultMessage": "Megújítás" + }, + "action.view-details": { + "defaultMessage": "Részletek megtekintése" + }, + "auditlogs": { + "defaultMessage": "Audit naplók" + }, + "auto": { + "defaultMessage": "Automatikus" + }, + "cancel": { + "defaultMessage": "Mégse" + }, + "certificate": { + "defaultMessage": "Tanúsítvány" + }, + "certificate.custom-certificate": { + "defaultMessage": "Tanúsítvány" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Tanúsítvány kulcs" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Köztes tanúsítvány" + }, + "certificate.in-use": { + "defaultMessage": "Használatban" + }, + "certificate.none.subtitle": { + "defaultMessage": "Nincs tanúsítvány hozzárendelve" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Ez a kiszolgáló nem fog HTTPS-t használni" + }, + "certificate.none.title": { + "defaultMessage": "Nincs" + }, + "certificate.not-in-use": { + "defaultMessage": "Nincs használatban" + }, + "certificate.renew": { + "defaultMessage": "Tanúsítvány megújítása" + }, + "certificates": { + "defaultMessage": "Tanúsítványok" + }, + "certificates.custom": { + "defaultMessage": "Egyéni tanúsítvány" + }, + "certificates.custom.warning": { + "defaultMessage": "Jelszóval védett kulcsfájlok nem támogatottak." + }, + "certificates.dns.credentials": { + "defaultMessage": "Hitelesítő fájl tartalma" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Ez a plugin egy konfigurációs fájlt igényel, amely API tokent vagy egyéb hitelesítő adatokat tartalmaz a szolgáltatóhoz" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Ezek az adatok sima szövegként lesznek tárolva az adatbázisban és egy fájlban!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagálási másodpercek" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Hagyja üresen a plugin alapértelmezett értékének használatához. Másodpercek száma a DNS propagálás megvárásához." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS szolgáltató" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Válasszon szolgáltatót..." + }, + "certificates.dns.warning": { + "defaultMessage": "Ez a szakasz némi ismeretet igényel a Certbot-ról és a DNS plugin-jeiről. Kérjük, olvassa el a megfelelő plugin dokumentációját." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Található szerver ezen a domain-en, de nem úgy tűnik, hogy Nginx Proxy Manager lenne. Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Az elérhetőség ellenőrzése sikertelen a site24x7.com kommunikációs hiba miatt." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Nincs elérhető szerver ezen a domain-en. Kérjük, győződjön meg róla, hogy a domain létezik és arra az IP címre mutat, ahol az NPM példánya fut, és szükség esetén a 80-as port továbbítva van a routerében." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "A szerver elérhető és a tanúsítványok létrehozása lehetséges lesz." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Található szerver ezen a domain-en, de váratlan {code} státuszkódot adott vissza. Ez az NPM szerver? Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Található szerver ezen a domain-en, de váratlan adatot adott vissza. Ez az NPM szerver? Kérjük, győződjön meg róla, hogy a domain arra az IP címre mutat, ahol az NPM példánya fut." + }, + "certificates.http.test-results": { + "defaultMessage": "Teszt eredmények" + }, + "certificates.http.warning": { + "defaultMessage": "Ezeknek a domain-eknek már konfigurálva kell lenniük, hogy erre a telepítésre mutassanak." + }, + "certificates.key-type": { + "defaultMessage": "Kulcs típus" + }, + "certificates.key-type-description": { + "defaultMessage": "Az RSA széles körben kompatibilis, az ECDSA gyorsabb és biztonságosabb, de nem biztos, hogy régebbi rendszerek támogatják" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "Let's Encrypt-tel" + }, + "certificates.request.title": { + "defaultMessage": "Új tanúsítvány kérelmezése" + }, + "column.access": { + "defaultMessage": "Hozzáférés" + }, + "column.authorization": { + "defaultMessage": "Jogosultság" + }, + "column.authorizations": { + "defaultMessage": "Jogosultságok" + }, + "column.custom-locations": { + "defaultMessage": "Egyéni útvonalak" + }, + "column.destination": { + "defaultMessage": "Cél" + }, + "column.details": { + "defaultMessage": "Részletek" + }, + "column.email": { + "defaultMessage": "E-mail" + }, + "column.event": { + "defaultMessage": "Esemény" + }, + "column.expires": { + "defaultMessage": "Lejár" + }, + "column.http-code": { + "defaultMessage": "HTTP kód" + }, + "column.incoming-port": { + "defaultMessage": "Bejövő port" + }, + "column.name": { + "defaultMessage": "Név" + }, + "column.protocol": { + "defaultMessage": "Protokoll" + }, + "column.provider": { + "defaultMessage": "Szolgáltató" + }, + "column.roles": { + "defaultMessage": "Szerepkörök" + }, + "column.rules": { + "defaultMessage": "Szabályok" + }, + "column.satisfy": { + "defaultMessage": "Teljesítés" + }, + "column.satisfy-all": { + "defaultMessage": "Összes" + }, + "column.satisfy-any": { + "defaultMessage": "Bármely" + }, + "column.scheme": { + "defaultMessage": "Séma" + }, + "column.source": { + "defaultMessage": "Forrás" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Állapot" + }, + "created-on": { + "defaultMessage": "Létrehozva: {date}" + }, + "dashboard": { + "defaultMessage": "Vezérlőpult" + }, + "dead-host": { + "defaultMessage": "404-es Kiszolgáló" + }, + "dead-hosts": { + "defaultMessage": "404-es Kiszolgálók" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404-es Kiszolgáló} other {404-es Kiszolgálók}}" + }, + "disabled": { + "defaultMessage": "Letiltva" + }, + "domain-names": { + "defaultMessage": "Domain nevek" + }, + "domain-names.max": { + "defaultMessage": "Maximum {count} domain név" + }, + "domain-names.placeholder": { + "defaultMessage": "Kezdjen el gépelni domain hozzáadásához..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Helyettesítő karakterek nem engedélyezettek ennél a típusnál" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Helyettesítő karakterek nem támogatottak ennél a CA-nál" + }, + "domains.force-ssl": { + "defaultMessage": "SSL kényszerítése" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS engedélyezve" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS aldomain-ek" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 támogatás" + }, + "domains.use-dns": { + "defaultMessage": "DNS Challenge használata" + }, + "email-address": { + "defaultMessage": "E-mail cím" + }, + "empty-search": { + "defaultMessage": "Nincs találat" + }, + "empty-subtitle": { + "defaultMessage": "Miért nem hoz létre egyet?" + }, + "enabled": { + "defaultMessage": "Engedélyezve" + }, + "error.access.at-least-one": { + "defaultMessage": "Legalább egy jogosultság vagy egy hozzáférési szabály szükséges" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "A jogosultsági felhasználóneveknek egyedieknek kell lenniük" + }, + "error.invalid-auth": { + "defaultMessage": "Érvénytelen e-mail vagy jelszó" + }, + "error.invalid-domain": { + "defaultMessage": "Érvénytelen domain: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Érvénytelen e-mail cím" + }, + "error.max-character-length": { + "defaultMessage": "Maximális hossz {max} karakter" + }, + "error.max-domains": { + "defaultMessage": "Túl sok domain, a maximum {max}" + }, + "error.maximum": { + "defaultMessage": "A maximum {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimális hossz {min} karakter" + }, + "error.minimum": { + "defaultMessage": "A minimum {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "A jelszavaknak egyezniük kell" + }, + "error.required": { + "defaultMessage": "Ez kötelező" + }, + "expires.on": { + "defaultMessage": "Lejár: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork-olj a GitHub-on" + }, + "host.flags.block-exploits": { + "defaultMessage": "Gyakori exploitok blokkolása" + }, + "host.flags.cache-assets": { + "defaultMessage": "Erőforrások gyorsítótárazása" + }, + "host.flags.preserve-path": { + "defaultMessage": "Útvonal megőrzése" + }, + "host.flags.protocols": { + "defaultMessage": "Protokollok" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets támogatás" + }, + "host.forward-port": { + "defaultMessage": "Továbbító port" + }, + "host.forward-scheme": { + "defaultMessage": "Séma" + }, + "hosts": { + "defaultMessage": "Kiszolgálók" + }, + "http-only": { + "defaultMessage": "Csak HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt DNS-en keresztül" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt HTTP-n keresztül" + }, + "loading": { + "defaultMessage": "Betöltés…" + }, + "login.2fa-code": { + "defaultMessage": "Ellenőrző kód" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Adja meg a kódot" + }, + "login.2fa-description": { + "defaultMessage": "Adja meg a kódot a hitelesítő alkalmazásából" + }, + "login.2fa-title": { + "defaultMessage": "Kétfaktoros hitelesítés" + }, + "login.2fa-verify": { + "defaultMessage": "Ellenőrzés" + }, + "login.title": { + "defaultMessage": "Jelentkezzen be a fiókjába" + }, + "nginx-config.label": { + "defaultMessage": "Egyéni Nginx konfiguráció" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Adja meg az egyéni Nginx konfigurációját itt, saját felelősségére!" + }, + "no-permission-error": { + "defaultMessage": "Nincs jogosultsága ennek megtekintéséhez." + }, + "notfound.action": { + "defaultMessage": "Vigyen haza" + }, + "notfound.content": { + "defaultMessage": "Sajnáljuk, de a keresett oldal nem található" + }, + "notfound.title": { + "defaultMessage": "Hoppá… Hibás oldalra talált" + }, + "notification.error": { + "defaultMessage": "Hiba" + }, + "notification.object-deleted": { + "defaultMessage": "{object} törölve lett" + }, + "notification.object-disabled": { + "defaultMessage": "{object} letiltva lett" + }, + "notification.object-enabled": { + "defaultMessage": "{object} engedélyezve lett" + }, + "notification.object-renewed": { + "defaultMessage": "{object} megújítva lett" + }, + "notification.object-saved": { + "defaultMessage": "{object} mentve lett" + }, + "notification.success": { + "defaultMessage": "Sikeres" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "{object} hozzáadása" + }, + "object.delete": { + "defaultMessage": "{object} törlése" + }, + "object.delete.content": { + "defaultMessage": "Biztosan törölni szeretné ezt: {object}?" + }, + "object.edit": { + "defaultMessage": "{object} szerkesztése" + }, + "object.empty": { + "defaultMessage": "Nincsenek {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} létrehozva" + }, + "object.event.deleted": { + "defaultMessage": "{object} törölve" + }, + "object.event.disabled": { + "defaultMessage": "{object} letiltva" + }, + "object.event.enabled": { + "defaultMessage": "{object} engedélyezve" + }, + "object.event.renewed": { + "defaultMessage": "{object} megújítva" + }, + "object.event.updated": { + "defaultMessage": "{object} frissítve" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Beállítások" + }, + "password": { + "defaultMessage": "Jelszó" + }, + "password.generate": { + "defaultMessage": "Véletlenszerű jelszó generálása" + }, + "password.hide": { + "defaultMessage": "Jelszó elrejtése" + }, + "password.show": { + "defaultMessage": "Jelszó megjelenítése" + }, + "permissions.hidden": { + "defaultMessage": "Rejtett" + }, + "permissions.manage": { + "defaultMessage": "Kezelés" + }, + "permissions.view": { + "defaultMessage": "Csak megtekintés" + }, + "permissions.visibility.all": { + "defaultMessage": "Összes elem" + }, + "permissions.visibility.title": { + "defaultMessage": "Elemek láthatósága" + }, + "permissions.visibility.user": { + "defaultMessage": "Csak létrehozott elemek" + }, + "proxy-host": { + "defaultMessage": "Proxy Kiszolgáló" + }, + "proxy-host.forward-host": { + "defaultMessage": "Továbbító hostnév / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Kiszolgálók" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Kiszolgáló} other {Proxy Kiszolgálók}}" + }, + "public": { + "defaultMessage": "Nyilvános" + }, + "redirection-host": { + "defaultMessage": "Átirányító Kiszolgáló" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Továbbító domain" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP kód" + }, + "redirection-hosts": { + "defaultMessage": "Átirányító Kiszolgálók" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Átirányító Kiszolgáló} other {Átirányító Kiszolgálók}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Többszörös választás" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Véglegesen áthelyezve" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Ideiglenesen áthelyezve" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Lásd másik" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Ideiglenes átirányítás" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Végleges átirányítás" + }, + "role.admin": { + "defaultMessage": "Adminisztrátor" + }, + "role.standard-user": { + "defaultMessage": "Általános felhasználó" + }, + "save": { + "defaultMessage": "Mentés" + }, + "setting": { + "defaultMessage": "Beállítás" + }, + "settings": { + "defaultMessage": "Beállítások" + }, + "settings.default-site": { + "defaultMessage": "Alapértelmezett oldal" + }, + "settings.default-site.404": { + "defaultMessage": "404-es oldal" + }, + "settings.default-site.444": { + "defaultMessage": "Nincs válasz (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Gratulálunk oldal" + }, + "settings.default-site.description": { + "defaultMessage": "Mit mutasson az Nginx ismeretlen Kiszolgáló esetén" + }, + "settings.default-site.html": { + "defaultMessage": "Egyéni HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Átirányítás" + }, + "setup.preamble": { + "defaultMessage": "Kezdje az admin fiók létrehozásával." + }, + "setup.title": { + "defaultMessage": "Üdvözöljük!" + }, + "sign-in": { + "defaultMessage": "Bejelentkezés" + }, + "ssl-certificate": { + "defaultMessage": "SSL tanúsítvány" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Továbbító kiszolgáló" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com vagy 10.0.0.1 vagy 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Bejövő port" + }, + "streams": { + "defaultMessage": "Streamek" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Teszt" + }, + "update-available": { + "defaultMessage": "Frissítés elérhető: {latestVersion}" + }, + "user": { + "defaultMessage": "Felhasználó" + }, + "user.change-password": { + "defaultMessage": "Jelszó megváltoztatása" + }, + "user.confirm-password": { + "defaultMessage": "Jelszó megerősítése" + }, + "user.current-password": { + "defaultMessage": "Jelenlegi jelszó" + }, + "user.edit-profile": { + "defaultMessage": "Profil szerkesztése" + }, + "user.full-name": { + "defaultMessage": "Teljes név" + }, + "user.login-as": { + "defaultMessage": "Bejelentkezés mint {name}" + }, + "user.logout": { + "defaultMessage": "Kijelentkezés" + }, + "user.new-password": { + "defaultMessage": "Új jelszó" + }, + "user.nickname": { + "defaultMessage": "Becenév" + }, + "user.set-password": { + "defaultMessage": "Jelszó beállítása" + }, + "user.set-permissions": { + "defaultMessage": "Engedélyek beállítása {name} számára" + }, + "user.switch-dark": { + "defaultMessage": "Váltás sötét módra" + }, + "user.switch-light": { + "defaultMessage": "Váltás világos módra" + }, + "user.two-factor": { + "defaultMessage": "Kétfaktoros hitelesítés" + }, + "username": { + "defaultMessage": "Felhasználónév" + }, + "users": { + "defaultMessage": "Felhasználók" + } +} diff --git a/frontend/src/locale/src/id.json b/frontend/src/locale/src/id.json new file mode 100644 index 0000000..cb498f0 --- /dev/null +++ b/frontend/src/locale/src/id.json @@ -0,0 +1,683 @@ +{ + "access-list": { + "defaultMessage": "Daftar Akses" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Aturan} other {Aturan}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Pengguna} other {Pengguna}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Jika setidaknya 1 aturan ada, aturan tolak semua ini akan ditambahkan paling akhir" + }, + "access-list.help.rules-order": { + "defaultMessage": "Perhatikan bahwa direktif izinkan dan tolak akan diterapkan sesuai urutan yang didefinisikan." + }, + "access-list.pass-auth": { + "defaultMessage": "Teruskan Auth ke Upstream" + }, + "access-list.public": { + "defaultMessage": "Dapat Diakses Publik" + }, + "access-list.public.subtitle": { + "defaultMessage": "Tidak perlu basic auth" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 atau 192.168.1.0/24 atau 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Penuhi Salah Satu" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Pengguna} other {Pengguna}}, {rules} {rules, plural, one {Aturan} other {Aturan}} - Dibuat: {date}" + }, + "access-lists": { + "defaultMessage": "Daftar Akses" + }, + "action.add": { + "defaultMessage": "Tambah" + }, + "action.add-location": { + "defaultMessage": "Tambah Lokasi" + }, + "action.allow": { + "defaultMessage": "Izinkan" + }, + "action.close": { + "defaultMessage": "Tutup" + }, + "action.delete": { + "defaultMessage": "Hapus" + }, + "action.deny": { + "defaultMessage": "Tolak" + }, + "action.disable": { + "defaultMessage": "Nonaktifkan" + }, + "action.download": { + "defaultMessage": "Unduh" + }, + "action.edit": { + "defaultMessage": "Edit" + }, + "action.enable": { + "defaultMessage": "Aktifkan" + }, + "action.permissions": { + "defaultMessage": "Izin" + }, + "action.renew": { + "defaultMessage": "Perpanjang" + }, + "action.view-details": { + "defaultMessage": "Lihat Detail" + }, + "auditlogs": { + "defaultMessage": "Log Audit" + }, + "auto": { + "defaultMessage": "Otomatis" + }, + "cancel": { + "defaultMessage": "Batal" + }, + "certificate": { + "defaultMessage": "Sertifikat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Sertifikat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Kunci Sertifikat" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Sertifikat Intermediate" + }, + "certificate.in-use": { + "defaultMessage": "Digunakan" + }, + "certificate.none.subtitle": { + "defaultMessage": "Tidak ada sertifikat yang ditetapkan" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Host ini tidak akan menggunakan HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Tidak Ada" + }, + "certificate.not-in-use": { + "defaultMessage": "Tidak Digunakan" + }, + "certificate.renew": { + "defaultMessage": "Perpanjang Sertifikat" + }, + "certificates": { + "defaultMessage": "Sertifikat" + }, + "certificates.custom": { + "defaultMessage": "Sertifikat Kustom" + }, + "certificates.custom.warning": { + "defaultMessage": "Berkas kunci yang dilindungi frasa sandi tidak didukung." + }, + "certificates.dns.credentials": { + "defaultMessage": "Konten File Kredensial" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Plugin ini memerlukan file konfigurasi yang berisi token API atau kredensial lain untuk penyedia Anda" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Data ini akan disimpan sebagai teks biasa di database dan dalam file!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Detik Propagasi" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Biarkan kosong untuk menggunakan nilai baku plugin. Jumlah detik menunggu propagasi DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Penyedia DNS" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Pilih Penyedia..." + }, + "certificates.dns.warning": { + "defaultMessage": "Bagian ini memerlukan pengetahuan tentang Certbot dan plugin DNS-nya. Silakan merujuk dokumentasi plugin terkait." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Ada server yang ditemukan pada domain ini tetapi tampaknya bukan Nginx Proxy Manager. Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Gagal memeriksa keterjangkauan karena kesalahan komunikasi dengan site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Tidak ada server yang tersedia pada domain ini. Pastikan domain Anda ada dan mengarah ke IP tempat instance NPM berjalan dan bila perlu port 80 diteruskan di router Anda." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Server Anda dapat dijangkau dan pembuatan sertifikat seharusnya memungkinkan." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Ada server yang ditemukan pada domain ini tetapi mengembalikan kode status tak terduga {code}. Apakah itu server NPM? Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Ada server yang ditemukan pada domain ini tetapi mengembalikan data yang tidak terduga. Apakah itu server NPM? Pastikan domain Anda mengarah ke IP tempat instance NPM berjalan." + }, + "certificates.http.test-results": { + "defaultMessage": "Hasil Uji" + }, + "certificates.http.warning": { + "defaultMessage": "Domain ini harus sudah dikonfigurasi agar mengarah ke instalasi ini." + }, + "certificates.request.subtitle": { + "defaultMessage": "dengan Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Minta Sertifikat Baru" + }, + "column.access": { + "defaultMessage": "Akses" + }, + "column.authorization": { + "defaultMessage": "Otorisasi" + }, + "column.authorizations": { + "defaultMessage": "Otorisasi" + }, + "column.custom-locations": { + "defaultMessage": "Lokasi Kustom" + }, + "column.destination": { + "defaultMessage": "Tujuan" + }, + "column.details": { + "defaultMessage": "Detail" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Peristiwa" + }, + "column.expires": { + "defaultMessage": "Kedaluwarsa" + }, + "column.http-code": { + "defaultMessage": "Kode HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Port Masuk" + }, + "column.name": { + "defaultMessage": "Nama" + }, + "column.protocol": { + "defaultMessage": "Protokol" + }, + "column.provider": { + "defaultMessage": "Penyedia" + }, + "column.roles": { + "defaultMessage": "Peran" + }, + "column.rules": { + "defaultMessage": "Aturan" + }, + "column.satisfy": { + "defaultMessage": "Pemenuhan" + }, + "column.satisfy-all": { + "defaultMessage": "Semua" + }, + "column.satisfy-any": { + "defaultMessage": "Salah Satu" + }, + "column.scheme": { + "defaultMessage": "Skema" + }, + "column.source": { + "defaultMessage": "Sumber" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Dibuat: {date}" + }, + "dashboard": { + "defaultMessage": "Dasbor" + }, + "dead-host": { + "defaultMessage": "Host 404" + }, + "dead-hosts": { + "defaultMessage": "Host 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host 404} other {Host 404}}" + }, + "disabled": { + "defaultMessage": "Nonaktif" + }, + "domain-names": { + "defaultMessage": "Nama Domain" + }, + "domain-names.max": { + "defaultMessage": "Maksimum {count} nama domain" + }, + "domain-names.placeholder": { + "defaultMessage": "Mulai mengetik untuk menambahkan domain..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcard tidak diizinkan untuk tipe ini" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcard tidak didukung untuk CA ini" + }, + "domains.force-ssl": { + "defaultMessage": "Paksa SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Diaktifkan" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Subdomain" + }, + "domains.http2-support": { + "defaultMessage": "Dukungan HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Gunakan DNS Challenge" + }, + "email-address": { + "defaultMessage": "Alamat email" + }, + "empty-search": { + "defaultMessage": "Tidak ada hasil" + }, + "empty-subtitle": { + "defaultMessage": "Mengapa tidak membuatnya?" + }, + "enabled": { + "defaultMessage": "Aktif" + }, + "error.access.at-least-one": { + "defaultMessage": "Setidaknya satu Otorisasi atau satu Aturan Akses diperlukan" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Nama pengguna otorisasi harus unik" + }, + "error.invalid-auth": { + "defaultMessage": "Email atau kata sandi tidak valid" + }, + "error.invalid-domain": { + "defaultMessage": "Domain tidak valid: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Alamat email tidak valid" + }, + "error.max-character-length": { + "defaultMessage": "Panjang maksimum adalah {max} karakter{max, plural, one {} other {}}" + }, + "error.max-domains": { + "defaultMessage": "Terlalu banyak domain, maksimum {max}" + }, + "error.maximum": { + "defaultMessage": "Maksimum adalah {max}" + }, + "error.min-character-length": { + "defaultMessage": "Panjang minimum adalah {min} karakter{min, plural, one {} other {}}" + }, + "error.minimum": { + "defaultMessage": "Minimum adalah {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Kata sandi harus cocok" + }, + "error.required": { + "defaultMessage": "Ini wajib diisi" + }, + "expires.on": { + "defaultMessage": "Kedaluwarsa: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork saya di GitHub" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blokir Eksploit Umum" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache Aset" + }, + "host.flags.preserve-path": { + "defaultMessage": "Pertahankan Path" + }, + "host.flags.protocols": { + "defaultMessage": "Protokol" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Dukungan Websocket" + }, + "host.forward-port": { + "defaultMessage": "Port Terusan" + }, + "host.forward-scheme": { + "defaultMessage": "Skema" + }, + "hosts": { + "defaultMessage": "Host" + }, + "http-only": { + "defaultMessage": "HTTP Saja" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Memuat…" + }, + "login.title": { + "defaultMessage": "Masuk ke akun Anda" + }, + "nginx-config.label": { + "defaultMessage": "Konfigurasi Nginx Kustom" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Masukkan konfigurasi Nginx kustom Anda di sini dengan risiko Anda sendiri!" + }, + "no-permission-error": { + "defaultMessage": "Anda tidak memiliki akses untuk melihat ini." + }, + "notfound.action": { + "defaultMessage": "Bawa saya pulang" + }, + "notfound.content": { + "defaultMessage": "Maaf, halaman yang Anda cari tidak ditemukan" + }, + "notfound.title": { + "defaultMessage": "Ups… Anda baru saja menemukan halaman error" + }, + "notification.error": { + "defaultMessage": "Kesalahan" + }, + "notification.object-deleted": { + "defaultMessage": "{object} telah dihapus" + }, + "notification.object-disabled": { + "defaultMessage": "{object} telah dinonaktifkan" + }, + "notification.object-enabled": { + "defaultMessage": "{object} telah diaktifkan" + }, + "notification.object-renewed": { + "defaultMessage": "{object} telah diperpanjang" + }, + "notification.object-saved": { + "defaultMessage": "{object} telah disimpan" + }, + "notification.success": { + "defaultMessage": "Berhasil" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Tambah {object}" + }, + "object.delete": { + "defaultMessage": "Hapus {object}" + }, + "object.delete.content": { + "defaultMessage": "Apakah Anda yakin ingin menghapus {object} ini?" + }, + "object.edit": { + "defaultMessage": "Edit {object}" + }, + "object.empty": { + "defaultMessage": "Tidak ada {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} dibuat" + }, + "object.event.deleted": { + "defaultMessage": "{object} dihapus" + }, + "object.event.disabled": { + "defaultMessage": "{object} dinonaktifkan" + }, + "object.event.enabled": { + "defaultMessage": "{object} diaktifkan" + }, + "object.event.renewed": { + "defaultMessage": "{object} diperpanjang" + }, + "object.event.updated": { + "defaultMessage": "{object} diperbarui" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Opsi" + }, + "password": { + "defaultMessage": "Kata sandi" + }, + "password.generate": { + "defaultMessage": "Buat kata sandi acak" + }, + "password.hide": { + "defaultMessage": "Sembunyikan Kata Sandi" + }, + "password.show": { + "defaultMessage": "Tampilkan Kata Sandi" + }, + "permissions.hidden": { + "defaultMessage": "Tersembunyi" + }, + "permissions.manage": { + "defaultMessage": "Kelola" + }, + "permissions.view": { + "defaultMessage": "Hanya Lihat" + }, + "permissions.visibility.all": { + "defaultMessage": "Semua Item" + }, + "permissions.visibility.title": { + "defaultMessage": "Visibilitas Item" + }, + "permissions.visibility.user": { + "defaultMessage": "Hanya Item yang Dibuat" + }, + "proxy-host": { + "defaultMessage": "Host Proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Hostname / IP Terusan" + }, + "proxy-hosts": { + "defaultMessage": "Host Proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host Proxy} other {Host Proxy}}" + }, + "public": { + "defaultMessage": "Publik" + }, + "redirection-host": { + "defaultMessage": "Host Pengalihan" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Domain Terusan" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Kode HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Host Pengalihan" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host Pengalihan} other {Host Pengalihan}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Banyak Pilihan" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Pindah permanen" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Pindah sementara" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Lihat lainnya" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Pengalihan sementara" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Pengalihan permanen" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Pengguna Standar" + }, + "save": { + "defaultMessage": "Simpan" + }, + "setting": { + "defaultMessage": "Pengaturan" + }, + "settings": { + "defaultMessage": "Pengaturan" + }, + "settings.default-site": { + "defaultMessage": "Situs Default" + }, + "settings.default-site.404": { + "defaultMessage": "Halaman 404" + }, + "settings.default-site.444": { + "defaultMessage": "Tidak Ada Respons (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Halaman Ucapan Selamat" + }, + "settings.default-site.description": { + "defaultMessage": "Apa yang ditampilkan saat Nginx diakses dengan Host yang tidak dikenal" + }, + "settings.default-site.html": { + "defaultMessage": "HTML Kustom" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Alihkan" + }, + "setup.preamble": { + "defaultMessage": "Mulai dengan membuat akun admin Anda." + }, + "setup.title": { + "defaultMessage": "Selamat datang!" + }, + "sign-in": { + "defaultMessage": "Masuk" + }, + "ssl-certificate": { + "defaultMessage": "Sertifikat SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Host Terusan" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com atau 10.0.0.1 atau 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Port Masuk" + }, + "streams": { + "defaultMessage": "Stream" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Uji" + }, + "update-available": { + "defaultMessage": "Pembaruan Tersedia: {latestVersion}" + }, + "user": { + "defaultMessage": "Pengguna" + }, + "user.change-password": { + "defaultMessage": "Ubah Kata Sandi" + }, + "user.confirm-password": { + "defaultMessage": "Konfirmasi Kata Sandi" + }, + "user.current-password": { + "defaultMessage": "Kata Sandi Saat Ini" + }, + "user.edit-profile": { + "defaultMessage": "Edit Profil" + }, + "user.full-name": { + "defaultMessage": "Nama Lengkap" + }, + "user.login-as": { + "defaultMessage": "Masuk sebagai {name}" + }, + "user.logout": { + "defaultMessage": "Keluar" + }, + "user.new-password": { + "defaultMessage": "Kata Sandi Baru" + }, + "user.nickname": { + "defaultMessage": "Nama Panggilan" + }, + "user.set-password": { + "defaultMessage": "Atur Kata Sandi" + }, + "user.set-permissions": { + "defaultMessage": "Atur Izin untuk {name}" + }, + "user.switch-dark": { + "defaultMessage": "Beralih ke mode gelap" + }, + "user.switch-light": { + "defaultMessage": "Beralih ke mode terang" + }, + "username": { + "defaultMessage": "Nama pengguna" + }, + "users": { + "defaultMessage": "Pengguna" + } +} diff --git a/frontend/src/locale/src/it.json b/frontend/src/locale/src/it.json new file mode 100644 index 0000000..7e5ca77 --- /dev/null +++ b/frontend/src/locale/src/it.json @@ -0,0 +1,659 @@ +{ + "access-list": { + "defaultMessage": "Lista di Accesso" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Regola} other {Regole}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Utente} other {Utenti}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Quando esiste almeno 1 regola, questa regola di negazione verrà aggiunta per ultima" + }, + "access-list.help.rules-order": { + "defaultMessage": "Nota che le direttive di allow e deny saranno applicate nell'ordine in cui sono definite." + }, + "access-list.pass-auth": { + "defaultMessage": "Passa Autenticazione all'Upstream" + }, + "access-list.public": { + "defaultMessage": "Accessibile Pubblicamente" + }, + "access-list.public.subtitle": { + "defaultMessage": "Nessuna autenticazione base richiesta" + }, + "access-list.satisfy-any": { + "defaultMessage": "Soddisfa Qualsiasi" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Utente} other {Utenti}}, {rules} {rules, plural, one {Regola} other {Regole}} - Creato: {date}" + }, + "access-lists": { + "defaultMessage": "Liste di Accesso" + }, + "action.add": { + "defaultMessage": "Aggiungi" + }, + "action.add-location": { + "defaultMessage": "Aggiungi Percorso" + }, + "action.close": { + "defaultMessage": "Chiudi" + }, + "action.delete": { + "defaultMessage": "Elimina" + }, + "action.disable": { + "defaultMessage": "Disabilita" + }, + "action.download": { + "defaultMessage": "Scarica" + }, + "action.edit": { + "defaultMessage": "Modifica" + }, + "action.enable": { + "defaultMessage": "Abilita" + }, + "action.permissions": { + "defaultMessage": "Permessi" + }, + "action.renew": { + "defaultMessage": "Rinnova" + }, + "action.view-details": { + "defaultMessage": "Visualizza Dettagli" + }, + "auditlogs": { + "defaultMessage": "Log di Audit" + }, + "cancel": { + "defaultMessage": "Annulla" + }, + "certificate": { + "defaultMessage": "Certificato" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificato" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Chiave del Certificato" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certificato Intermedio" + }, + "certificate.in-use": { + "defaultMessage": "In Uso" + }, + "certificate.none.subtitle": { + "defaultMessage": "Nessun certificato assegnato" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Questo host non utilizzerà HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Nessuno" + }, + "certificate.not-in-use": { + "defaultMessage": "Non in Uso" + }, + "certificate.renew": { + "defaultMessage": "Rinnova Certificato" + }, + "certificates": { + "defaultMessage": "Certificati" + }, + "certificates.custom": { + "defaultMessage": "Certificato Personalizzato" + }, + "certificates.custom.warning": { + "defaultMessage": "I file di chiave protetti da passphrase non sono supportati." + }, + "certificates.dns.credentials": { + "defaultMessage": "Contenuto File Credenziali" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Questo plugin richiede un file di configurazione contenente un token API o altre credenziali per il tuo provider" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Questi dati saranno memorizzati in chiaro nel database e in un file!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Secondi di Propagazione" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Lascia vuoto per usare il valore predefinito del plugin. Numero di secondi da attendere per la propagazione DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Provider DNS" + }, + "certificates.dns.warning": { + "defaultMessage": "Questa sezione richiede conoscenze su Certbot e i relativi plugin DNS. Consulta la documentazione del plugin." + }, + "certificates.http.reachability-404": { + "defaultMessage": "È stato trovato un server su questo dominio, ma non sembra essere Nginx Proxy Manager. Assicurati che il dominio punti all'IP dove è in esecuzione NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Verifica di raggiungibilità fallita per errore di comunicazione con site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Nessun server disponibile su questo dominio. Assicurati che il dominio esista e punti all'IP corretto e che la porta 80 sia inoltrata." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Il server è raggiungibile e la creazione dei certificati è possibile." + }, + "certificates.http.reachability-other": { + "defaultMessage": "È stato trovato un server su questo dominio ma ha restituito un codice di stato imprevisto {code}. È il server NPM? Controlla che il dominio punti correttamente all'IP." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "È stato trovato un server su questo dominio ma ha restituito dati imprevisti. È il server NPM? Controlla che il dominio punti correttamente all'IP." + }, + "certificates.http.test-results": { + "defaultMessage": "Risultati Test" + }, + "certificates.http.warning": { + "defaultMessage": "Questi domini devono già essere configurati per puntare a questa installazione." + }, + "certificates.key-type": { + "defaultMessage": "Tipo di Chiave" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA è ampiamente compatibile, ECDSA è più veloce e sicuro ma potrebbe non essere supportato da sistemi più vecchi" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "con Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Richiedi un nuovo Certificato" + }, + "column.access": { + "defaultMessage": "Accesso" + }, + "column.authorization": { + "defaultMessage": "Autorizzazione" + }, + "column.authorizations": { + "defaultMessage": "Autorizzazioni" + }, + "column.custom-locations": { + "defaultMessage": "Percorsi Personalizzati" + }, + "column.destination": { + "defaultMessage": "Destinazione" + }, + "column.details": { + "defaultMessage": "Dettagli" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Evento" + }, + "column.expires": { + "defaultMessage": "Scadenza" + }, + "column.http-code": { + "defaultMessage": "Codice HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Porta in Ingresso" + }, + "column.name": { + "defaultMessage": "Nome" + }, + "column.protocol": { + "defaultMessage": "Protocollo" + }, + "column.provider": { + "defaultMessage": "Provider" + }, + "column.roles": { + "defaultMessage": "Ruoli" + }, + "column.rules": { + "defaultMessage": "Regole" + }, + "column.satisfy": { + "defaultMessage": "Condizione" + }, + "column.satisfy-all": { + "defaultMessage": "Tutte" + }, + "column.satisfy-any": { + "defaultMessage": "Qualsiasi" + }, + "column.scheme": { + "defaultMessage": "Schema" + }, + "column.source": { + "defaultMessage": "Origine" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Stato" + }, + "created-on": { + "defaultMessage": "Creato: {date}" + }, + "dashboard": { + "defaultMessage": "Dashboard" + }, + "dead-host": { + "defaultMessage": "Host 404" + }, + "dead-hosts": { + "defaultMessage": "Hosts 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host 404} other {Hosts 404}}" + }, + "disabled": { + "defaultMessage": "Disabilitato" + }, + "domain-names": { + "defaultMessage": "Nomi di Dominio" + }, + "domain-names.max": { + "defaultMessage": "Massimo {count} nomi di dominio" + }, + "domain-names.placeholder": { + "defaultMessage": "Inizia a digitare per aggiungere un dominio..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcard non consentite per questo tipo" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcard non supportate per questa CA" + }, + "domains.force-ssl": { + "defaultMessage": "Forza SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Abilitato" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Sottodomini HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Supporto HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Usa Challenge DNS" + }, + "email-address": { + "defaultMessage": "Indirizzo Email" + }, + "empty-search": { + "defaultMessage": "Nessun risultato trovato" + }, + "empty-subtitle": { + "defaultMessage": "Perché non ne crei uno?" + }, + "enabled": { + "defaultMessage": "Abilitato" + }, + "error.access.at-least-one": { + "defaultMessage": "È richiesta almeno un'Autorizzazione o una Regola di Accesso" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "I nomi utente devono essere unici" + }, + "error.invalid-auth": { + "defaultMessage": "Email o password non validi" + }, + "error.invalid-domain": { + "defaultMessage": "Dominio non valido: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Indirizzo email non valido" + }, + "error.max-character-length": { + "defaultMessage": "Lunghezza massima {max} caratter{max, plural, one {e} other {i}}" + }, + "error.max-domains": { + "defaultMessage": "Troppi domini, massimo {max}" + }, + "error.maximum": { + "defaultMessage": "Massimo {max}" + }, + "error.min-character-length": { + "defaultMessage": "Lunghezza minima {min} caratter{min, plural, one {e} other {i}}" + }, + "error.minimum": { + "defaultMessage": "Minimo {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Le password devono coincidere" + }, + "error.required": { + "defaultMessage": "Campo obbligatorio" + }, + "expires.on": { + "defaultMessage": "Scade: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forkami su GitHub" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blocca Exploit Comuni" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache degli Asset" + }, + "host.flags.preserve-path": { + "defaultMessage": "Preserva Percorso" + }, + "host.flags.protocols": { + "defaultMessage": "Protocolli" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Supporto WebSockets" + }, + "host.forward-port": { + "defaultMessage": "Porta di Destinazione" + }, + "host.forward-scheme": { + "defaultMessage": "Schema" + }, + "hosts": { + "defaultMessage": "Host" + }, + "http-only": { + "defaultMessage": "Solo HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Caricamento…" + }, + "login.title": { + "defaultMessage": "Accedi al tuo account" + }, + "nginx-config.label": { + "defaultMessage": "Configurazione Nginx Personalizzata" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Inserisci qui la configurazione Nginx personalizzata a tuo rischio!" + }, + "no-permission-error": { + "defaultMessage": "Non hai accesso per visualizzare questa pagina." + }, + "notfound.action": { + "defaultMessage": "Torna alla Home" + }, + "notfound.content": { + "defaultMessage": "Spiacenti, la pagina richiesta non è stata trovata" + }, + "notfound.title": { + "defaultMessage": "Oops… Hai trovato una pagina di errore" + }, + "notification.error": { + "defaultMessage": "Errore" + }, + "notification.object-deleted": { + "defaultMessage": "{object} è stato eliminato" + }, + "notification.object-disabled": { + "defaultMessage": "{object} è stato disabilitato" + }, + "notification.object-enabled": { + "defaultMessage": "{object} è stato abilitato" + }, + "notification.object-renewed": { + "defaultMessage": "{object} è stato rinnovato" + }, + "notification.object-saved": { + "defaultMessage": "{object} è stato salvato" + }, + "notification.success": { + "defaultMessage": "Successo" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Aggiungi {object}" + }, + "object.delete": { + "defaultMessage": "Elimina {object}" + }, + "object.delete.content": { + "defaultMessage": "Sei sicuro di voler eliminare questo {object}?" + }, + "object.edit": { + "defaultMessage": "Modifica {object}" + }, + "object.empty": { + "defaultMessage": "Non ci sono {objects} presenti" + }, + "object.event.created": { + "defaultMessage": "{object} creato" + }, + "object.event.deleted": { + "defaultMessage": "{object} eliminato" + }, + "object.event.disabled": { + "defaultMessage": "{object} disabilitato" + }, + "object.event.enabled": { + "defaultMessage": "{object} abilitato" + }, + "object.event.renewed": { + "defaultMessage": "{object} rinnovato" + }, + "object.event.updated": { + "defaultMessage": "{object} aggiornato" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Opzioni" + }, + "password": { + "defaultMessage": "Password" + }, + "password.generate": { + "defaultMessage": "Genera password casuale" + }, + "password.hide": { + "defaultMessage": "Nascondi Password" + }, + "password.show": { + "defaultMessage": "Mostra Password" + }, + "permissions.hidden": { + "defaultMessage": "Nascosto" + }, + "permissions.manage": { + "defaultMessage": "Gestisci" + }, + "permissions.view": { + "defaultMessage": "Sola Lettura" + }, + "permissions.visibility.all": { + "defaultMessage": "Tutti gli Elementi" + }, + "permissions.visibility.title": { + "defaultMessage": "Visibilità Elementi" + }, + "permissions.visibility.user": { + "defaultMessage": "Solo Elementi Creati" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Hostname / IP di Destinazione" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Hosts" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host Proxy} other {Host Proxy}}" + }, + "public": { + "defaultMessage": "Pubblico" + }, + "redirection-host": { + "defaultMessage": "Host di Reindirizzamento" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Dominio di Destinazione" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Codice HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Host di Reindirizzamento" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host di Reindirizzamento} other {Host di Reindirizzamento}}" + }, + "role.admin": { + "defaultMessage": "Amministratore" + }, + "role.standard-user": { + "defaultMessage": "Utente Standard" + }, + "save": { + "defaultMessage": "Salva" + }, + "setting": { + "defaultMessage": "Impostazione" + }, + "settings": { + "defaultMessage": "Impostazioni" + }, + "settings.default-site": { + "defaultMessage": "Sito Predefinito" + }, + "settings.default-site.404": { + "defaultMessage": "Pagina 404" + }, + "settings.default-site.444": { + "defaultMessage": "Nessuna Risposta (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Pagina di Congratulazioni" + }, + "settings.default-site.description": { + "defaultMessage": "Cosa mostrare quando Nginx riceve una richiesta da un host sconosciuto" + }, + "settings.default-site.html": { + "defaultMessage": "HTML Personalizzato" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Reindirizza" + }, + "setup.preamble": { + "defaultMessage": "Inizia creando il tuo account amministratore." + }, + "setup.title": { + "defaultMessage": "Benvenuto!" + }, + "sign-in": { + "defaultMessage": "Accedi" + }, + "ssl-certificate": { + "defaultMessage": "Certificato SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Host di Destinazione" + }, + "stream.incoming-port": { + "defaultMessage": "Porta in Ingresso" + }, + "streams": { + "defaultMessage": "Stream" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Stream}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Aggiornamento Disponibile: {latestVersion}" + }, + "user": { + "defaultMessage": "Utente" + }, + "user.change-password": { + "defaultMessage": "Cambia Password" + }, + "user.confirm-password": { + "defaultMessage": "Conferma Password" + }, + "user.current-password": { + "defaultMessage": "Password Attuale" + }, + "user.edit-profile": { + "defaultMessage": "Modifica Profilo" + }, + "user.full-name": { + "defaultMessage": "Nome Completo" + }, + "user.login-as": { + "defaultMessage": "Accedi come {name}" + }, + "user.logout": { + "defaultMessage": "Disconnetti" + }, + "user.new-password": { + "defaultMessage": "Nuova Password" + }, + "user.nickname": { + "defaultMessage": "Soprannome" + }, + "user.set-password": { + "defaultMessage": "Imposta Password" + }, + "user.set-permissions": { + "defaultMessage": "Imposta Permessi per {name}" + }, + "user.switch-dark": { + "defaultMessage": "Passa alla modalità Scura" + }, + "user.switch-light": { + "defaultMessage": "Passa alla modalità Chiara" + }, + "username": { + "defaultMessage": "Nome Utente" + }, + "users": { + "defaultMessage": "Utenti" + } +} diff --git a/frontend/src/locale/src/ja.json b/frontend/src/locale/src/ja.json new file mode 100644 index 0000000..438dc21 --- /dev/null +++ b/frontend/src/locale/src/ja.json @@ -0,0 +1,653 @@ +{ + "access-list": { + "defaultMessage": "アクセスリスト" + }, + "access-list.access-count": { + "defaultMessage": "{count} ルール" + }, + "access-list.auth-count": { + "defaultMessage": "{count} ユーザー" + }, + "access-list.help-rules-last": { + "defaultMessage": "少なくとも 1 つのルールが存在する場合、 他のすべてを拒否するルールが最後に追加されます" + }, + "access-list.help.rules-order": { + "defaultMessage": "許可コマンドと拒否コマンドは定義された順番で適用されます" + }, + "access-list.pass-auth": { + "defaultMessage": "認証情報をアップストリームに送信する" + }, + "access-list.public": { + "defaultMessage": "公開されたアクセス" + }, + "access-list.public.subtitle": { + "defaultMessage": "ベーシック認証を使用しません" + }, + "access-list.satisfy-any": { + "defaultMessage": "いずれかを満たす" + }, + "access-list.subtitle": { + "defaultMessage": "{users} ユーザー, {rules} ルール - 作成日時: {date}" + }, + "access-lists": { + "defaultMessage": "アクセスリスト" + }, + "action.add": { + "defaultMessage": "追加" + }, + "action.add-location": { + "defaultMessage": "場所を追加" + }, + "action.close": { + "defaultMessage": "閉じる" + }, + "action.delete": { + "defaultMessage": "削除" + }, + "action.disable": { + "defaultMessage": "無効化" + }, + "action.download": { + "defaultMessage": "ダウンロード" + }, + "action.edit": { + "defaultMessage": "編集" + }, + "action.enable": { + "defaultMessage": "有効化" + }, + "action.permissions": { + "defaultMessage": "権限" + }, + "action.renew": { + "defaultMessage": "更新" + }, + "action.view-details": { + "defaultMessage": "詳細" + }, + "auditlogs": { + "defaultMessage": "監査ログ" + }, + "cancel": { + "defaultMessage": "キャンセル" + }, + "certificate": { + "defaultMessage": "証明書" + }, + "certificate.custom-certificate": { + "defaultMessage": "証明書" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "証明書キー" + }, + "certificate.custom-intermediate": { + "defaultMessage": "中間証明書" + }, + "certificate.in-use": { + "defaultMessage": "使用中" + }, + "certificate.none.subtitle": { + "defaultMessage": "証明書が割り当てられていません" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "このホストはHTTPSを使用しません" + }, + "certificate.none.title": { + "defaultMessage": "無し" + }, + "certificate.not-in-use": { + "defaultMessage": "未使用" + }, + "certificate.renew": { + "defaultMessage": "証明書を更新" + }, + "certificates": { + "defaultMessage": "証明書" + }, + "certificates.custom": { + "defaultMessage": "カスタム証明書" + }, + "certificates.custom.warning": { + "defaultMessage": "パスワードによって保護されたキーファイルはサポートされていません" + }, + "certificates.dns.credentials": { + "defaultMessage": "資格情報ファイルの内容" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "このプラグインはプロバイダーのAPIキーか認証情報を含む設定ファイルが必要です" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "このデータはファイルとデータベースにプレーンテキストとして保存されます" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "DNS伝播時間(秒)" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "DNSの伝搬時間を秒で指定します。空にするとデフォルトの値を使用します。" + }, + "certificates.dns.provider": { + "defaultMessage": "DNSプロバイダー" + }, + "certificates.dns.warning": { + "defaultMessage": "このセクションはCertbotとそのDNSプラグインの知識が必要です。各プラグインのドキュメントを参照してください。" + }, + "certificates.http.reachability-404": { + "defaultMessage": "このドメインはNginx Proxy Managerではないサーバーを指しているようです。ドメインがこのNPMインスタンスを指していることを確認してください。" + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "site24x7.comへの接続でエラーが発生し、到達性チェックに失敗しました" + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "このドメインには利用可能なサーバーがありません。ドメインが存在し、NPMインスタンスのIPアドレスを指していること、必要に応じてルーターでポート80が転送されていることを確認してください。" + }, + "certificates.http.reachability-ok": { + "defaultMessage": "サーバーへ到達可能であり、証明書の作成が可能です。" + }, + "certificates.http.reachability-other": { + "defaultMessage": "このドメインでサーバーが見つかりましたが予期しないステータスコード {code} を返しました. NPMサーバーが動いていますか? ドメインがこのNPMインスタンスを指していることを確認してください。" + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "このドメインでサーバーが見つかりましたが予期しないデータを返しました. NPMサーバーが動いていますか? ドメインがこのNPMインスタンスを指していることを確認してください。" + }, + "certificates.http.test-results": { + "defaultMessage": "テスト結果" + }, + "certificates.http.warning": { + "defaultMessage": "これらのドメインは、すでにこのインストール先を指すように設定されている必要がありますあ." + }, + "certificates.key-type": { + "defaultMessage": "鍵タイプ" + }, + "certificates.key-type-description": { + "defaultMessage": "RSAは広く互換性があり、ECDSAはより高速で安全ですが、古いシステムではサポートされていない場合があります" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "Let's Encryptを使用する" + }, + "certificates.request.title": { + "defaultMessage": "新しい証明書を作成" + }, + "column.access": { + "defaultMessage": "アクセス" + }, + "column.authorization": { + "defaultMessage": "認証" + }, + "column.authorizations": { + "defaultMessage": "認証" + }, + "column.custom-locations": { + "defaultMessage": "カスタムロケーション" + }, + "column.destination": { + "defaultMessage": "宛先" + }, + "column.details": { + "defaultMessage": "詳細" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "イベント" + }, + "column.expires": { + "defaultMessage": "期限切れ" + }, + "column.http-code": { + "defaultMessage": "アクセス" + }, + "column.incoming-port": { + "defaultMessage": "受信ポート" + }, + "column.name": { + "defaultMessage": "名前" + }, + "column.protocol": { + "defaultMessage": "プロトコル" + }, + "column.provider": { + "defaultMessage": "プロバイダー" + }, + "column.roles": { + "defaultMessage": "Roles" + }, + "column.rules": { + "defaultMessage": "ルール" + }, + "column.satisfy": { + "defaultMessage": "Satisfy" + }, + "column.satisfy-all": { + "defaultMessage": "すべて" + }, + "column.satisfy-any": { + "defaultMessage": "いずれか" + }, + "column.scheme": { + "defaultMessage": "スキーム" + }, + "column.source": { + "defaultMessage": "ソース" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "ステータス" + }, + "created-on": { + "defaultMessage": "作成日時: {date}" + }, + "dashboard": { + "defaultMessage": "ダッシュボード" + }, + "dead-host": { + "defaultMessage": "404 ホスト" + }, + "dead-hosts": { + "defaultMessage": "404 ホスト" + }, + "dead-hosts.count": { + "defaultMessage": "{count} 404 ホスト" + }, + "disabled": { + "defaultMessage": "無効化" + }, + "domain-names": { + "defaultMessage": "ドメイン名" + }, + "domain-names.max": { + "defaultMessage": "{count}のドメイン名が最大です" + }, + "domain-names.placeholder": { + "defaultMessage": "追加するドメインを入力..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "ワイルドカードはこのタイプでは許可されていません" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "ワイルドカードはこのCAではサポートされていません" + }, + "domains.force-ssl": { + "defaultMessage": "SSLを強制" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTSを有効化" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTSサブドメイン" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2サポート" + }, + "domains.use-dns": { + "defaultMessage": "DNSチャレンジを使用" + }, + "email-address": { + "defaultMessage": "Emailアドレス" + }, + "empty-search": { + "defaultMessage": "見つかりませんでした" + }, + "empty-subtitle": { + "defaultMessage": "作ってみましょう" + }, + "enabled": { + "defaultMessage": "有効" + }, + "error.access.at-least-one": { + "defaultMessage": "少なくとも一つの認証またはアクセスルールが必要です" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "認証のユーザー名は他と同じ名前は使用できません" + }, + "error.invalid-auth": { + "defaultMessage": "無効なemailまたはパスワード" + }, + "error.invalid-domain": { + "defaultMessage": "無効なドメイン: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "無効なemailアドレス" + }, + "error.max-character-length": { + "defaultMessage": "文字数は長くとも{max}文字です" + }, + "error.max-domains": { + "defaultMessage": "ドメインが多すぎます, 最大値は{max}です" + }, + "error.maximum": { + "defaultMessage": "最大値は{max}です" + }, + "error.min-character-length": { + "defaultMessage": "文字数は少なくとも{min}文字です" + }, + "error.minimum": { + "defaultMessage": "最小値は{min}です" + }, + "error.passwords-must-match": { + "defaultMessage": "パスワードは一致する必要があります" + }, + "error.required": { + "defaultMessage": "必須項目です" + }, + "expires.on": { + "defaultMessage": "有効期限: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork me on Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "一般的なエクスプロイトをブロックする" + }, + "host.flags.cache-assets": { + "defaultMessage": "アセットをキャッシュする" + }, + "host.flags.preserve-path": { + "defaultMessage": "パスワードは一致する必要があります" + }, + "host.flags.protocols": { + "defaultMessage": "プロトコル" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websocketsサポート" + }, + "host.forward-port": { + "defaultMessage": "転送ポート" + }, + "host.forward-scheme": { + "defaultMessage": "スキーム" + }, + "hosts": { + "defaultMessage": "ホスト" + }, + "http-only": { + "defaultMessage": "HTTP Only" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Loading…" + }, + "login.title": { + "defaultMessage": "アカウントにログイン" + }, + "nginx-config.label": { + "defaultMessage": "カスタムNginx設定" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Enter your custom Nginx configuration here at your own risk!" + }, + "no-permission-error": { + "defaultMessage": "これを表示する権限がありません" + }, + "notfound.action": { + "defaultMessage": "ホームに戻る" + }, + "notfound.content": { + "defaultMessage": "申し訳ありませんが探しているページは見つかりませんでした" + }, + "notfound.title": { + "defaultMessage": "おっと... エラーページにたどり着いてしまったようです" + }, + "notification.error": { + "defaultMessage": "エラー" + }, + "notification.object-deleted": { + "defaultMessage": "{object}は削除されました" + }, + "notification.object-disabled": { + "defaultMessage": "{object}は無効化されました" + }, + "notification.object-enabled": { + "defaultMessage": "{object}は有効化されました" + }, + "notification.object-renewed": { + "defaultMessage": "{object}は再作成されました" + }, + "notification.object-saved": { + "defaultMessage": "{object}は保存されました" + }, + "notification.success": { + "defaultMessage": "成功" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "{object}を追加" + }, + "object.delete": { + "defaultMessage": "{object}を削除" + }, + "object.delete.content": { + "defaultMessage": "本当に{object}を削除しますか?" + }, + "object.edit": { + "defaultMessage": "{object}を編集" + }, + "object.empty": { + "defaultMessage": "{objects}はありません" + }, + "object.event.created": { + "defaultMessage": "{object}を作成済み" + }, + "object.event.deleted": { + "defaultMessage": "{object}を削除済み" + }, + "object.event.disabled": { + "defaultMessage": "{object}を無効化済み" + }, + "object.event.enabled": { + "defaultMessage": "{object}を有効化済み" + }, + "object.event.renewed": { + "defaultMessage": "{object}を再作成済み" + }, + "object.event.updated": { + "defaultMessage": "{object}を更新済み" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Options" + }, + "password": { + "defaultMessage": "パスワード" + }, + "password.generate": { + "defaultMessage": "ランダムなパスワードを生成" + }, + "password.hide": { + "defaultMessage": "パスワードを隠す" + }, + "password.show": { + "defaultMessage": "パスワードを表示する" + }, + "permissions.hidden": { + "defaultMessage": "非公開" + }, + "permissions.manage": { + "defaultMessage": "管理" + }, + "permissions.view": { + "defaultMessage": "表示のみ" + }, + "permissions.visibility.all": { + "defaultMessage": "すべて" + }, + "permissions.visibility.title": { + "defaultMessage": "可視性" + }, + "permissions.visibility.user": { + "defaultMessage": "作成したもののみ" + }, + "proxy-host": { + "defaultMessage": "プロキシホスト" + }, + "proxy-host.forward-host": { + "defaultMessage": "転送ホスト名/IP" + }, + "proxy-hosts": { + "defaultMessage": "プロキシホスト" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} プロキシホスト" + }, + "public": { + "defaultMessage": "Public" + }, + "redirection-host": { + "defaultMessage": "リダイレクトホスト" + }, + "redirection-host.forward-domain": { + "defaultMessage": "転送ホスト" + }, + "redirection-hosts": { + "defaultMessage": "リダイレクトホスト" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} リダイレクトホスト" + }, + "role.admin": { + "defaultMessage": "管理者" + }, + "role.standard-user": { + "defaultMessage": "一般ユーザー" + }, + "save": { + "defaultMessage": "保存" + }, + "setting": { + "defaultMessage": "設定" + }, + "settings": { + "defaultMessage": "設定" + }, + "settings.default-site": { + "defaultMessage": "デフォルトサイト" + }, + "settings.default-site.404": { + "defaultMessage": "404ページ" + }, + "settings.default-site.444": { + "defaultMessage": "返答しない (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "設定ページ" + }, + "settings.default-site.description": { + "defaultMessage": "不明なホストを要求されたときにNginxが何を返すかを設定します" + }, + "settings.default-site.html": { + "defaultMessage": "カスタムHTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "リダイレクト" + }, + "setup.preamble": { + "defaultMessage": "管理者アカウントを作成して始めましょう" + }, + "setup.title": { + "defaultMessage": "ようこそ!" + }, + "sign-in": { + "defaultMessage": "サインイン" + }, + "ssl-certificate": { + "defaultMessage": "SSL証明書" + }, + "stream": { + "defaultMessage": "ストリーム" + }, + "stream.forward-host": { + "defaultMessage": "転送ホスト" + }, + "stream.incoming-port": { + "defaultMessage": "受信ポート" + }, + "streams": { + "defaultMessage": "ストリーム" + }, + "streams.count": { + "defaultMessage": "{count} ストリーム" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "テスト" + }, + "user": { + "defaultMessage": "ユーザー" + }, + "user.change-password": { + "defaultMessage": "変更するパスワード" + }, + "user.confirm-password": { + "defaultMessage": "変更するパスワードを確認" + }, + "user.current-password": { + "defaultMessage": "現在のパスワード" + }, + "user.edit-profile": { + "defaultMessage": "プロフィールを編集" + }, + "user.full-name": { + "defaultMessage": "フルネーム" + }, + "user.login-as": { + "defaultMessage": "{name}としてサインイン" + }, + "user.logout": { + "defaultMessage": "ログアウト" + }, + "user.new-password": { + "defaultMessage": "新しいパスワード" + }, + "user.nickname": { + "defaultMessage": "ニックネーム" + }, + "user.set-password": { + "defaultMessage": "パスワードを設定" + }, + "user.set-permissions": { + "defaultMessage": "{name}に権限を設定" + }, + "user.switch-dark": { + "defaultMessage": "ダークモードに変更" + }, + "user.switch-light": { + "defaultMessage": "ライトモードに変更" + }, + "username": { + "defaultMessage": "ユーザー名" + }, + "users": { + "defaultMessage": "ユーザー" + } +} diff --git a/frontend/src/locale/src/ko.json b/frontend/src/locale/src/ko.json new file mode 100644 index 0000000..9c00935 --- /dev/null +++ b/frontend/src/locale/src/ko.json @@ -0,0 +1,695 @@ +{ + "access-list": { + "defaultMessage": "접근 정책" + }, + "access-list.access-count": { + "defaultMessage": "{count}개의 정책" + }, + "access-list.auth-count": { + "defaultMessage": "{count}명의 사용자" + }, + "access-list.help-rules-last": { + "defaultMessage": "규칙이 하나라도 있으면 아래 ‘전체 거부’ 규칙이 마지막에 추가됩니다." + }, + "access-list.help.rules-order": { + "defaultMessage": "허용/거부 규칙은 정의된 순서대로 적용됩니다." + }, + "access-list.pass-auth": { + "defaultMessage": "인증 정보를 원본 서버로 전달" + }, + "access-list.public": { + "defaultMessage": "누구나 접근 가능" + }, + "access-list.public.subtitle": { + "defaultMessage": "기본 인증 필요 없음" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 / 192.168.1.0/24 / IPv6" + }, + "access-list.satisfy-any": { + "defaultMessage": "조건 중 하나라도 충족" + }, + "access-list.subtitle": { + "defaultMessage": "{users}명 {users, plural, one {사용자} other {사용자}}, {rules}개 {rules, plural, one {규칙} other {규칙}} - 생성일: {date}" + }, + "access-lists": { + "defaultMessage": "접근 정책" + }, + "action.add": { + "defaultMessage": "추가" + }, + "action.add-location": { + "defaultMessage": "경로 추가" + }, + "action.allow": { + "defaultMessage": "허용" + }, + "action.close": { + "defaultMessage": "닫기" + }, + "action.delete": { + "defaultMessage": "삭제" + }, + "action.deny": { + "defaultMessage": "거부" + }, + "action.disable": { + "defaultMessage": "비활성화" + }, + "action.download": { + "defaultMessage": "다운로드" + }, + "action.edit": { + "defaultMessage": "편집" + }, + "action.enable": { + "defaultMessage": "활성화" + }, + "action.permissions": { + "defaultMessage": "권한" + }, + "action.renew": { + "defaultMessage": "갱신" + }, + "action.view-details": { + "defaultMessage": "자세히 보기" + }, + "auditlogs": { + "defaultMessage": "감사 로그" + }, + "auto": { + "defaultMessage": "자동" + }, + "cancel": { + "defaultMessage": "취소" + }, + "certificate": { + "defaultMessage": "인증서" + }, + "certificate.custom-certificate": { + "defaultMessage": "인증서" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "인증서 키" + }, + "certificate.custom-intermediate": { + "defaultMessage": "중간 인증서" + }, + "certificate.in-use": { + "defaultMessage": "사용 중" + }, + "certificate.none.subtitle": { + "defaultMessage": "지정된 인증서 없음" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "이 호스트는 HTTPS를 사용하지 않습니다." + }, + "certificate.none.title": { + "defaultMessage": "없음" + }, + "certificate.not-in-use": { + "defaultMessage": "사용 안 함" + }, + "certificate.renew": { + "defaultMessage": "인증서 갱신" + }, + "certificates": { + "defaultMessage": "인증서" + }, + "certificates.custom": { + "defaultMessage": "사용자 지정 인증서" + }, + "certificates.custom.warning": { + "defaultMessage": "비밀번호로 보호된 키 파일은 지원되지 않습니다." + }, + "certificates.dns.credentials": { + "defaultMessage": "DNS 자격 증명 입력" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "이 플러그인은 API 토큰 등이 포함된 설정 파일이 필요합니다." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "입력한 정보는 데이터베이스와 파일에 평문으로 저장됩니다." + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "DNS 전파 시간" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "비워두면 기본값을 사용합니다. DNS 전파를 기다리는 시간(초)입니다." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS 공급자" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "공급자를 선택하세요..." + }, + "certificates.dns.warning": { + "defaultMessage": "이 기능을 사용하려면 Certbot과 DNS 플러그인에 대한 기본적인 이해가 필요합니다. 자세한 내용은 관련 문서를 참고해 주세요." + }, + "certificates.http.reachability-404": { + "defaultMessage": "해당 도메인에서 서버가 탐지되었지만 Nginx Proxy Manager가 아닌 것으로 보입니다. 도메인이 NPM이 실행 중인 IP를 가리키는지 확인하세요." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "site24x7.com과의 통신 오류로 인해 도달 가능 여부를 확인할 수 없습니다." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "해당 도메인에 접근 가능한 서버가 없습니다. 도메인이 존재하며 NPM이 실행되는 IP를 가리키고, 필요하면 라우터에서 80포트가 포워딩되어 있는지 확인하세요." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "서버에 정상적으로 접근할 수 있으며 인증서 발급이 가능합니다." + }, + "certificates.http.reachability-other": { + "defaultMessage": "해당 도메인에서 서버가 발견되었지만 예상치 못한 상태 코드 {code}를 반환했습니다. NPM 서버가 맞는지 확인하세요." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "서버가 응답했지만 예상치 못한 데이터를 반환했습니다. NPM 서버가 맞는지 확인하세요." + }, + "certificates.http.test-results": { + "defaultMessage": "테스트 결과" + }, + "certificates.http.warning": { + "defaultMessage": "도메인이 이 서버를 가리키도록 설정되어 있어야 합니다." + }, + "certificates.key-type": { + "defaultMessage": "키 유형" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA는 호환성이 넓고, ECDSA는 더 빠르고 안전하지만 오래된 시스템에서 지원되지 않을 수 있습니다" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "Let's Encrypt 사용" + }, + "certificates.request.title": { + "defaultMessage": "새 인증서 요청" + }, + "column.access": { + "defaultMessage": "접근 정책" + }, + "column.authorization": { + "defaultMessage": "인증 사용자" + }, + "column.authorizations": { + "defaultMessage": "인증 사용자" + }, + "column.custom-locations": { + "defaultMessage": "사용자 지정 경로" + }, + "column.destination": { + "defaultMessage": "전달 대상" + }, + "column.details": { + "defaultMessage": "기본 설정" + }, + "column.email": { + "defaultMessage": "이메일" + }, + "column.event": { + "defaultMessage": "이벤트" + }, + "column.expires": { + "defaultMessage": "만료일" + }, + "column.http-code": { + "defaultMessage": "HTTP 코드" + }, + "column.incoming-port": { + "defaultMessage": "수신 포트" + }, + "column.name": { + "defaultMessage": "이름" + }, + "column.protocol": { + "defaultMessage": "프로토콜" + }, + "column.provider": { + "defaultMessage": "공급자" + }, + "column.roles": { + "defaultMessage": "권한" + }, + "column.rules": { + "defaultMessage": "IP 정책" + }, + "column.satisfy": { + "defaultMessage": "조건 방식" + }, + "column.satisfy-all": { + "defaultMessage": "모두 충족" + }, + "column.satisfy-any": { + "defaultMessage": "하나라도 충족" + }, + "column.scheme": { + "defaultMessage": "프로토콜" + }, + "column.source": { + "defaultMessage": "도메인" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "상태" + }, + "created-on": { + "defaultMessage": "생성일: {date}" + }, + "dashboard": { + "defaultMessage": "대시보드" + }, + "dead-host": { + "defaultMessage": "404 호스트" + }, + "dead-hosts": { + "defaultMessage": "404 호스트" + }, + "dead-hosts.count": { + "defaultMessage": "{count}개의 404 호스트" + }, + "disabled": { + "defaultMessage": "비활성화" + }, + "domain-names": { + "defaultMessage": "도메인 이름" + }, + "domain-names.max": { + "defaultMessage": "최대 {count}개의 도메인 이름" + }, + "domain-names.placeholder": { + "defaultMessage": "도메인을 입력해주세요." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "HTTP 방식으로는 와일드카드 인증서를 발급할 수 없습니다." + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "이 인증 기관(CA)은 와일드카드를 지원하지 않습니다." + }, + "domains.force-ssl": { + "defaultMessage": "SSL 강제 적용" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS 활성화" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS 서브도메인 포함" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 지원" + }, + "domains.use-dns": { + "defaultMessage": "DNS 챌린지 사용" + }, + "email-address": { + "defaultMessage": "이메일 주소" + }, + "empty-search": { + "defaultMessage": "검색 결과 없음" + }, + "empty-subtitle": { + "defaultMessage": "하나 만들어 보는 건 어떨까요?" + }, + "enabled": { + "defaultMessage": "활성화" + }, + "error.access.at-least-one": { + "defaultMessage": "인증 또는 접근 규칙 중 하나는 반드시 필요합니다." + }, + "error.access.duplicate-usernames": { + "defaultMessage": "인증 사용자 이름은 중복될 수 없습니다." + }, + "error.invalid-auth": { + "defaultMessage": "이메일 또는 비밀번호가 잘못되었습니다." + }, + "error.invalid-domain": { + "defaultMessage": "잘못된 도메인: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "잘못된 이메일 주소입니다." + }, + "error.max-character-length": { + "defaultMessage": "최대 길이는 {max}자입니다." + }, + "error.max-domains": { + "defaultMessage": "도메인이 너무 많습니다. 최대 {max}개까지 가능합니다." + }, + "error.maximum": { + "defaultMessage": "최댓값은 {max}입니다." + }, + "error.min-character-length": { + "defaultMessage": "최소 길이는 {min}자입니다." + }, + "error.minimum": { + "defaultMessage": "최솟값은 {min}입니다." + }, + "error.passwords-must-match": { + "defaultMessage": "비밀번호가 일치해야 합니다." + }, + "error.required": { + "defaultMessage": "필수 항목입니다." + }, + "expires.on": { + "defaultMessage": "만료일: {date}" + }, + "footer.github-fork": { + "defaultMessage": "GitHub에서 포크하기" + }, + "host.flags.block-exploits": { + "defaultMessage": "일반적인 공격 차단" + }, + "host.flags.cache-assets": { + "defaultMessage": "정적 에셋 캐싱" + }, + "host.flags.preserve-path": { + "defaultMessage": "요청 경로 유지" + }, + "host.flags.protocols": { + "defaultMessage": "프로토콜" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "웹소켓 지원" + }, + "host.forward-port": { + "defaultMessage": "전달할 포트" + }, + "host.forward-scheme": { + "defaultMessage": "프로토콜" + }, + "hosts": { + "defaultMessage": "호스트 목록" + }, + "http-only": { + "defaultMessage": "HTTP 전용" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt (DNS 방식)" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt (HTTP 방식)" + }, + "loading": { + "defaultMessage": "불러오는 중…" + }, + "login.title": { + "defaultMessage": "로그인" + }, + "nginx-config.label": { + "defaultMessage": "사용자 지정 Nginx 설정" + }, + "nginx-config.placeholder": { + "defaultMessage": "# 위험을 감수하고 여기에 사용자 지정 Nginx 설정을 입력하세요!" + }, + "no-permission-error": { + "defaultMessage": "이 내용을 볼 권한이 없습니다." + }, + "notfound.action": { + "defaultMessage": "홈으로 이동" + }, + "notfound.content": { + "defaultMessage": "죄송합니다. 찾으시는 페이지를 찾을 수 없습니다." + }, + "notfound.title": { + "defaultMessage": "이런… 오류 페이지에 도착했습니다." + }, + "notification.error": { + "defaultMessage": "오류" + }, + "notification.object-deleted": { + "defaultMessage": "{object}이(가) 삭제되었습니다." + }, + "notification.object-disabled": { + "defaultMessage": "{object}이(가) 비활성화되었습니다." + }, + "notification.object-enabled": { + "defaultMessage": "{object}이(가) 활성화되었습니다." + }, + "notification.object-renewed": { + "defaultMessage": "{object}이(가) 갱신되었습니다." + }, + "notification.object-saved": { + "defaultMessage": "{object}이(가) 저장되었습니다." + }, + "notification.success": { + "defaultMessage": "성공" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "{object} 추가" + }, + "object.delete": { + "defaultMessage": "{object} 삭제" + }, + "object.delete.content": { + "defaultMessage": "이 {object}을(를) 정말 삭제하시겠습니까?" + }, + "object.edit": { + "defaultMessage": "{object} 편집" + }, + "object.empty": { + "defaultMessage": "{objects}이(가) 없습니다." + }, + "object.event.created": { + "defaultMessage": "{object}이(가) 생성됨" + }, + "object.event.deleted": { + "defaultMessage": "{object}이(가) 삭제됨" + }, + "object.event.disabled": { + "defaultMessage": "{object}이(가) 비활성화됨" + }, + "object.event.enabled": { + "defaultMessage": "{object}이(가) 활성화됨" + }, + "object.event.renewed": { + "defaultMessage": "{object}이(가) 갱신됨" + }, + "object.event.updated": { + "defaultMessage": "{object}이(가) 업데이트됨" + }, + "offline": { + "defaultMessage": "비활성화" + }, + "online": { + "defaultMessage": "활성화" + }, + "options": { + "defaultMessage": "옵션" + }, + "password": { + "defaultMessage": "비밀번호" + }, + "password.generate": { + "defaultMessage": "무작위 비밀번호 생성" + }, + "password.hide": { + "defaultMessage": "비밀번호 숨기기" + }, + "password.show": { + "defaultMessage": "비밀번호 표시" + }, + "permissions.hidden": { + "defaultMessage": "숨김" + }, + "permissions.manage": { + "defaultMessage": "관리" + }, + "permissions.view": { + "defaultMessage": "보기 전용" + }, + "permissions.visibility.all": { + "defaultMessage": "모든 항목" + }, + "permissions.visibility.title": { + "defaultMessage": "항목 표시 설정" + }, + "permissions.visibility.user": { + "defaultMessage": "내가 만든 항목만" + }, + "proxy-host": { + "defaultMessage": "프록시 호스트" + }, + "proxy-host.forward-host": { + "defaultMessage": "전달할 호스트명 / IP" + }, + "proxy-hosts": { + "defaultMessage": "프록시 호스트" + }, + "proxy-hosts.count": { + "defaultMessage": "{count}개의 프록시 호스트" + }, + "public": { + "defaultMessage": "공개" + }, + "redirection-host": { + "defaultMessage": "리다이렉션 호스트" + }, + "redirection-host.forward-domain": { + "defaultMessage": "전달할 도메인" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP 코드" + }, + "redirection-hosts": { + "defaultMessage": "리다이렉션 호스트" + }, + "redirection-hosts.count": { + "defaultMessage": "{count}개의 리다이렉션 호스트" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Multiple Choices" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Moved permanently" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Moved temporarily" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 See other" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Temporary redirect" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Permanent redirect" + }, + "role.admin": { + "defaultMessage": "관리자" + }, + "role.standard-user": { + "defaultMessage": "일반 사용자" + }, + "save": { + "defaultMessage": "저장" + }, + "setting": { + "defaultMessage": "설정" + }, + "settings": { + "defaultMessage": "설정" + }, + "settings.default-site": { + "defaultMessage": "기본 사이트" + }, + "settings.default-site.404": { + "defaultMessage": "404 페이지" + }, + "settings.default-site.444": { + "defaultMessage": "응답 없음 (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "축하 페이지" + }, + "settings.default-site.description": { + "defaultMessage": "알 수 없는 호스트로 요청이 들어왔을 때 표시할 내용" + }, + "settings.default-site.html": { + "defaultMessage": "사용자 지정 HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "리다이렉트" + }, + "setup.preamble": { + "defaultMessage": "관리자 계정을 만들어 시작하세요." + }, + "setup.title": { + "defaultMessage": "환영합니다!" + }, + "sign-in": { + "defaultMessage": "로그인" + }, + "ssl-certificate": { + "defaultMessage": "SSL 인증서" + }, + "stream": { + "defaultMessage": "호스트 스트림" + }, + "stream.forward-host": { + "defaultMessage": "전달할 호스트" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com / 10.0.0.1 / IPv6" + }, + "stream.incoming-port": { + "defaultMessage": "수신 포트" + }, + "streams": { + "defaultMessage": "호스트 스트림" + }, + "streams.count": { + "defaultMessage": "{count}개의 호스트 스트림" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "테스트" + }, + "update-available": { + "defaultMessage": "업데이트 가능: {latestVersion}" + }, + "user": { + "defaultMessage": "사용자" + }, + "user.change-password": { + "defaultMessage": "비밀번호 변경" + }, + "user.confirm-password": { + "defaultMessage": "비밀번호 확인" + }, + "user.current-password": { + "defaultMessage": "현재 비밀번호" + }, + "user.edit-profile": { + "defaultMessage": "프로필 편집" + }, + "user.full-name": { + "defaultMessage": "전체 이름" + }, + "user.login-as": { + "defaultMessage": "{name}으로 로그인" + }, + "user.logout": { + "defaultMessage": "로그아웃" + }, + "user.new-password": { + "defaultMessage": "새 비밀번호" + }, + "user.nickname": { + "defaultMessage": "닉네임" + }, + "user.set-password": { + "defaultMessage": "비밀번호 설정" + }, + "user.set-permissions": { + "defaultMessage": "{name}의 권한 설정" + }, + "user.switch-dark": { + "defaultMessage": "다크 모드로 전환" + }, + "user.switch-light": { + "defaultMessage": "라이트 모드로 전환" + }, + "username": { + "defaultMessage": "사용자 이름" + }, + "users": { + "defaultMessage": "사용자" + } +} diff --git a/frontend/src/locale/src/lang-list.json b/frontend/src/locale/src/lang-list.json new file mode 100644 index 0000000..79dabe2 --- /dev/null +++ b/frontend/src/locale/src/lang-list.json @@ -0,0 +1,68 @@ +{ + "locale-en-US": { + "defaultMessage": "English" + }, + "locale-es-ES": { + "defaultMessage": "Español" + }, + "locale-et-EE": { + "defaultMessage": "Eesti" + }, + "locale-ie-GA": { + "defaultMessage": "Gaeilge" + }, + "locale-de-DE": { + "defaultMessage": "German" + }, + "locale-pt-PT": { + "defaultMessage": "Português (Europeu)" + }, + "locale-fr-FR": { + "defaultMessage": "Français" + }, + "locale-id-ID": { + "defaultMessage": "Bahasa Indonesia" + }, + "locale-ja-JP": { + "defaultMessage": "日本語" + }, + "locale-ru-RU": { + "defaultMessage": "Русский" + }, + "locale-sk-SK": { + "defaultMessage": "Slovenčina" + }, + "locale-cs-CZ": { + "defaultMessage": "Čeština" + }, + "locale-zh-CN": { + "defaultMessage": "中文" + }, + "locale-pl-PL": { + "defaultMessage": "Polski" + }, + "locale-it-IT": { + "defaultMessage": "Italiano" + }, + "locale-vi-VN": { + "defaultMessage": "Tiếng Việt" + }, + "locale-nl-NL": { + "defaultMessage": "Nederlands" + }, + "locale-ko-KR": { + "defaultMessage": "한국어" + }, + "locale-bg-BG": { + "defaultMessage": "Български" + }, + "locale-tr-TR": { + "defaultMessage": "Türkçe" + }, + "locale-hu-HU": { + "defaultMessage": "Magyar" + }, + "locale-no-NO": { + "defaultMessage": "Norsk" + } +} diff --git a/frontend/src/locale/src/nl.json b/frontend/src/locale/src/nl.json new file mode 100644 index 0000000..81c3705 --- /dev/null +++ b/frontend/src/locale/src/nl.json @@ -0,0 +1,659 @@ +{ + "access-list": { + "defaultMessage": "Toegangslijst" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Regel} other {Regels}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Gebruiker} other {Gebruikers}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Als er minimaal 1 regel bestaat, wordt deze regel als laatste toegevoegd" + }, + "access-list.help.rules-order": { + "defaultMessage": "Onthoud dat de regels van boven naar beneden worden toegevoegd." + }, + "access-list.pass-auth": { + "defaultMessage": "Pass Auth to Upstream" + }, + "access-list.public": { + "defaultMessage": "Publiekelijk toegankelijk" + }, + "access-list.public.subtitle": { + "defaultMessage": "Geen basisautentificatie vereist" + }, + "access-list.satisfy-any": { + "defaultMessage": "Voldoe aan elke" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Gebruiker} other {Gebruikers}}, {rules} {rules, plural, one {Regel} other {Regels}} - Aangemaakt: {date}" + }, + "access-lists": { + "defaultMessage": "Toegangslijsten" + }, + "action.add": { + "defaultMessage": "Toevoegen" + }, + "action.add-location": { + "defaultMessage": "Locatie Toevoegen" + }, + "action.close": { + "defaultMessage": "Sluiten" + }, + "action.delete": { + "defaultMessage": "Verwijderen" + }, + "action.disable": { + "defaultMessage": "Uitzetten" + }, + "action.download": { + "defaultMessage": "Download" + }, + "action.edit": { + "defaultMessage": "Bewerken" + }, + "action.enable": { + "defaultMessage": "Aanzetten" + }, + "action.permissions": { + "defaultMessage": "Rechten" + }, + "action.renew": { + "defaultMessage": "Vernieuwen" + }, + "action.view-details": { + "defaultMessage": "Bekijk Details" + }, + "auditlogs": { + "defaultMessage": "Logboeken" + }, + "cancel": { + "defaultMessage": "Annuleren" + }, + "certificate": { + "defaultMessage": "Certificaat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificaat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Certificaat Sleutel" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Intermediate Certificaat" + }, + "certificate.in-use": { + "defaultMessage": "In Gebruik" + }, + "certificate.none.subtitle": { + "defaultMessage": "Geen certificaat toegewezen" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Deze host gebruikt geen HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Geen" + }, + "certificate.not-in-use": { + "defaultMessage": "Niet Gebruikt" + }, + "certificate.renew": { + "defaultMessage": "Certificaat Vernieuwen" + }, + "certificates": { + "defaultMessage": "Certificaten" + }, + "certificates.custom": { + "defaultMessage": "Aangepast Certificaat" + }, + "certificates.custom.warning": { + "defaultMessage": "Sleutels met een wachtzin zijn niet ondersteund." + }, + "certificates.dns.credentials": { + "defaultMessage": "Credentials File Content" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Deze plugin vereist een configuratiebestand met een API token of andere gegevens van de provider." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Deze data zal worden opgeslagen als plaintext in de database en in een bestand!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Verwerkingstijd (seconden)" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Laat leeg om de standaardwaarde van de plugin te gebruiken. Aantal seconden om te wachten op DNS propagatie." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS Provider" + }, + "certificates.dns.warning": { + "defaultMessage": "Deze sectie vereist wat informatie over Certbot en zijn DNS plugins. Gebruik de documentatie van de bijbehorende plugins." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Er is een server gevonden op deze domeinnaam, maar dat lijkt niet Nginx Proxy Manager te zijn. Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Bereikbaarheid kan niet worden bepaald door een communicatiefout met site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Er is geen server beschikbaar op dit domein. Zorg ervoor dat je domein bestaat en naar het IP waar je NPM instance draait wijst en eventueel port 80 wordt doorgegeven in je router." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Jouw server is bereikbaar en certificaten kunnen worden aangemaakt." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Er is een server gevonden op deze domeinnaam, maar heeft een onverwachte statuscode ({code}) teruggegeven. Is dat de NPM server? Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Er is een server gevonden op deze domeinnaam, maar heeft een onverwachte gegevens teruggegeven. Is dat de NPM server? Zorg ervoor dat je domein naar het IP waar je NPM instance draait wijst." + }, + "certificates.http.test-results": { + "defaultMessage": "Testresultaten" + }, + "certificates.http.warning": { + "defaultMessage": "Deze domeinen moeten al worden geconfigureerd om naar deze installatie te wijzen." + }, + "certificates.key-type": { + "defaultMessage": "Sleuteltype" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA is breed compatibel, ECDSA is sneller en veiliger maar wordt mogelijk niet ondersteund door oudere systemen" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "met Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Vraag een nieuwe Certificaat aan" + }, + "column.access": { + "defaultMessage": "Toegang" + }, + "column.authorization": { + "defaultMessage": "Authorizatie" + }, + "column.authorizations": { + "defaultMessage": "Authorizaties" + }, + "column.custom-locations": { + "defaultMessage": "Aangepaste Locaties" + }, + "column.destination": { + "defaultMessage": "Doel" + }, + "column.details": { + "defaultMessage": "Details" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Gebeurtenis" + }, + "column.expires": { + "defaultMessage": "Verloopt" + }, + "column.http-code": { + "defaultMessage": "HTTP Code" + }, + "column.incoming-port": { + "defaultMessage": "Inkomende Poort" + }, + "column.name": { + "defaultMessage": "Naam" + }, + "column.protocol": { + "defaultMessage": "Protocol" + }, + "column.provider": { + "defaultMessage": "Provider" + }, + "column.roles": { + "defaultMessage": "Rollen" + }, + "column.rules": { + "defaultMessage": "Regels" + }, + "column.satisfy": { + "defaultMessage": "Vervul" + }, + "column.satisfy-all": { + "defaultMessage": "Alle" + }, + "column.satisfy-any": { + "defaultMessage": "Elke" + }, + "column.scheme": { + "defaultMessage": "Schema" + }, + "column.source": { + "defaultMessage": "Bron" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Aangemaakt: {date}" + }, + "dashboard": { + "defaultMessage": "Dashboard" + }, + "dead-host": { + "defaultMessage": "404 Host" + }, + "dead-hosts": { + "defaultMessage": "404 Hosts" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}" + }, + "disabled": { + "defaultMessage": "Uitgezet" + }, + "domain-names": { + "defaultMessage": "Domeinnamen" + }, + "domain-names.max": { + "defaultMessage": "{count} domeinnamen toegestaan" + }, + "domain-names.placeholder": { + "defaultMessage": "Voeg een domeinnaam toe..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards zijn niet toegestaan voor dit type" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards zijn niet ondersteund voor deze CA" + }, + "domains.force-ssl": { + "defaultMessage": "Forceer SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Aangezet" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Subdomein" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 Ondersteuning" + }, + "domains.use-dns": { + "defaultMessage": "Gebruik DNS Challenge" + }, + "email-address": { + "defaultMessage": "E-mailadres" + }, + "empty-search": { + "defaultMessage": "Geen resultaten gevonden" + }, + "empty-subtitle": { + "defaultMessage": "Waarom niet een maken?" + }, + "enabled": { + "defaultMessage": "Aangezet" + }, + "error.access.at-least-one": { + "defaultMessage": "Minimaal één authorizatie- of één toegangsregel is vereist" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Gebruikersnamen moeten uniek zijn" + }, + "error.invalid-auth": { + "defaultMessage": "Ongeldige email of wachtwoord" + }, + "error.invalid-domain": { + "defaultMessage": "Ongeldige domeinnaam: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Ongeldig e-mailadres" + }, + "error.max-character-length": { + "defaultMessage": "Maximale lengte is {max} karakter{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Te veel domeinnamen, max is {max}" + }, + "error.maximum": { + "defaultMessage": "Maximale is {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimale lengte is {min} karakter{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Minimale is {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Wachtwoorden moeten overeenkomen" + }, + "error.required": { + "defaultMessage": "Dit is verplicht" + }, + "expires.on": { + "defaultMessage": "Verloopt: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Maak een Fork op Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blokkeer Veelvoorkomende Kwetsbaarheden" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache Assets" + }, + "host.flags.preserve-path": { + "defaultMessage": "Pad Behouden" + }, + "host.flags.protocols": { + "defaultMessage": "Protocollen" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets Ondersteuning" + }, + "host.forward-port": { + "defaultMessage": "Poort Doorsturen" + }, + "host.forward-scheme": { + "defaultMessage": "Schema" + }, + "hosts": { + "defaultMessage": "Hosts" + }, + "http-only": { + "defaultMessage": "Alleen HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Laden…" + }, + "login.title": { + "defaultMessage": "Inloggen" + }, + "nginx-config.label": { + "defaultMessage": "Aangepaste Nginx Configuratie" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Voeg jouw aangepaste Nginx configuratie hier op eigen risico toe!" + }, + "no-permission-error": { + "defaultMessage": "Jij hebt geen toegang om dit te bekijken." + }, + "notfound.action": { + "defaultMessage": "Thuis" + }, + "notfound.content": { + "defaultMessage": "De pagina waar je naar op zoek bent kan niet worden gevonden" + }, + "notfound.title": { + "defaultMessage": "Oeps… Je hebt een foutpagina gevonden" + }, + "notification.error": { + "defaultMessage": "Fout" + }, + "notification.object-deleted": { + "defaultMessage": "{object} is verwijderd" + }, + "notification.object-disabled": { + "defaultMessage": "{object} is uitgezet" + }, + "notification.object-enabled": { + "defaultMessage": "{object} is aangezet" + }, + "notification.object-renewed": { + "defaultMessage": "{object} is vernieuwd" + }, + "notification.object-saved": { + "defaultMessage": "{object} is opgeslagen" + }, + "notification.success": { + "defaultMessage": "Succes" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Voeg {object} toe" + }, + "object.delete": { + "defaultMessage": "Verwijder {object}" + }, + "object.delete.content": { + "defaultMessage": "Weet je zeker dat je {object} wilt verwijderen?" + }, + "object.edit": { + "defaultMessage": "Bewerk {object}" + }, + "object.empty": { + "defaultMessage": "Er zijn geen {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} is aangemaakt" + }, + "object.event.deleted": { + "defaultMessage": "{object} is verwijderd" + }, + "object.event.disabled": { + "defaultMessage": "{object} is uitgezet" + }, + "object.event.enabled": { + "defaultMessage": "{object} is aangezet" + }, + "object.event.renewed": { + "defaultMessage": "{object} is vernieuwd" + }, + "object.event.updated": { + "defaultMessage": "{object} is bijgewerkt" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Opties" + }, + "password": { + "defaultMessage": "Wachtwoord" + }, + "password.generate": { + "defaultMessage": "Willekeurig wachtwoord genereren" + }, + "password.hide": { + "defaultMessage": "Wachtwoord Verbergen" + }, + "password.show": { + "defaultMessage": "Toon Wachtwoord" + }, + "permissions.hidden": { + "defaultMessage": "Verborgen" + }, + "permissions.manage": { + "defaultMessage": "Beheer" + }, + "permissions.view": { + "defaultMessage": "Alleen Bekijken" + }, + "permissions.visibility.all": { + "defaultMessage": "Alle Items" + }, + "permissions.visibility.title": { + "defaultMessage": "Item Zichtbaarheid" + }, + "permissions.visibility.user": { + "defaultMessage": "Alleen Aangemaakte Items" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Hostname / IP Doorsturen" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Hosts" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}" + }, + "public": { + "defaultMessage": "Openbaar" + }, + "redirection-host": { + "defaultMessage": "Redirection Host" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Doorgestuurd Domein" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP Code" + }, + "redirection-hosts": { + "defaultMessage": "Redirection Hosts" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}" + }, + "role.admin": { + "defaultMessage": "Beheerder" + }, + "role.standard-user": { + "defaultMessage": "Standaard Gebruiker" + }, + "save": { + "defaultMessage": "Opslaan" + }, + "setting": { + "defaultMessage": "Instelling" + }, + "settings": { + "defaultMessage": "Instellingen" + }, + "settings.default-site": { + "defaultMessage": "Standaard Site" + }, + "settings.default-site.404": { + "defaultMessage": "404 Pagina" + }, + "settings.default-site.444": { + "defaultMessage": "Geen Antwoord (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Felicitatiepagina" + }, + "settings.default-site.description": { + "defaultMessage": "Wat te tonen als Nginx een onbekende Host ontvangt" + }, + "settings.default-site.html": { + "defaultMessage": "Aangepaste HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Omleiding" + }, + "setup.preamble": { + "defaultMessage": "Begin met het aanmaken van je beheerder account." + }, + "setup.title": { + "defaultMessage": "Welkom!" + }, + "sign-in": { + "defaultMessage": "Inloggen" + }, + "ssl-certificate": { + "defaultMessage": "SSL Certificaten" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Doorgestuurde Host" + }, + "stream.incoming-port": { + "defaultMessage": "Inkomende Poort" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Update Beschikbaar: {latestVersion}" + }, + "user": { + "defaultMessage": "Gebruiker" + }, + "user.change-password": { + "defaultMessage": "Verander Wachtwoord" + }, + "user.confirm-password": { + "defaultMessage": "Bevestig Wachtwoord" + }, + "user.current-password": { + "defaultMessage": "Huidig Wachtwoord" + }, + "user.edit-profile": { + "defaultMessage": "Profiel Bewerken" + }, + "user.full-name": { + "defaultMessage": "Volledige Naam" + }, + "user.login-as": { + "defaultMessage": "Inloggen als {name}" + }, + "user.logout": { + "defaultMessage": "Uitloggen" + }, + "user.new-password": { + "defaultMessage": "Nieuw Wachtwoord" + }, + "user.nickname": { + "defaultMessage": "Bijnaam" + }, + "user.set-password": { + "defaultMessage": "Zet Wachtwoord" + }, + "user.set-permissions": { + "defaultMessage": "Zet machtigingen voor {name}" + }, + "user.switch-dark": { + "defaultMessage": "Verander naar donkere modus" + }, + "user.switch-light": { + "defaultMessage": "Verander naar lichte modus" + }, + "username": { + "defaultMessage": "Gebruikersnaam" + }, + "users": { + "defaultMessage": "Gebruikers" + } +} diff --git a/frontend/src/locale/src/no.json b/frontend/src/locale/src/no.json new file mode 100644 index 0000000..f14ea54 --- /dev/null +++ b/frontend/src/locale/src/no.json @@ -0,0 +1,776 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Gjenstående backup-koder: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Lagre disse backup-kodene på et sikkert sted. Hver kode kan kun brukes én gang." + }, + "2fa.disable": { + "defaultMessage": "Deaktiver tofaktorautentisering" + }, + "2fa.disable-confirm": { + "defaultMessage": "Deaktiver 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Å deaktivere tofaktorautentisering vil gjøre kontoen din mindre sikker." + }, + "2fa.disabled": { + "defaultMessage": "Deaktivert" + }, + "2fa.done": { + "defaultMessage": "Jeg har lagret backup-kodene mine" + }, + "2fa.enable": { + "defaultMessage": "Aktiver tofaktorautentisering" + }, + "2fa.enabled": { + "defaultMessage": "Aktivert" + }, + "2fa.enter-code": { + "defaultMessage": "Angi verifiseringskode" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Angi verifiseringskode for å deaktivere" + }, + "2fa.regenerate": { + "defaultMessage": "Regenerer" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Generer nye backup-koder" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Angi en verifiseringskode for å generere nye backup-koder. Dine gamle koder vil bli ugyldige." + }, + "2fa.secret-key": { + "defaultMessage": "Hemmelig nøkkel" + }, + "2fa.setup-instructions": { + "defaultMessage": "Skann denne QR-koden med autentiseringsappen din, eller skriv inn nøkkelen manuelt." + }, + "2fa.status": { + "defaultMessage": "Status" + }, + "2fa.title": { + "defaultMessage": "Tofaktorautentisering" + }, + "2fa.verify-enable": { + "defaultMessage": "Verifiser og aktiver" + }, + "access-list": { + "defaultMessage": "Tilgangsliste" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {regel} other {regler}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {bruker} other {brukere}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Når minst én regel finnes, legges denne \"deny all\"-regelen til sist" + }, + "access-list.help.rules-order": { + "defaultMessage": "Merk at tillat- og nekt-direktivene brukes i den rekkefølgen de er definert." + }, + "access-list.pass-auth": { + "defaultMessage": "Send autentisering til upstream" + }, + "access-list.public": { + "defaultMessage": "Offentlig tilgjengelig" + }, + "access-list.public.subtitle": { + "defaultMessage": "Ingen grunnleggende autentisering kreves" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 eller 192.168.1.0/24 eller 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Oppfyll en av kravene" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {bruker} other {brukere}}, {rules} {rules, plural, one {regel} other {regler}} - Opprettet: {date}" + }, + "access-lists": { + "defaultMessage": "Tilgangslister" + }, + "action.add": { + "defaultMessage": "Legg til" + }, + "action.add-location": { + "defaultMessage": "Legg til plassering" + }, + "action.allow": { + "defaultMessage": "Tillat" + }, + "action.close": { + "defaultMessage": "Lukk" + }, + "action.delete": { + "defaultMessage": "Slett" + }, + "action.deny": { + "defaultMessage": "Nekt" + }, + "action.disable": { + "defaultMessage": "Deaktiver" + }, + "action.download": { + "defaultMessage": "Last ned" + }, + "action.edit": { + "defaultMessage": "Rediger" + }, + "action.enable": { + "defaultMessage": "Aktiver" + }, + "action.permissions": { + "defaultMessage": "Tillatelser" + }, + "action.renew": { + "defaultMessage": "Forny" + }, + "action.view-details": { + "defaultMessage": "Vis detaljer" + }, + "auditlogs": { + "defaultMessage": "Revisjonslogger" + }, + "auto": { + "defaultMessage": "Auto" + }, + "cancel": { + "defaultMessage": "Avbryt" + }, + "certificate": { + "defaultMessage": "Sertifikat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Egendefinert Sertifikat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Egendefinert Sertifikat nøkkel" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Egendefinert Intermediate Sertifikat" + }, + "certificate.in-use": { + "defaultMessage": "I bruk" + }, + "certificate.none.subtitle": { + "defaultMessage": "Ingen sertifikat tildelt" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Denne verten vil ikke bruke HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Ingen" + }, + "certificate.not-in-use": { + "defaultMessage": "Ikke i bruk" + }, + "certificate.renew": { + "defaultMessage": "Forny sertifikat" + }, + "certificates": { + "defaultMessage": "Sertifikater" + }, + "certificates.custom": { + "defaultMessage": "Egendefinert Sertifikat" + }, + "certificates.custom.warning": { + "defaultMessage": "Nøkkelfiler beskyttet med passordfrase støttes ikke." + }, + "certificates.dns.credentials": { + "defaultMessage": "Innhold i legitimasjonsfil" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Denne pluginen krever en konfigurasjonsfil som inneholder en API-token eller andre legitimasjoner for leverandøren din" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Disse dataene vil bli lagret som ren tekst i databasen og i en fil!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propageringsekunder" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "La stå tomt for å bruke pluginens standardverdi. Antall sekunder å vente på DNS-propagasjon." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS-leverandør" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Velg en leverandør..." + }, + "certificates.dns.warning": { + "defaultMessage": "Denne seksjonen krever noe kunnskap om Certbot og dets DNS-plugins. Vennligst konsulter dokumentasjonen for de respektive pluginene." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Det finnes en server på dette domenet, men det ser ikke ut til å være Nginx Proxy Manager. Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Kunne ikke sjekke tilgjengeligheten på grunn av en kommunikasjonsfeil med site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Det finnes ingen server tilgjengelig på dette domenet. Vennligst sørg for at domenet ditt eksisterer og peker til IP-en der NPM-instansen kjører, og om nødvendig at port 80 er videresendt i ruteren din." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Serveren din er tilgjengelig, og det bør være mulig å opprette sertifikater." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Det finnes en server på dette domenet, men den returnerte en uventet statuskode {code}. Er det NPM-serveren? Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Det finnes en server på dette domenet, men den returnerte uventet data. Er det NPM-serveren? Vennligst sørg for at domenet ditt peker til IP-en der NPM-instansen kjører." + }, + "certificates.http.test-results": { + "defaultMessage": "Testresultater" + }, + "certificates.http.warning": { + "defaultMessage": "Disse domenene må allerede være konfigurert til å peke til denne installasjonen." + }, + "certificates.key-type": { + "defaultMessage": "Nøkkeltype" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA er bredt kompatibel, ECDSA er raskere og mer sikker, men støttes kanskje ikke av eldre systemer" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "med Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Be om et nytt sertifikat" + }, + "column.access": { + "defaultMessage": "Tilgang" + }, + "column.authorization": { + "defaultMessage": "Autorisasjon" + }, + "column.authorizations": { + "defaultMessage": "Autorisasjoner" + }, + "column.custom-locations": { + "defaultMessage": "Egendefinerte plasseringer" + }, + "column.destination": { + "defaultMessage": "Destinasjon" + }, + "column.details": { + "defaultMessage": "Detaljer" + }, + "column.email": { + "defaultMessage": "E-post" + }, + "column.event": { + "defaultMessage": "Hendelse" + }, + "column.expires": { + "defaultMessage": "Utløper" + }, + "column.http-code": { + "defaultMessage": "HTTP-kode" + }, + "column.incoming-port": { + "defaultMessage": "Innkommende port" + }, + "column.name": { + "defaultMessage": "Navn" + }, + "column.protocol": { + "defaultMessage": "Protokoll" + }, + "column.provider": { + "defaultMessage": "Leverandør" + }, + "column.roles": { + "defaultMessage": "Roller" + }, + "column.rules": { + "defaultMessage": "Regler" + }, + "column.satisfy": { + "defaultMessage": "Oppfylle" + }, + "column.satisfy-all": { + "defaultMessage": "Alle" + }, + "column.satisfy-any": { + "defaultMessage": "Noen" + }, + "column.scheme": { + "defaultMessage": "Skjema" + }, + "column.source": { + "defaultMessage": "Kilde" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Opprettet: {date}" + }, + "dashboard": { + "defaultMessage": "Dashboard" + }, + "dead-host": { + "defaultMessage": "404 Tjener ikke funnet" + }, + "dead-hosts": { + "defaultMessage": "404 Tjenere ikke funnet" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Tjener} other {404 Tjenere}}" + }, + "disabled": { + "defaultMessage": "Deaktivert" + }, + "domain-names": { + "defaultMessage": "Domener" + }, + "domain-names.max": { + "defaultMessage": "{count} domener maksimum" + }, + "domain-names.placeholder": { + "defaultMessage": "Begynn å skrive for å legge til domene..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards er ikke tillatt for denne typen" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards støttes ikke for denne CA-en" + }, + "domains.advanced": { + "defaultMessage": "Avansert" + }, + "domains.force-ssl": { + "defaultMessage": "Tving SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Aktivert" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Underdomener" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 Støtte" + }, + "domains.trust-forwarded-proto": { + "defaultMessage": "Stol på Upstream Forwarded Proto Headers" + }, + "domains.use-dns": { + "defaultMessage": "Bruk DNS Utfordring" + }, + "email-address": { + "defaultMessage": "E-postadresse" + }, + "empty-search": { + "defaultMessage": "Ingen resultater funnet" + }, + "empty-subtitle": { + "defaultMessage": "Hvorfor ikke opprette en?" + }, + "enabled": { + "defaultMessage": "Aktivert" + }, + "error.access.at-least-one": { + "defaultMessage": "Enten en autorisasjon eller en tilgangsregel er påkrevd" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Autorisasjonsbrukernavn må være unike" + }, + "error.invalid-auth": { + "defaultMessage": "Ugyldig e-post eller passord" + }, + "error.invalid-domain": { + "defaultMessage": "Ugyldig domene: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Ugyldig e-postadresse" + }, + "error.max-character-length": { + "defaultMessage": "Maksimal lengde er {max} tegn" + }, + "error.max-domains": { + "defaultMessage": "For mange domener, maks er {max}" + }, + "error.maximum": { + "defaultMessage": "Maksimum er {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimum lengde er {min} tegn" + }, + "error.minimum": { + "defaultMessage": "Minimum er {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Passordene må være like" + }, + "error.required": { + "defaultMessage": "Dette er påkrevd" + }, + "expires.on": { + "defaultMessage": "Utløper: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork meg på Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blokker vanlige utnyttelser" + }, + "host.flags.cache-assets": { + "defaultMessage": "Mellomlagre ressurser" + }, + "host.flags.preserve-path": { + "defaultMessage": "Behold sti" + }, + "host.flags.protocols": { + "defaultMessage": "Protokoller" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets-støtte" + }, + "host.forward-port": { + "defaultMessage": "Viderekoble Port" + }, + "host.forward-scheme": { + "defaultMessage": "Skjema" + }, + "hosts": { + "defaultMessage": "Vertsnavn" + }, + "http-only": { + "defaultMessage": "Kun HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Laster…" + }, + "login.2fa-code": { + "defaultMessage": "Verifikasjonskode" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Skriv inn kode" + }, + "login.2fa-description": { + "defaultMessage": "Skriv inn koden fra autentiseringsappen din" + }, + "login.2fa-title": { + "defaultMessage": "To-faktorautentisering" + }, + "login.2fa-verify": { + "defaultMessage": "Verifiser" + }, + "login.title": { + "defaultMessage": "Logg på kontoen din" + }, + "nginx-config.label": { + "defaultMessage": "Egendefinert Nginx-konfigurasjon" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Skriv inn din egendefinerte Nginx-konfigurasjon her på egen risiko!" + }, + "no-permission-error": { + "defaultMessage": "Du har ikke tilgang til å se dette." + }, + "notfound.action": { + "defaultMessage": "Ta meg hjem" + }, + "notfound.content": { + "defaultMessage": "Beklager, siden du leter etter ble ikke funnet" + }, + "notfound.title": { + "defaultMessage": "Oops… Du har nettopp funnet en feilsiden" + }, + "notification.error": { + "defaultMessage": "Feil" + }, + "notification.object-deleted": { + "defaultMessage": "{object} har blitt slettet" + }, + "notification.object-disabled": { + "defaultMessage": "{object} har blitt deaktivert" + }, + "notification.object-enabled": { + "defaultMessage": "{object} har blitt aktivert" + }, + "notification.object-renewed": { + "defaultMessage": "{object} har blitt fornyet" + }, + "notification.object-saved": { + "defaultMessage": "{object} har blitt lagret" + }, + "notification.success": { + "defaultMessage": "Suksess" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Legg til {object}" + }, + "object.delete": { + "defaultMessage": "Slett {object}" + }, + "object.delete.content": { + "defaultMessage": "Er du sikker på at du vil slette dette {object}?" + }, + "object.edit": { + "defaultMessage": "Rediger {object}" + }, + "object.empty": { + "defaultMessage": "Det finnes ingen {objects}" + }, + "object.event.created": { + "defaultMessage": "Opprettet {object}" + }, + "object.event.deleted": { + "defaultMessage": "Slettet {object}" + }, + "object.event.disabled": { + "defaultMessage": "Deaktivert {object}" + }, + "object.event.enabled": { + "defaultMessage": "Aktivert {object}" + }, + "object.event.renewed": { + "defaultMessage": "Fornyet {object}" + }, + "object.event.updated": { + "defaultMessage": "Oppdatert {object}" + }, + "offline": { + "defaultMessage": "Utilgjengelig" + }, + "online": { + "defaultMessage": "Tilgjengelig" + }, + "options": { + "defaultMessage": "Alternativer" + }, + "password": { + "defaultMessage": "Passord" + }, + "password.generate": { + "defaultMessage": "Generer tilfeldig passord" + }, + "password.hide": { + "defaultMessage": "Skjul passord" + }, + "password.show": { + "defaultMessage": "Vis passord" + }, + "permissions.hidden": { + "defaultMessage": "Skjult" + }, + "permissions.manage": { + "defaultMessage": "Administrer" + }, + "permissions.view": { + "defaultMessage": "Kun visning" + }, + "permissions.visibility.all": { + "defaultMessage": "Alle elementer" + }, + "permissions.visibility.title": { + "defaultMessage": "Element Synlighet" + }, + "permissions.visibility.user": { + "defaultMessage": "Kun opprettede elementer" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Forward Hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy-verter" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy-vert} other {Proxy-verter}}" + }, + "public": { + "defaultMessage": "Offentlig" + }, + "redirection-host": { + "defaultMessage": "Omdirigeringsvert" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Viderekoble domene" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP-kode" + }, + "redirection-hosts": { + "defaultMessage": "Omdirigeringsverter" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Omdirigeringsvert} other {Omdirigeringsverter}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Multiple Choices" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Flyttet permanent" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Flyttet midlertidig" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Se andre" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Midlertidig omdirigering" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Permanent omdirigering" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Standardbruker" + }, + "save": { + "defaultMessage": "Lagre" + }, + "setting": { + "defaultMessage": "Innstilling" + }, + "settings": { + "defaultMessage": "Innstillinger" + }, + "settings.default-site": { + "defaultMessage": "Standardnettsted" + }, + "settings.default-site.404": { + "defaultMessage": "404-side" + }, + "settings.default-site.444": { + "defaultMessage": "Ingen respons (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Gratulerer-side" + }, + "settings.default-site.description": { + "defaultMessage": "Hva som skal vises når Nginx treffes med en ukjent vert" + }, + "settings.default-site.html": { + "defaultMessage": "Egendefinert HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Omdiriger" + }, + "setup.preamble": { + "defaultMessage": "Kom i gang ved å opprette din administratorkonto." + }, + "setup.title": { + "defaultMessage": "Velkommen!" + }, + "sign-in": { + "defaultMessage": "Logg inn" + }, + "ssl-certificate": { + "defaultMessage": "SSL-sertifikat" + }, + "stream": { + "defaultMessage": "Strøm" + }, + "stream.forward-host": { + "defaultMessage": "Viderekoble vert" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com eller 10.0.0.1 eller 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Innkommende port" + }, + "streams": { + "defaultMessage": "Strømmer" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Strøm} other {Strømmer}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Oppdatering tilgjengelig: {latestVersion}" + }, + "user": { + "defaultMessage": "Bruker" + }, + "user.change-password": { + "defaultMessage": "Endre passord" + }, + "user.confirm-password": { + "defaultMessage": "Bekreft passord" + }, + "user.current-password": { + "defaultMessage": "Nåværende passord" + }, + "user.edit-profile": { + "defaultMessage": "Rediger profil" + }, + "user.full-name": { + "defaultMessage": "Fullt navn" + }, + "user.login-as": { + "defaultMessage": "Logg inn som {name}" + }, + "user.logout": { + "defaultMessage": "Logg ut" + }, + "user.new-password": { + "defaultMessage": "Nytt passord" + }, + "user.nickname": { + "defaultMessage": "Kallenavn" + }, + "user.set-password": { + "defaultMessage": "Angi passord" + }, + "user.set-permissions": { + "defaultMessage": "Angi tillatelser for {name}" + }, + "user.switch-dark": { + "defaultMessage": "Bytt til mørk modus" + }, + "user.switch-light": { + "defaultMessage": "Bytt til lys modus" + }, + "user.two-factor": { + "defaultMessage": "To-faktor autentisering" + }, + "username": { + "defaultMessage": "Brukernavn" + }, + "users": { + "defaultMessage": "Brukere" + } +} diff --git a/frontend/src/locale/src/pl.json b/frontend/src/locale/src/pl.json new file mode 100644 index 0000000..a5fb2ad --- /dev/null +++ b/frontend/src/locale/src/pl.json @@ -0,0 +1,662 @@ +{ + "access-list": { + "defaultMessage": "wpis listy dostępu" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Reguła} few {Reguły} other {Reguł}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Użytkownik} few {Użytkownicy} other {Użytkowników}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Gdy istnieje co najmniej 1 reguła, ta reguła blokująca wszystko zostanie dodana na końcu" + }, + "access-list.help.rules-order": { + "defaultMessage": "Należy pamiętać, że dyrektywy zezwolenia i odmowy będą stosowane w kolejności, w jakiej zostały zdefiniowane." + }, + "access-list.pass-auth": { + "defaultMessage": "Przekaż uwierzytelnienie do serwera docelowego" + }, + "access-list.public": { + "defaultMessage": "Publicznie dostępne" + }, + "access-list.public.subtitle": { + "defaultMessage": "Nie wymaga uwierzytelnienia podstawowego" + }, + "access-list.satisfy-any": { + "defaultMessage": "Spełnij dowolny warunek" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Użytkownik} few {Użytkowników} other {Użytkowników}}, {rules} {rules, plural, one {Reguła} few {Reguły} other {Reguł}} - Utworzono: {date}" + }, + "access-lists": { + "defaultMessage": "Listy dostępu" + }, + "action.add": { + "defaultMessage": "Dodaj" + }, + "action.add-location": { + "defaultMessage": "Dodaj lokalizację" + }, + "action.allow": { + "defaultMessage": "Zezwól" + }, + "action.close": { + "defaultMessage": "Zamknij" + }, + "action.delete": { + "defaultMessage": "Usuń" + }, + "action.deny": { + "defaultMessage": "Odrzuć" + }, + "action.disable": { + "defaultMessage": "Wyłącz" + }, + "action.download": { + "defaultMessage": "Pobierz" + }, + "action.edit": { + "defaultMessage": "Edytuj" + }, + "action.enable": { + "defaultMessage": "Włącz" + }, + "action.permissions": { + "defaultMessage": "Uprawnienia" + }, + "action.renew": { + "defaultMessage": "Odnów" + }, + "action.view-details": { + "defaultMessage": "Pokaż szczegóły" + }, + "auditlogs": { + "defaultMessage": "Logi" + }, + "cancel": { + "defaultMessage": "Anuluj" + }, + "certificate": { + "defaultMessage": "certyfikat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certyfikat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Klucz certyfikatu" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certyfikat pośredni" + }, + "certificate.in-use": { + "defaultMessage": "W użyciu" + }, + "certificate.none.subtitle": { + "defaultMessage": "Nie przypisano certyfikatu" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Ten host nie będzie używał HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Brak" + }, + "certificate.not-in-use": { + "defaultMessage": "Nie używany" + }, + "certificate.renew": { + "defaultMessage": "Odnów certyfikat" + }, + "certificates": { + "defaultMessage": "Certyfikaty" + }, + "certificates.custom": { + "defaultMessage": "Własny certyfikat" + }, + "certificates.custom.warning": { + "defaultMessage": "Pliki kluczy chronione hasłem nie są obsługiwane." + }, + "certificates.dns.credentials": { + "defaultMessage": "Zawartość pliku z poświadczeniami" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Ta wtyczka wymaga pliku konfiguracyjnego zawierającego token API lub inne poświadczenia dla twojego dostawcy" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Te dane zostaną zapisane jako zwykły tekst w bazie danych i pliku!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Sekundy propagacji" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Pozostaw puste, aby użyć domyślnej wartości wtyczki. Liczba sekund oczekiwania na propagację DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Dostawca DNS" + }, + "certificates.dns.warning": { + "defaultMessage": "Ta sekcja wymaga pewnej wiedzy na temat Certbot i jego wtyczek DNS. Zapoznaj się z dokumentacją odpowiednich wtyczek." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Znaleziono serwer pod tą domeną, ale nie wygląda na to, że jest to Nginx Proxy Manager. Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Nie udało się sprawdzić dostępności z powodu błędu komunikacji z site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Brak dostępnego serwera pod tą domeną. Upewnij się, że twoja domena istnieje i wskazuje na adres IP, gdzie działa twoja instancja NPM, oraz w razie potrzeby, że port 80 jest przekierowany w routerze lub owarty w firewall-u." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Twój serwer jest dostępny i tworzenie certyfikatów powinno być możliwe." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Znaleziono serwer pod tą domeną, ale zwrócił nieoczekiwany kod statusu {code}. Czy to serwer NPM? Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Znaleziono serwer pod tą domeną, ale zwrócił nieoczekiwane dane. Czy to serwer NPM? Upewnij się, że twoja domena wskazuje na adres IP, gdzie działa twoja instancja NPM." + }, + "certificates.http.test-results": { + "defaultMessage": "Wyniki testu" + }, + "certificates.http.warning": { + "defaultMessage": "Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer" + }, + "certificates.key-type": { + "defaultMessage": "Typ klucza" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA jest szeroko kompatybilny, ECDSA jest szybszy i bezpieczniejszy, ale może nie być obsługiwany przez starsze systemy" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "z Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Wygeneruj nowy certyfikat" + }, + "column.access": { + "defaultMessage": "Dostęp" + }, + "column.authorization": { + "defaultMessage": "Autoryzacja" + }, + "column.authorizations": { + "defaultMessage": "Autoryzacje" + }, + "column.custom-locations": { + "defaultMessage": "Własne ustawienia lokalizacji" + }, + "column.destination": { + "defaultMessage": "Cel" + }, + "column.details": { + "defaultMessage": "Szczegóły" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Zdarzenie" + }, + "column.expires": { + "defaultMessage": "Wygasa" + }, + "column.http-code": { + "defaultMessage": "Kod HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Port przychodzący" + }, + "column.name": { + "defaultMessage": "Nazwa" + }, + "column.protocol": { + "defaultMessage": "Protokół" + }, + "column.provider": { + "defaultMessage": "Dostawca" + }, + "column.roles": { + "defaultMessage": "Rola" + }, + "column.rules": { + "defaultMessage": "Reguły" + }, + "column.satisfy": { + "defaultMessage": "Spełnij" + }, + "column.satisfy-all": { + "defaultMessage": "Wszystkie" + }, + "column.satisfy-any": { + "defaultMessage": "Dowolny" + }, + "column.scheme": { + "defaultMessage": "Schemat" + }, + "column.source": { + "defaultMessage": "Źródło" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Status" + }, + "created-on": { + "defaultMessage": "Utworzono: {date}" + }, + "dashboard": { + "defaultMessage": "Panel" + }, + "dead-host": { + "defaultMessage": "host 404" + }, + "dead-hosts": { + "defaultMessage": "404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {host 404} few {hosty 404} other {hostów 404}}" + }, + "disabled": { + "defaultMessage": "Wyłączone" + }, + "domain-names": { + "defaultMessage": "Nazwy domen" + }, + "domain-names.max": { + "defaultMessage": "Maksymalnie {count} nazw domen" + }, + "domain-names.placeholder": { + "defaultMessage": "Zacznij pisać, aby dodać domenę..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Symbole wieloznaczne nie są dozwolone dla tego typu" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Symbole wieloznaczne nie są obsługiwane dla tego CA" + }, + "domains.force-ssl": { + "defaultMessage": "Wymuś SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "Włącz HSTS " + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS dla subdomen" + }, + "domains.http2-support": { + "defaultMessage": "Obsługa HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Użyj wyzwania DNS" + }, + "email-address": { + "defaultMessage": "Adres email" + }, + "empty-search": { + "defaultMessage": "Nie znaleziono wyników" + }, + "empty-subtitle": { + "defaultMessage": "Może utworzysz nowy?" + }, + "enabled": { + "defaultMessage": "Włączone" + }, + "error.access.at-least-one": { + "defaultMessage": "Wymagana jest co najmniej jedna autoryzacja lub jedna reguła dostępu" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Nazwy użytkowników autoryzacji muszą być unikalne" + }, + "error.invalid-auth": { + "defaultMessage": "Nieprawidłowy email lub hasło" + }, + "error.invalid-domain": { + "defaultMessage": "Nieprawidłowa domena: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Nieprawidłowy adres email" + }, + "error.max-character-length": { + "defaultMessage": "Maksymalna długość to {max} {max, plural, one {znak} few {znaki} other {znaków}}" + }, + "error.max-domains": { + "defaultMessage": "Zbyt wiele domen, maksimum to {max}" + }, + "error.maximum": { + "defaultMessage": "Maksimum to {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimalna długość to {min} {min, plural, one {znak} few {znaki} other {znaków}}" + }, + "error.minimum": { + "defaultMessage": "Minimum to {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Hasła muszą się zgadzać" + }, + "error.required": { + "defaultMessage": "To pole jest wymagane" + }, + "expires.on": { + "defaultMessage": "Wygasa: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forkuj mnie na Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blokuj typowe exploity" + }, + "host.flags.cache-assets": { + "defaultMessage": "Buforuj zasoby statyczne (ang. cache)" + }, + "host.flags.preserve-path": { + "defaultMessage": "Zachowaj ścieżkę" + }, + "host.flags.protocols": { + "defaultMessage": "Protokoły" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Obsługa WebSockets" + }, + "host.forward-port": { + "defaultMessage": "Port docelowy" + }, + "host.forward-scheme": { + "defaultMessage": "Schemat" + }, + "hosts": { + "defaultMessage": "Hosty" + }, + "http-only": { + "defaultMessage": "Tylko HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt przez DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt przez HTTP" + }, + "loading": { + "defaultMessage": "Ładowanie…" + }, + "login.title": { + "defaultMessage": "Zaloguj się na swoje konto" + }, + "nginx-config.label": { + "defaultMessage": "Własna konfiguracja Nginx" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Wprowadź tutaj własną konfigurację Nginx na własną odpowiedzialność!" + }, + "no-permission-error": { + "defaultMessage": "Nie masz uprawnień do wyświetlenia tego." + }, + "notfound.action": { + "defaultMessage": "Zabierz mnie do strony głównej" + }, + "notfound.content": { + "defaultMessage": "Przepraszamy, ale strona, której szukasz, nie została znaleziona" + }, + "notfound.title": { + "defaultMessage": "Ups… Właśnie znalazłeś stronę błędu" + }, + "notification.error": { + "defaultMessage": "Błąd" + }, + "notification.object-deleted": { + "defaultMessage": "{object} został usunięty" + }, + "notification.object-disabled": { + "defaultMessage": "{object} został wyłączony" + }, + "notification.object-enabled": { + "defaultMessage": "{object} został włączony" + }, + "notification.object-renewed": { + "defaultMessage": "{object} został odnowiony" + }, + "notification.object-saved": { + "defaultMessage": "{object} został zapisany" + }, + "notification.success": { + "defaultMessage": "Sukces" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Nowy {object}" + }, + "object.delete": { + "defaultMessage": "Usuń {object}" + }, + "object.delete.content": { + "defaultMessage": "Czy na pewno chcesz usunąć {object}?" + }, + "object.edit": { + "defaultMessage": "Edytuj {object}" + }, + "object.empty": { + "defaultMessage": "Brak {objects}" + }, + "object.event.created": { + "defaultMessage": "Utworzono {object}" + }, + "object.event.deleted": { + "defaultMessage": "Usunięto {object}" + }, + "object.event.disabled": { + "defaultMessage": "Wyłączono {object}" + }, + "object.event.enabled": { + "defaultMessage": "Włączono {object}" + }, + "object.event.renewed": { + "defaultMessage": "Odnowiono {object}" + }, + "object.event.updated": { + "defaultMessage": "Zaktualizowano {object}" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Opcje" + }, + "password": { + "defaultMessage": "Hasło" + }, + "password.generate": { + "defaultMessage": "Wygeneruj losowe hasło" + }, + "password.hide": { + "defaultMessage": "Ukryj hasło" + }, + "password.show": { + "defaultMessage": "Pokaż hasło" + }, + "permissions.hidden": { + "defaultMessage": "Ukryte" + }, + "permissions.manage": { + "defaultMessage": "Zarządzaj" + }, + "permissions.view": { + "defaultMessage": "Tylko podgląd" + }, + "permissions.visibility.all": { + "defaultMessage": "Wszystkie elementy" + }, + "permissions.visibility.title": { + "defaultMessage": "Widoczność elementów" + }, + "permissions.visibility.user": { + "defaultMessage": "Tylko utworzone elementy" + }, + "proxy-host": { + "defaultMessage": "host proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Przekieruj na hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {host proxy} few {hosty proxy} many {hostów proxy} other {hostów proxy}}" + }, + "public": { + "defaultMessage": "Publiczne" + }, + "redirection-host": { + "defaultMessage": "adres przekierowania" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Domena docelowa" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Kod HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Przekierowania" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {przekierowanie} few {przekierowania} many {przekierowań} other {przekierowań}}" + }, + "role.admin": { + "defaultMessage": "Administrator" + }, + "role.standard-user": { + "defaultMessage": "Standardowy użytkownik" + }, + "save": { + "defaultMessage": "Zapisz" + }, + "setting": { + "defaultMessage": "Ustawienie" + }, + "settings": { + "defaultMessage": "Ustawienia" + }, + "settings.default-site": { + "defaultMessage": "Domyślna strona" + }, + "settings.default-site.404": { + "defaultMessage": "Strona 404" + }, + "settings.default-site.444": { + "defaultMessage": "Brak odpowiedzi (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Strona gratulacyjna" + }, + "settings.default-site.description": { + "defaultMessage": "Co wyświetlić, gdy Nginx otrzyma nieznany Host" + }, + "settings.default-site.html": { + "defaultMessage": "Własny HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Przekierowanie" + }, + "setup.preamble": { + "defaultMessage": "Zacznij od utworzenia konta administratora." + }, + "setup.title": { + "defaultMessage": "Witaj!" + }, + "sign-in": { + "defaultMessage": "Zaloguj się" + }, + "ssl-certificate": { + "defaultMessage": "Certyfikat SSL" + }, + "stream": { + "defaultMessage": "strumień" + }, + "stream.forward-host": { + "defaultMessage": "Host docelowy" + }, + "stream.incoming-port": { + "defaultMessage": "Port przychodzący" + }, + "streams": { + "defaultMessage": "Strumienie" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {strumień} few {strumienie} many {strumieni} other {strumieni}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "user": { + "defaultMessage": "użytkownik" + }, + "user.change-password": { + "defaultMessage": "Zmień hasło" + }, + "user.confirm-password": { + "defaultMessage": "Potwierdź nowe hasło" + }, + "user.current-password": { + "defaultMessage": "Aktualne hasło" + }, + "user.edit-profile": { + "defaultMessage": "Edytuj profil" + }, + "user.full-name": { + "defaultMessage": "Imię / Nazwisko" + }, + "user.login-as": { + "defaultMessage": "Zaloguj jako {name}" + }, + "user.logout": { + "defaultMessage": "Wyloguj" + }, + "user.new-password": { + "defaultMessage": "Nowe hasło" + }, + "user.nickname": { + "defaultMessage": "Pseudonim" + }, + "user.set-password": { + "defaultMessage": "Ustaw hasło" + }, + "user.set-permissions": { + "defaultMessage": "Ustaw uprawnienia dla {name}" + }, + "user.switch-dark": { + "defaultMessage": "Przełącz na tryb ciemny" + }, + "user.switch-light": { + "defaultMessage": "Przełącz na tryb jasny" + }, + "username": { + "defaultMessage": "Nazwa użytkownika" + }, + "users": { + "defaultMessage": "Użytkownicy" + } +} diff --git a/frontend/src/locale/src/pt.json b/frontend/src/locale/src/pt.json new file mode 100644 index 0000000..0a789f4 --- /dev/null +++ b/frontend/src/locale/src/pt.json @@ -0,0 +1,683 @@ +{ + "access-list": { + "defaultMessage": "Lista de Controlo de Acesso (ACL)" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Regra} other {Regras}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Utilizador} other {Utilizadores}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Quando existir pelo menos 1 regra, esta regra de negação geral será aplicada em último lugar" + }, + "access-list.help.rules-order": { + "defaultMessage": "Nota: as diretivas allow e deny são aplicadas pela ordem em que forem definidas." + }, + "access-list.pass-auth": { + "defaultMessage": "Passar Autenticação para o Upstream" + }, + "access-list.public": { + "defaultMessage": "Acesso Público" + }, + "access-list.public.subtitle": { + "defaultMessage": "Sem autenticação básica" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 ou 192.168.1.0/24 ou 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Satisfazer Qualquer" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Utilizador} other {Utilizadores}}, {rules} {rules, plural, one {Regra} other {Regras}} – Criado em: {date}" + }, + "access-lists": { + "defaultMessage": "Listas de Controlo de Acesso (ACL)" + }, + "action.add": { + "defaultMessage": "Adicionar" + }, + "action.add-location": { + "defaultMessage": "Adicionar Location" + }, + "action.allow": { + "defaultMessage": "Permitir" + }, + "action.close": { + "defaultMessage": "Fechar" + }, + "action.delete": { + "defaultMessage": "Eliminar" + }, + "action.deny": { + "defaultMessage": "Negar" + }, + "action.disable": { + "defaultMessage": "Desativar" + }, + "action.download": { + "defaultMessage": "Descarregar" + }, + "action.edit": { + "defaultMessage": "Editar" + }, + "action.enable": { + "defaultMessage": "Ativar" + }, + "action.permissions": { + "defaultMessage": "Permissões" + }, + "action.renew": { + "defaultMessage": "Renovar" + }, + "action.view-details": { + "defaultMessage": "Ver Detalhes" + }, + "auditlogs": { + "defaultMessage": "Registos de Auditoria" + }, + "auto": { + "defaultMessage": "Automático" + }, + "cancel": { + "defaultMessage": "Cancelar" + }, + "certificate": { + "defaultMessage": "Certificado" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificado Personalizado" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Chave do Certificado" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certificado Intermédio" + }, + "certificate.in-use": { + "defaultMessage": "Em Utilização" + }, + "certificate.none.subtitle": { + "defaultMessage": "Nenhum certificado atribuído" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Este host não irá utilizar HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Nenhum" + }, + "certificate.not-in-use": { + "defaultMessage": "Não Utilizado" + }, + "certificate.renew": { + "defaultMessage": "Renovar Certificado" + }, + "certificates": { + "defaultMessage": "Certificados" + }, + "certificates.custom": { + "defaultMessage": "Certificado Personalizado" + }, + "certificates.custom.warning": { + "defaultMessage": "Ficheiros de chave protegidos por palavra-passe não são suportados." + }, + "certificates.dns.credentials": { + "defaultMessage": "Conteúdo do Ficheiro de Credenciais" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Este plugin requer um ficheiro de configuração contendo um token API ou outras credenciais do fornecedor DNS." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Estes dados serão guardados em texto simples na base de dados e num ficheiro!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Segundos de Propagação" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Deixe em branco para usar o valor predefinido do plugin. Número de segundos a aguardar pela propagação DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Fornecedor DNS" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Selecionar fornecedor..." + }, + "certificates.dns.warning": { + "defaultMessage": "Esta secção requer conhecimentos sobre o Certbot e os seus plugins DNS. Consulte a documentação dos plugins." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Foi encontrado um servidor neste domínio, mas não parece ser o Nginx Proxy Manager. Certifique-se de que o domínio aponta para o IP onde a sua instância está a correr." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Falha ao verificar acessibilidade devido a um erro de comunicação com site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Não existe nenhum servidor acessível neste domínio. Certifique-se de que o domínio existe, aponta para o IP correto e que a porta 80 está encaminhada no seu router." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "O servidor está acessível e a criação de certificados deverá ser possível." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Foi encontrado um servidor neste domínio, mas devolveu um código inesperado ({code}). Será o servidor NPM? Confirme que o domínio aponta para o IP correto." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Foi encontrado um servidor neste domínio, mas devolveu dados inesperados. Será o servidor NPM? Confirme que o domínio aponta para o IP correto." + }, + "certificates.http.test-results": { + "defaultMessage": "Resultados do Teste" + }, + "certificates.http.warning": { + "defaultMessage": "Estes domínios devem já estar configurados para apontar para esta instalação." + }, + "certificates.request.subtitle": { + "defaultMessage": "com o Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Pedir Novo Certificado" + }, + "column.access": { + "defaultMessage": "Acesso" + }, + "column.authorization": { + "defaultMessage": "Autorização" + }, + "column.authorizations": { + "defaultMessage": "Autorizações" + }, + "column.custom-locations": { + "defaultMessage": "Locations Personalizados" + }, + "column.destination": { + "defaultMessage": "Destino" + }, + "column.details": { + "defaultMessage": "Detalhes" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Evento" + }, + "column.expires": { + "defaultMessage": "Expira" + }, + "column.http-code": { + "defaultMessage": "Código HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Porta de Entrada" + }, + "column.name": { + "defaultMessage": "Nome" + }, + "column.protocol": { + "defaultMessage": "Protocolo" + }, + "column.provider": { + "defaultMessage": "Fornecedor" + }, + "column.roles": { + "defaultMessage": "Funções" + }, + "column.rules": { + "defaultMessage": "Regras" + }, + "column.satisfy": { + "defaultMessage": "Satisfazer" + }, + "column.satisfy-all": { + "defaultMessage": "Todos" + }, + "column.satisfy-any": { + "defaultMessage": "Qualquer" + }, + "column.scheme": { + "defaultMessage": "Esquema" + }, + "column.source": { + "defaultMessage": "Origem" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Estado" + }, + "created-on": { + "defaultMessage": "Criado em: {date}" + }, + "dashboard": { + "defaultMessage": "Painel" + }, + "dead-host": { + "defaultMessage": "Host 404" + }, + "dead-hosts": { + "defaultMessage": "Hosts 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host 404} other {Hosts 404}}" + }, + "disabled": { + "defaultMessage": "Desativado" + }, + "domain-names": { + "defaultMessage": "Nomes de Domínio" + }, + "domain-names.max": { + "defaultMessage": "Máximo de {count} domínios" + }, + "domain-names.placeholder": { + "defaultMessage": "Comece a escrever para adicionar um domínio..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards não permitidos para este tipo" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards não suportados por esta AC" + }, + "domains.force-ssl": { + "defaultMessage": "Forçar SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Ativado" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS para Subdomínios" + }, + "domains.http2-support": { + "defaultMessage": "Suporte HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Utilizar DNS Challenge" + }, + "email-address": { + "defaultMessage": "Endereço de Email" + }, + "empty-search": { + "defaultMessage": "Nenhum resultado encontrado" + }, + "empty-subtitle": { + "defaultMessage": "Porque não cria um?" + }, + "enabled": { + "defaultMessage": "Ativado" + }, + "error.access.at-least-one": { + "defaultMessage": "É necessária pelo menos uma Autorização ou uma Regra de Acesso" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Os nomes de utilizador de autorização devem ser únicos" + }, + "error.invalid-auth": { + "defaultMessage": "Email ou palavra-passe inválidos" + }, + "error.invalid-domain": { + "defaultMessage": "Domínio inválido: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Endereço de email inválido" + }, + "error.max-character-length": { + "defaultMessage": "Tamanho máximo: {max} caractere{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Demasiados domínios; o máximo é {max}" + }, + "error.maximum": { + "defaultMessage": "Máximo permitido: {max}" + }, + "error.min-character-length": { + "defaultMessage": "Tamanho mínimo: {min} caractere{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Mínimo permitido: {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "As palavras-passe têm de coincidir" + }, + "error.required": { + "defaultMessage": "Campo obrigatório" + }, + "expires.on": { + "defaultMessage": "Expira em: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Faz fork no GitHub" + }, + "host.flags.block-exploits": { + "defaultMessage": "Bloquear Exploits Comuns" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache de Conteúdos Estáticos" + }, + "host.flags.preserve-path": { + "defaultMessage": "Preservar Caminho" + }, + "host.flags.protocols": { + "defaultMessage": "Protocolos" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Suporte para WebSockets" + }, + "host.forward-port": { + "defaultMessage": "Porta de Encaminhamento" + }, + "host.forward-scheme": { + "defaultMessage": "Esquema" + }, + "hosts": { + "defaultMessage": "Hosts" + }, + "http-only": { + "defaultMessage": "Apenas HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "A carregar…" + }, + "login.title": { + "defaultMessage": "Iniciar sessão na sua conta" + }, + "nginx-config.label": { + "defaultMessage": "Configuração Nginx Personalizada" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Insira aqui a sua configuração Nginx personalizada (utilize por sua conta e risco!)" + }, + "no-permission-error": { + "defaultMessage": "Não tem permissões para ver esta página." + }, + "notfound.action": { + "defaultMessage": "Voltar à página inicial" + }, + "notfound.content": { + "defaultMessage": "A página que procura não foi encontrada." + }, + "notfound.title": { + "defaultMessage": "Oops… Encontrou uma página de erro" + }, + "notification.error": { + "defaultMessage": "Erro" + }, + "notification.object-deleted": { + "defaultMessage": "{object} foi eliminado" + }, + "notification.object-disabled": { + "defaultMessage": "{object} foi desativado" + }, + "notification.object-enabled": { + "defaultMessage": "{object} foi ativado" + }, + "notification.object-renewed": { + "defaultMessage": "{object} foi renovado" + }, + "notification.object-saved": { + "defaultMessage": "{object} foi guardado" + }, + "notification.success": { + "defaultMessage": "Sucesso" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Adicionar {object}" + }, + "object.delete": { + "defaultMessage": "Eliminar {object}" + }, + "object.delete.content": { + "defaultMessage": "Tem a certeza de que deseja eliminar este {object}?" + }, + "object.edit": { + "defaultMessage": "Editar {object}" + }, + "object.empty": { + "defaultMessage": "Não existem {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} criado" + }, + "object.event.deleted": { + "defaultMessage": "{object} eliminado" + }, + "object.event.disabled": { + "defaultMessage": "{object} desativado" + }, + "object.event.enabled": { + "defaultMessage": "{object} ativado" + }, + "object.event.renewed": { + "defaultMessage": "{object} renovado" + }, + "object.event.updated": { + "defaultMessage": "{object} atualizado" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Opções" + }, + "password": { + "defaultMessage": "Palavra-passe" + }, + "password.generate": { + "defaultMessage": "Gerar palavra-passe aleatória" + }, + "password.hide": { + "defaultMessage": "Esconder Palavra-passe" + }, + "password.show": { + "defaultMessage": "Mostrar Palavra-passe" + }, + "permissions.hidden": { + "defaultMessage": "Oculto" + }, + "permissions.manage": { + "defaultMessage": "Gerir" + }, + "permissions.view": { + "defaultMessage": "Apenas Visualização" + }, + "permissions.visibility.all": { + "defaultMessage": "Todos os Itens" + }, + "permissions.visibility.title": { + "defaultMessage": "Visibilidade do Item" + }, + "permissions.visibility.user": { + "defaultMessage": "Apenas Itens Criados" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "Hostname/IP de Encaminhamento" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Hosts" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}" + }, + "public": { + "defaultMessage": "Público" + }, + "redirection-host": { + "defaultMessage": "Host de Redirecionamento" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Domínio de Destino" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Código HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Hosts de Redirecionamento" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Host de Redirecionamento} other {Hosts de Redirecionamento}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Múltiplas Escolhas" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Movido Permanentemente" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Movido Temporariamente" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Ver Outro" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Redirecionamento Temporário" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Redirecionamento Permanente" + }, + "role.admin": { + "defaultMessage": "Administrador" + }, + "role.standard-user": { + "defaultMessage": "Utilizador Comum" + }, + "save": { + "defaultMessage": "Guardar" + }, + "setting": { + "defaultMessage": "Definição" + }, + "settings": { + "defaultMessage": "Definições" + }, + "settings.default-site": { + "defaultMessage": "Site Predefinido" + }, + "settings.default-site.404": { + "defaultMessage": "Página 404" + }, + "settings.default-site.444": { + "defaultMessage": "Sem Resposta (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Página de Boas-vindas" + }, + "settings.default-site.description": { + "defaultMessage": "O que apresentar quando o Nginx recebe um Host desconhecido" + }, + "settings.default-site.html": { + "defaultMessage": "HTML Personalizado" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirecionar" + }, + "setup.preamble": { + "defaultMessage": "Comece por criar a sua conta de administrador." + }, + "setup.title": { + "defaultMessage": "Bem-vindo!" + }, + "sign-in": { + "defaultMessage": "Iniciar Sessão" + }, + "ssl-certificate": { + "defaultMessage": "Certificado SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Host de Destino" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com ou 10.0.0.1 ou 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Porta de Entrada" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Testar" + }, + "update-available": { + "defaultMessage": "Atualização Disponível: {latestVersion}" + }, + "user": { + "defaultMessage": "Utilizador" + }, + "user.change-password": { + "defaultMessage": "Alterar Palavra-passe" + }, + "user.confirm-password": { + "defaultMessage": "Confirmar Palavra-passe" + }, + "user.current-password": { + "defaultMessage": "Palavra-passe Atual" + }, + "user.edit-profile": { + "defaultMessage": "Editar Perfil" + }, + "user.full-name": { + "defaultMessage": "Nome Completo" + }, + "user.login-as": { + "defaultMessage": "Iniciar sessão como {name}" + }, + "user.logout": { + "defaultMessage": "Terminar Sessão" + }, + "user.new-password": { + "defaultMessage": "Nova Palavra-passe" + }, + "user.nickname": { + "defaultMessage": "Alcunha" + }, + "user.set-password": { + "defaultMessage": "Definir Palavra-passe" + }, + "user.set-permissions": { + "defaultMessage": "Definir Permissões para {name}" + }, + "user.switch-dark": { + "defaultMessage": "Ativar Modo Escuro" + }, + "user.switch-light": { + "defaultMessage": "Ativar Modo Claro" + }, + "username": { + "defaultMessage": "Nome de Utilizador" + }, + "users": { + "defaultMessage": "Utilizadores" + } +} diff --git a/frontend/src/locale/src/ru.json b/frontend/src/locale/src/ru.json new file mode 100644 index 0000000..44dff12 --- /dev/null +++ b/frontend/src/locale/src/ru.json @@ -0,0 +1,656 @@ +{ + "access-list": { + "defaultMessage": "Список доступа" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {правило} few {правила} many {правил} other {правила}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {пользователь} few {пользователя} many {пользователей} other {пользователя}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Если есть хотя бы одно правило, правило 'запретить всё' будет добавлено последним" + }, + "access-list.help.rules-order": { + "defaultMessage": "Обратите внимание: разрешающие и запрещающие директивы применяются в порядке их определения." + }, + "access-list.pass-auth": { + "defaultMessage": "Передавать авторизацию на upstream-сервер" + }, + "access-list.public": { + "defaultMessage": "Общедоступный" + }, + "access-list.public.subtitle": { + "defaultMessage": "Без аутентификации" + }, + "access-list.satisfy-any": { + "defaultMessage": "Любое совпадение" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {пользователь} few {пользователя} many {пользователей} other {пользователя}}, {rules} {rules, plural, one {правило} few {правила} many {правил} other {правила}} - создан: {date}" + }, + "access-lists": { + "defaultMessage": "Списки доступа" + }, + "action.add": { + "defaultMessage": "Добавить" + }, + "action.add-location": { + "defaultMessage": "Добавить маршрут" + }, + "action.close": { + "defaultMessage": "Закрыть" + }, + "action.delete": { + "defaultMessage": "Удалить" + }, + "action.disable": { + "defaultMessage": "Выключить" + }, + "action.download": { + "defaultMessage": "Скачать" + }, + "action.edit": { + "defaultMessage": "Изменить" + }, + "action.enable": { + "defaultMessage": "Включить" + }, + "action.permissions": { + "defaultMessage": "Разрешения" + }, + "action.renew": { + "defaultMessage": "Продлить" + }, + "action.view-details": { + "defaultMessage": "Просмотреть сведения" + }, + "auditlogs": { + "defaultMessage": "Журнал аудита" + }, + "cancel": { + "defaultMessage": "Отменить" + }, + "certificate": { + "defaultMessage": "Сертификат" + }, + "certificate.custom-certificate": { + "defaultMessage": "Сертификат" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Ключ сертификата" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Промежуточный сертификат" + }, + "certificate.in-use": { + "defaultMessage": "Используется" + }, + "certificate.none.subtitle": { + "defaultMessage": "Сертификат не назначен" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Этот хост не будет использовать HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Нет" + }, + "certificate.not-in-use": { + "defaultMessage": "Не используется" + }, + "certificate.renew": { + "defaultMessage": "Продлить сертификат" + }, + "certificates": { + "defaultMessage": "Сертификаты" + }, + "certificates.custom": { + "defaultMessage": "Свой сертификат" + }, + "certificates.custom.warning": { + "defaultMessage": "Файлы ключей, защищённые паролем, не поддерживаются." + }, + "certificates.dns.credentials": { + "defaultMessage": "Содержимое файла учётных данных" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Этот плагин требует файл конфигурации, содержащий API-токен или другие учётные данные вашего провайдера" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Эти данные будут храниться в незашифрованном виде в базе данных и файле!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Ожидание распространения (сек.)" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Оставьте пустым для значения по умолчанию плагина. Секунды ожидания распространения DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS-провайдер" + }, + "certificates.dns.warning": { + "defaultMessage": "Этот раздел требует знаний о Certbot и его DNS-плагинах. Пожалуйста, обратитесь к документации соответствующих плагинов." + }, + "certificates.http.reachability-404": { + "defaultMessage": "На этом домене найден сервер, но, похоже, это не Nginx Proxy Manager. Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Не удалось проверить доступность из‑за ошибки связи с site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "На этом домене недоступен сервер. Убедитесь, что домен существует и указывает на IP-адрес, где запущен ваш экземпляр NPM, и при необходимости порт 80 проброшен на вашем роутере." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Сервер доступен, выпуск сертификатов возможен." + }, + "certificates.http.reachability-other": { + "defaultMessage": "На этом домене найден сервер, но он вернул неожиданный статус‑код {code}. Это сервер NPM? Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "На этом домене найден сервер, но он вернул неожиданные данные. Это сервер NPM? Убедитесь, что ваш домен указывает на IP-адрес, где запущен ваш экземпляр NPM." + }, + "certificates.http.test-results": { + "defaultMessage": "Результаты проверки" + }, + "certificates.http.warning": { + "defaultMessage": "Эти домены должны быть настроены и указывать на этот экземпляр." + }, + "certificates.key-type": { + "defaultMessage": "Тип ключа" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA широко совместим, ECDSA быстрее и безопаснее, но может не поддерживаться старыми системами" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "через Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Получить новый сертификат" + }, + "column.access": { + "defaultMessage": "Доступ" + }, + "column.authorization": { + "defaultMessage": "Авторизация" + }, + "column.authorizations": { + "defaultMessage": "Авторизации" + }, + "column.custom-locations": { + "defaultMessage": "Свои маршруты" + }, + "column.destination": { + "defaultMessage": "Назначение" + }, + "column.details": { + "defaultMessage": "Сведения" + }, + "column.email": { + "defaultMessage": "Эл. почта" + }, + "column.event": { + "defaultMessage": "Событие" + }, + "column.expires": { + "defaultMessage": "Истекает" + }, + "column.http-code": { + "defaultMessage": "HTTP-код" + }, + "column.incoming-port": { + "defaultMessage": "Входящий порт" + }, + "column.name": { + "defaultMessage": "Имя" + }, + "column.protocol": { + "defaultMessage": "Протокол" + }, + "column.provider": { + "defaultMessage": "Провайдер" + }, + "column.roles": { + "defaultMessage": "Роли" + }, + "column.rules": { + "defaultMessage": "Правила" + }, + "column.satisfy": { + "defaultMessage": "Условия" + }, + "column.satisfy-all": { + "defaultMessage": "Все" + }, + "column.satisfy-any": { + "defaultMessage": "Любое" + }, + "column.scheme": { + "defaultMessage": "Схема" + }, + "column.source": { + "defaultMessage": "Источник" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Статус" + }, + "created-on": { + "defaultMessage": "Создан: {date}" + }, + "dashboard": { + "defaultMessage": "Обзор" + }, + "dead-host": { + "defaultMessage": "404-хост" + }, + "dead-hosts": { + "defaultMessage": "404-хосты" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404-хост} few {404-хоста} many {404-хостов} other {404-хоста}}" + }, + "disabled": { + "defaultMessage": "Выключен" + }, + "domain-names": { + "defaultMessage": "Домены" + }, + "domain-names.max": { + "defaultMessage": "Максимум {count} доменов" + }, + "domain-names.placeholder": { + "defaultMessage": "Начните ввод, чтобы добавить домен..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Подстановочные домены не разрешены для этого типа" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Подстановочные домены не поддерживаются этим центром сертификации" + }, + "domains.force-ssl": { + "defaultMessage": "Всегда SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "Поддержка HSTS" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Поддомены HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Поддержка HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Проверка через DNS" + }, + "email-address": { + "defaultMessage": "Адрес эл. почты" + }, + "empty-search": { + "defaultMessage": "Ничего не найдено" + }, + "empty-subtitle": { + "defaultMessage": "Почему бы не создать его?" + }, + "enabled": { + "defaultMessage": "Включён" + }, + "error.access.at-least-one": { + "defaultMessage": "Требуется хотя бы одна авторизация или одно правило доступа" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Имена пользователей для авторизации должны быть уникальными" + }, + "error.invalid-auth": { + "defaultMessage": "Неверный адрес эл. почты или пароль" + }, + "error.invalid-domain": { + "defaultMessage": "Неверный домен: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Неверный адрес эл. почты" + }, + "error.max-character-length": { + "defaultMessage": "Максимальная длина {max} {max, plural, one {символ} few {символа} many {символов} other {символа}}" + }, + "error.max-domains": { + "defaultMessage": "Слишком много доменов, максимум {max}" + }, + "error.maximum": { + "defaultMessage": "Максимум {max}" + }, + "error.min-character-length": { + "defaultMessage": "Минимальная длина {min} {min, plural, one {символ} few {символа} many {символов} other {символа}}" + }, + "error.minimum": { + "defaultMessage": "Минимум {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Пароли должны совпадать" + }, + "error.required": { + "defaultMessage": "Обязательное поле" + }, + "expires.on": { + "defaultMessage": "Истекает: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Сделать форк на GitHub" + }, + "host.flags.block-exploits": { + "defaultMessage": "Блокировать известные эксплойты" + }, + "host.flags.cache-assets": { + "defaultMessage": "Кэшировать ресурсы" + }, + "host.flags.preserve-path": { + "defaultMessage": "Сохранять путь" + }, + "host.flags.protocols": { + "defaultMessage": "Протоколы" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Поддержка WebSocket" + }, + "host.forward-port": { + "defaultMessage": "Порт перенаправления" + }, + "host.forward-scheme": { + "defaultMessage": "Схема" + }, + "hosts": { + "defaultMessage": "Хосты" + }, + "http-only": { + "defaultMessage": "Только HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt через DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt через HTTP" + }, + "loading": { + "defaultMessage": "Загрузка…" + }, + "login.title": { + "defaultMessage": "Авторизация" + }, + "nginx-config.label": { + "defaultMessage": "Своя Nginx-конфигурация" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Введите здесь свою Nginx-конфигурацию, будьте осторожны!" + }, + "no-permission-error": { + "defaultMessage": "У вас нет доступа для просмотра." + }, + "notfound.action": { + "defaultMessage": "Вернуться на главную" + }, + "notfound.content": { + "defaultMessage": "Извините, но страница, которую вы ищете, не найдена" + }, + "notfound.title": { + "defaultMessage": "Упс… Вы попали на страницу ошибки" + }, + "notification.error": { + "defaultMessage": "Ошибка" + }, + "notification.object-deleted": { + "defaultMessage": "{object} удален" + }, + "notification.object-disabled": { + "defaultMessage": "{object} выключен" + }, + "notification.object-enabled": { + "defaultMessage": "{object} включен" + }, + "notification.object-renewed": { + "defaultMessage": "{object} продлен" + }, + "notification.object-saved": { + "defaultMessage": "{object} сохранен" + }, + "notification.success": { + "defaultMessage": "Успешно" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Добавить {object}" + }, + "object.delete": { + "defaultMessage": "Удалить {object}" + }, + "object.delete.content": { + "defaultMessage": "Вы уверены, что хотите удалить {object}?" + }, + "object.edit": { + "defaultMessage": "Изменить {object}" + }, + "object.empty": { + "defaultMessage": "{objects} отсутствуют" + }, + "object.event.created": { + "defaultMessage": "{object} создан" + }, + "object.event.deleted": { + "defaultMessage": "{object} удален" + }, + "object.event.disabled": { + "defaultMessage": "{object} выключен" + }, + "object.event.enabled": { + "defaultMessage": "{object} включен" + }, + "object.event.renewed": { + "defaultMessage": "{object} продлен" + }, + "object.event.updated": { + "defaultMessage": "{object} обновлен" + }, + "offline": { + "defaultMessage": "Офлайн" + }, + "online": { + "defaultMessage": "Онлайн" + }, + "options": { + "defaultMessage": "Параметры" + }, + "password": { + "defaultMessage": "Пароль" + }, + "password.generate": { + "defaultMessage": "Сгенерировать случайный пароль" + }, + "password.hide": { + "defaultMessage": "Скрыть пароль" + }, + "password.show": { + "defaultMessage": "Показать пароль" + }, + "permissions.hidden": { + "defaultMessage": "Скрыто" + }, + "permissions.manage": { + "defaultMessage": "Управление" + }, + "permissions.view": { + "defaultMessage": "Только просмотр" + }, + "permissions.visibility.all": { + "defaultMessage": "Все элементы" + }, + "permissions.visibility.title": { + "defaultMessage": "Видимость элементов" + }, + "permissions.visibility.user": { + "defaultMessage": "Созданные элементы" + }, + "proxy-host": { + "defaultMessage": "Прокси-хост" + }, + "proxy-host.forward-host": { + "defaultMessage": "Хост / IP перенаправления" + }, + "proxy-hosts": { + "defaultMessage": "Прокси-хосты" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {прокси-хост} few {прокси-хоста} many {прокси-хостов} other {прокси-хоста}}" + }, + "public": { + "defaultMessage": "Общедоступный" + }, + "redirection-host": { + "defaultMessage": "Редирект-хост" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Домен перенаправления" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP-код" + }, + "redirection-hosts": { + "defaultMessage": "Редирект-хосты" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {редирект-хост} few {редирект-хоста} many {редирект-хостов} other {редирект-хоста}}" + }, + "role.admin": { + "defaultMessage": "Администратор" + }, + "role.standard-user": { + "defaultMessage": "Обычный пользователь" + }, + "save": { + "defaultMessage": "Сохранить" + }, + "setting": { + "defaultMessage": "Настройка" + }, + "settings": { + "defaultMessage": "Настройки" + }, + "settings.default-site": { + "defaultMessage": "Страница по умолчанию" + }, + "settings.default-site.404": { + "defaultMessage": "404-страница" + }, + "settings.default-site.444": { + "defaultMessage": "Нет ответа (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Страница поздравления" + }, + "settings.default-site.description": { + "defaultMessage": "Что показывать, когда Nginx получает неизвестный хост" + }, + "settings.default-site.html": { + "defaultMessage": "Свой HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Перенаправление" + }, + "setup.preamble": { + "defaultMessage": "Начните с создания учётной записи администратора." + }, + "setup.title": { + "defaultMessage": "Добро пожаловать!" + }, + "sign-in": { + "defaultMessage": "Войти" + }, + "ssl-certificate": { + "defaultMessage": "SSL-сертификат" + }, + "stream": { + "defaultMessage": "Поток" + }, + "stream.forward-host": { + "defaultMessage": "Хост перенаправления" + }, + "stream.incoming-port": { + "defaultMessage": "Входящий порт" + }, + "streams": { + "defaultMessage": "Потоки" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {поток} few {потока} many {потоков} other {потока}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Проверить" + }, + "user": { + "defaultMessage": "Пользователь" + }, + "user.change-password": { + "defaultMessage": "Изменить пароль" + }, + "user.confirm-password": { + "defaultMessage": "Повторите пароль" + }, + "user.current-password": { + "defaultMessage": "Текущий пароль" + }, + "user.edit-profile": { + "defaultMessage": "Изменить профиль" + }, + "user.full-name": { + "defaultMessage": "Полное имя" + }, + "user.login-as": { + "defaultMessage": "Войти как {name}" + }, + "user.logout": { + "defaultMessage": "Выйти" + }, + "user.new-password": { + "defaultMessage": "Новый пароль" + }, + "user.nickname": { + "defaultMessage": "Псевдоним" + }, + "user.set-password": { + "defaultMessage": "Задать пароль" + }, + "user.set-permissions": { + "defaultMessage": "Задать разрешения для {name}" + }, + "user.switch-dark": { + "defaultMessage": "Включить тёмную тему" + }, + "user.switch-light": { + "defaultMessage": "Включить светлую тему" + }, + "username": { + "defaultMessage": "Имя пользователя" + }, + "users": { + "defaultMessage": "Пользователи" + } +} diff --git a/frontend/src/locale/src/sk.json b/frontend/src/locale/src/sk.json new file mode 100644 index 0000000..8d48cf8 --- /dev/null +++ b/frontend/src/locale/src/sk.json @@ -0,0 +1,770 @@ +{ + "2fa.backup-codes-remaining": { + "defaultMessage": "Počet zostávajúcich záložných kódov: {count}" + }, + "2fa.backup-warning": { + "defaultMessage": "Tieto záložné kódy si uložte na bezpečnom mieste. Každý kód je možné použiť len raz." + }, + "2fa.disable": { + "defaultMessage": "Vypnúť dvojfaktorové overovanie" + }, + "2fa.disable-confirm": { + "defaultMessage": "Vypnúť 2FA" + }, + "2fa.disable-warning": { + "defaultMessage": "Vypnutím dvojfaktorového overovania sa zníži bezpečnosť vášho účtu." + }, + "2fa.disabled": { + "defaultMessage": "Vypnuté" + }, + "2fa.done": { + "defaultMessage": "Uložil som si svoje záložné kódy." + }, + "2fa.enable": { + "defaultMessage": "Zapnúť dvojfaktorové overovanie" + }, + "2fa.enabled": { + "defaultMessage": "Zapnuté" + }, + "2fa.enter-code": { + "defaultMessage": "Zadajte overovací kód" + }, + "2fa.enter-code-disable": { + "defaultMessage": "Zadajte overovací kód na vypnutie" + }, + "2fa.regenerate": { + "defaultMessage": "Znova vytvoriť" + }, + "2fa.regenerate-backup": { + "defaultMessage": "Znova vytvoriť záložné kódy" + }, + "2fa.regenerate-instructions": { + "defaultMessage": "Zadajte overovací kód, aby sa vytvorili nové záložné kódy. Vaše staré kódy budú neplatné." + }, + "2fa.secret-key": { + "defaultMessage": "Tajný kľúč" + }, + "2fa.setup-instructions": { + "defaultMessage": "Naskenujte tento QR kód pomocou svojej overovacej aplikácie alebo zadajte tajný kľúč ručne." + }, + "2fa.status": { + "defaultMessage": "Stav" + }, + "2fa.title": { + "defaultMessage": "Dvojfaktorové overenie" + }, + "2fa.verify-enable": { + "defaultMessage": "Overiť a zapnúť" + }, + "access-list": { + "defaultMessage": "zoznam prístupov" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {pravidlo} few {pravidlá} other {pravidiel}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {používateľ} few {používatelia} other {používateľov}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "Keď existuje aspoň jedno pravidlo, toto pravidlo „zamietnuť všetko“ bude pridané ako posledné" + }, + "access-list.help.rules-order": { + "defaultMessage": "Upozornenie: pravidlá povoliť a zamietnuť budú uplatňované v poradí, v akom sú definované." + }, + "access-list.pass-auth": { + "defaultMessage": "Odoslať overenie na Upstream" + }, + "access-list.public": { + "defaultMessage": "Verejne prístupné" + }, + "access-list.public.subtitle": { + "defaultMessage": "Nie je potrebné základné overenie" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 alebo 192.168.1.0/24 alebo 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Splniť ktorékoľvek" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {používateľ} few {používatelia} other {používateľov}}, {rules} {rules, plural, one {pravidlo} few {pravidlá} other {pravidiel}} - Vytvorené: {date}" + }, + "access-lists": { + "defaultMessage": "Zoznamy prístupov" + }, + "action.add": { + "defaultMessage": "Pridať" + }, + "action.add-location": { + "defaultMessage": "Pridať umiestnenie" + }, + "action.allow": { + "defaultMessage": "Povoliť" + }, + "action.close": { + "defaultMessage": "Zavrieť" + }, + "action.delete": { + "defaultMessage": "Vymazať" + }, + "action.deny": { + "defaultMessage": "Zamietnuť" + }, + "action.disable": { + "defaultMessage": "Deaktivovať" + }, + "action.download": { + "defaultMessage": "Stiahnuť" + }, + "action.edit": { + "defaultMessage": "Upraviť" + }, + "action.enable": { + "defaultMessage": "Aktivovať" + }, + "action.permissions": { + "defaultMessage": "Oprávnenia" + }, + "action.renew": { + "defaultMessage": "Obnoviť" + }, + "action.view-details": { + "defaultMessage": "Zobraziť podrobnosti" + }, + "auditlogs": { + "defaultMessage": "Záznamy auditu" + }, + "auto": { + "defaultMessage": "Automaticky" + }, + "cancel": { + "defaultMessage": "Zrušiť" + }, + "certificate": { + "defaultMessage": "certifikát" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certifikát" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Kľúč certifikátu" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Sprostredkovateľský certifikát" + }, + "certificate.in-use": { + "defaultMessage": "Používa sa" + }, + "certificate.none.subtitle": { + "defaultMessage": "Nie je priradený žiadny certifikát" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Tento hostiteľ nebude používať HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Žiadny" + }, + "certificate.not-in-use": { + "defaultMessage": "Nepoužíva sa" + }, + "certificate.renew": { + "defaultMessage": "Obnoviť certifikát" + }, + "certificates": { + "defaultMessage": "Certifikáty" + }, + "certificates.custom": { + "defaultMessage": "Vlastný certifikát" + }, + "certificates.custom.warning": { + "defaultMessage": "Súbory kľúčov chránené heslom nie sú podporované." + }, + "certificates.dns.credentials": { + "defaultMessage": "Obsah súboru s prihlasovacími údajmi" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Tento doplnok vyžaduje konfiguračný súbor obsahujúci API token alebo iné prihlasovacie údaje vášho poskytovateľa" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Tieto údaje budú uložené v databáze a v súbore ako obyčajný text!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagácia v sekundách" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Ponechajte prázdne pre predvolenú hodnotu doplnku. Počet sekúnd, počas ktorých sa čaká na propagáciu DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS poskytovateľ" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Vyberte poskytovateľa..." + }, + "certificates.dns.warning": { + "defaultMessage": "Táto sekcia vyžaduje znalosť Certbotu a jeho DNS doplnkov. Prosím, pozrite si dokumentáciu príslušného doplnku." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Na tejto doméne bol nájdený server, ale nezdá sa, že ide o Nginx Proxy Manager. Uistite sa, že vaša doména smeruje na IP, kde beží vaša inštancia NPM." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Nepodarilo sa overiť dostupnosť kvôli chybe komunikácie so službou site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Na tejto doméne nie je dostupný žiadny server. Uistite sa, že doména existuje a smeruje na IP adresu s NPM a ak je to potrebné, port 80 je presmerovaný vo vašom smerovači." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Váš server je dostupný a vytvorenie certifikátu by malo byť možné." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Na tejto doméne bol nájdený server, ale vrátil neočakávaný stavový kód {code}. Je to NPM server? Uistite sa prosím, že doména smeruje na IP, kde beží vaša inštancia NPM." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Na tejto doméne bol nájdený server, ale vrátil neočakávané údaje. Je to NPM server? Uistite sa, že doména smeruje na IP, kde beží vaša inštancia NPM." + }, + "certificates.http.test-results": { + "defaultMessage": "Výsledky testu" + }, + "certificates.http.warning": { + "defaultMessage": "Tieto domény musia byť už nakonfigurované tak, aby smerovali na túto inštaláciu." + }, + "certificates.key-type": { + "defaultMessage": "Typ kľúča" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA je široko kompatibilný, ECDSA je rýchlejší a bezpečnejší, ale nemusí byť podporovaný staršími systémami" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "pomocou Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Vyžiadať nový certifikát" + }, + "column.access": { + "defaultMessage": "Prístup" + }, + "column.authorization": { + "defaultMessage": "Autorizácia" + }, + "column.authorizations": { + "defaultMessage": "Autorizácie" + }, + "column.custom-locations": { + "defaultMessage": "Vlastné umiestnenia" + }, + "column.destination": { + "defaultMessage": "Cieľ" + }, + "column.details": { + "defaultMessage": "Podrobnosti" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Udalosť" + }, + "column.expires": { + "defaultMessage": "Platnosť do" + }, + "column.http-code": { + "defaultMessage": "Prístup" + }, + "column.incoming-port": { + "defaultMessage": "Vstupný port" + }, + "column.name": { + "defaultMessage": "Názov" + }, + "column.protocol": { + "defaultMessage": "Protokol" + }, + "column.provider": { + "defaultMessage": "Poskytovateľ" + }, + "column.roles": { + "defaultMessage": "Roly" + }, + "column.rules": { + "defaultMessage": "Pravidlá" + }, + "column.satisfy": { + "defaultMessage": "Splniť" + }, + "column.satisfy-all": { + "defaultMessage": "Všetky" + }, + "column.satisfy-any": { + "defaultMessage": "Ktorékoľvek" + }, + "column.scheme": { + "defaultMessage": "Schéma" + }, + "column.source": { + "defaultMessage": "Zdroj" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Stav" + }, + "created-on": { + "defaultMessage": "Vytvorené: {date}" + }, + "dashboard": { + "defaultMessage": "Panel" + }, + "dead-host": { + "defaultMessage": "404 hostiteľa" + }, + "dead-hosts": { + "defaultMessage": "404 Hostitelia" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 hostiteľ} few {404 hostitelia} other {404 hostiteľov}}" + }, + "disabled": { + "defaultMessage": "Deaktivované" + }, + "domain-names": { + "defaultMessage": "Doménové mená" + }, + "domain-names.max": { + "defaultMessage": "Maximálne {count} doménových mien" + }, + "domain-names.placeholder": { + "defaultMessage": "Začnite písať pre pridanie domény..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Wildcards nie sú pre tento typ povolené" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Wildcards nie sú podporované pre túto certifikačnú autoritu" + }, + "domains.force-ssl": { + "defaultMessage": "Vynútiť SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS povolené" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS pre subdomény" + }, + "domains.http2-support": { + "defaultMessage": "Podpora HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Použiť DNS výzvu" + }, + "email-address": { + "defaultMessage": "Emailová adresa" + }, + "empty-search": { + "defaultMessage": "Nenašli sa žiadne výsledky" + }, + "empty-subtitle": { + "defaultMessage": "Prečo nevytvoríte nejaký?" + }, + "enabled": { + "defaultMessage": "Aktivované" + }, + "error.access.at-least-one": { + "defaultMessage": "Je vyžadovaná aspoň jedna autorizácia alebo jedno prístupové pravidlo" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Používateľské mená pre autorizáciu musia byť jedinečné" + }, + "error.invalid-auth": { + "defaultMessage": "Neplatný email alebo heslo" + }, + "error.invalid-domain": { + "defaultMessage": "Neplatná doména: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Neplatná emailová adresa" + }, + "error.max-character-length": { + "defaultMessage": "Maximálna dĺžka je {max} znak{max, plural, one {} few {y} other {ov}}" + }, + "error.max-domains": { + "defaultMessage": "Príliš veľa domén, maximum je {max}" + }, + "error.maximum": { + "defaultMessage": "Maximum je {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimálna dĺžka je {min} znak{min, plural, one {} few {y} other {ov}}" + }, + "error.minimum": { + "defaultMessage": "Minimum je {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Heslá sa musia zhodovať" + }, + "error.required": { + "defaultMessage": "Toto pole je povinné" + }, + "expires.on": { + "defaultMessage": "Platnosť do: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forknite ma na GitHube" + }, + "host.flags.block-exploits": { + "defaultMessage": "Blokovať bežné exploity" + }, + "host.flags.cache-assets": { + "defaultMessage": "Uložiť zdroje do vyrovnávacej pamäte" + }, + "host.flags.preserve-path": { + "defaultMessage": "Zachovať cestu" + }, + "host.flags.protocols": { + "defaultMessage": "Protokoly" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Podpora WebSockets" + }, + "host.forward-port": { + "defaultMessage": "Port presmerovania" + }, + "host.forward-scheme": { + "defaultMessage": "Schéma" + }, + "hosts": { + "defaultMessage": "Hostitelia" + }, + "http-only": { + "defaultMessage": "Len HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt cez DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt cez HTTP" + }, + "loading": { + "defaultMessage": "Načítava sa…" + }, + "login.2fa-code": { + "defaultMessage": "Overovací kód" + }, + "login.2fa-code-placeholder": { + "defaultMessage": "Vložiť kód" + }, + "login.2fa-description": { + "defaultMessage": "Vložte kód z vašej overovacej aplikácie" + }, + "login.2fa-title": { + "defaultMessage": "Dvoj-faktorové overenie" + }, + "login.2fa-verify": { + "defaultMessage": "Overiť" + }, + "login.title": { + "defaultMessage": "Prihláste sa do svojho účtu" + }, + "nginx-config.label": { + "defaultMessage": "Vlastná Nginx konfigurácia" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Zadajte vlastnú Nginx konfiguráciu na vlastné riziko!" + }, + "no-permission-error": { + "defaultMessage": "Nemáte oprávnenie na zobrazenie tohto obsahu." + }, + "notfound.action": { + "defaultMessage": "Späť na hlavnú stránku" + }, + "notfound.content": { + "defaultMessage": "Ľutujeme, stránka, ktorú hľadáte, nebola nájdená" + }, + "notfound.title": { + "defaultMessage": "Ups… Našli ste chybovú stránku" + }, + "notification.error": { + "defaultMessage": "Chyba" + }, + "notification.object-deleted": { + "defaultMessage": "{object} bol odstránený" + }, + "notification.object-disabled": { + "defaultMessage": "{object} bol deaktivovaný" + }, + "notification.object-enabled": { + "defaultMessage": "{object} bol aktivovaný" + }, + "notification.object-renewed": { + "defaultMessage": "{object} bol obnovený" + }, + "notification.object-saved": { + "defaultMessage": "{object} bol uložený" + }, + "notification.success": { + "defaultMessage": "Úspech" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Pridať {object}" + }, + "object.delete": { + "defaultMessage": "Vymazať {object}" + }, + "object.delete.content": { + "defaultMessage": "Naozaj chcete vymazať tento {object}?" + }, + "object.edit": { + "defaultMessage": "Upraviť {object}" + }, + "object.empty": { + "defaultMessage": "Nie sú {objects}" + }, + "object.event.created": { + "defaultMessage": "Vytvorený {object}" + }, + "object.event.deleted": { + "defaultMessage": "Vymazaný {object}" + }, + "object.event.disabled": { + "defaultMessage": "Deaktivovaný {object}" + }, + "object.event.enabled": { + "defaultMessage": "Aktivovaný {object}" + }, + "object.event.renewed": { + "defaultMessage": "Obnovený {object}" + }, + "object.event.updated": { + "defaultMessage": "Aktualizovaný {object}" + }, + "offline": { + "defaultMessage": "Offline" + }, + "online": { + "defaultMessage": "Online" + }, + "options": { + "defaultMessage": "Možnosti" + }, + "password": { + "defaultMessage": "Heslo" + }, + "password.generate": { + "defaultMessage": "Vygenerovať náhodné heslo" + }, + "password.hide": { + "defaultMessage": "Skryť heslo" + }, + "password.show": { + "defaultMessage": "Zobraziť heslo" + }, + "permissions.hidden": { + "defaultMessage": "Skryté" + }, + "permissions.manage": { + "defaultMessage": "Spravovať" + }, + "permissions.view": { + "defaultMessage": "Len na zobrazenie" + }, + "permissions.visibility.all": { + "defaultMessage": "Všetky položky" + }, + "permissions.visibility.title": { + "defaultMessage": "Viditeľnosť položky" + }, + "permissions.visibility.user": { + "defaultMessage": "Len vytvorené položky" + }, + "proxy-host": { + "defaultMessage": "proxy hostiteľa" + }, + "proxy-host.forward-host": { + "defaultMessage": "Cieľový názov hostiteľa / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy hostitelia" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {proxy hostiteľ} few {proxy hostitelia} other {proxy hostiteľov}}" + }, + "public": { + "defaultMessage": "Verejné" + }, + "redirection-host": { + "defaultMessage": "presmerovacieho hostiteľa" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Cieľová doména" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP kód" + }, + "redirection-hosts": { + "defaultMessage": "Presmerovací hostitelia" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {presmerovací hostiteľ} few {presmerovací hostitelia} other {presmerovacích hostiteľov}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Viacero možností" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Trvalo presunuté" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Dočasne presunuté" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Pozrieť iné" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Dočasné presmerovanie" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Trvalé presmerovanie" + }, + "role.admin": { + "defaultMessage": "Administrátor" + }, + "role.standard-user": { + "defaultMessage": "Bežný používateľ" + }, + "save": { + "defaultMessage": "Uložiť" + }, + "setting": { + "defaultMessage": "Nastavenie" + }, + "settings": { + "defaultMessage": "Nastavenia" + }, + "settings.default-site": { + "defaultMessage": "Predvolená stránka" + }, + "settings.default-site.404": { + "defaultMessage": "Stránka 404" + }, + "settings.default-site.444": { + "defaultMessage": "Bez odpovede (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Gratulačná stránka" + }, + "settings.default-site.description": { + "defaultMessage": "Čo zobraziť, keď Nginx zachytí neznámeho hostiteľa" + }, + "settings.default-site.html": { + "defaultMessage": "Vlastné HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Presmerovať" + }, + "setup.preamble": { + "defaultMessage": "Začnite vytvorením administrátorského účtu." + }, + "setup.title": { + "defaultMessage": "Vitajte!" + }, + "sign-in": { + "defaultMessage": "Prihlásiť sa" + }, + "ssl-certificate": { + "defaultMessage": "SSL certifikát" + }, + "stream": { + "defaultMessage": "stream" + }, + "stream.forward-host": { + "defaultMessage": "Cieľový hostiteľ" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "napriklad.sk alebo 10.0.0.1 alebo 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Vstupný port" + }, + "streams": { + "defaultMessage": "Streamy" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {stream} few {streamy} other {streamov}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Dostupná aktualizácia: {latestVersion}" + }, + "user": { + "defaultMessage": "používateľa" + }, + "user.change-password": { + "defaultMessage": "Zmeniť heslo" + }, + "user.confirm-password": { + "defaultMessage": "Potvrdiť heslo" + }, + "user.current-password": { + "defaultMessage": "Aktuálne heslo" + }, + "user.edit-profile": { + "defaultMessage": "Upraviť profil" + }, + "user.full-name": { + "defaultMessage": "Celé meno" + }, + "user.login-as": { + "defaultMessage": "Prihlásiť sa ako {name}" + }, + "user.logout": { + "defaultMessage": "Odhlásiť sa" + }, + "user.new-password": { + "defaultMessage": "Nové heslo" + }, + "user.nickname": { + "defaultMessage": "Prezývka" + }, + "user.set-password": { + "defaultMessage": "Nastaviť heslo" + }, + "user.set-permissions": { + "defaultMessage": "Nastaviť oprávnenia pre {name}" + }, + "user.switch-dark": { + "defaultMessage": "Prepnúť na tmavý režim" + }, + "user.switch-light": { + "defaultMessage": "Prepnúť na svetlý režim" + }, + "user.two-factor": { + "defaultMessage": "Dvojfakt. overenie" + }, + "username": { + "defaultMessage": "Používateľské meno" + }, + "users": { + "defaultMessage": "Používatelia" + } +} diff --git a/frontend/src/locale/src/tr.json b/frontend/src/locale/src/tr.json new file mode 100644 index 0000000..972fa89 --- /dev/null +++ b/frontend/src/locale/src/tr.json @@ -0,0 +1,683 @@ +{ + "access-list": { + "defaultMessage": "Erişim Listesi" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Kural} other {Kural}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Kullanıcı} other {Kullanıcı}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "En az 1 kural mevcut olduğunda, bu tümünü reddet kuralı en son eklenir" + }, + "access-list.help.rules-order": { + "defaultMessage": "İzin ver ve reddet direktiflerinin tanımlandıkları sırayla uygulanacağını unutmayın." + }, + "access-list.pass-auth": { + "defaultMessage": "Kimlik Doğrulamayı Yukarı Akışa İlet" + }, + "access-list.public": { + "defaultMessage": "Herkese Açık" + }, + "access-list.public.subtitle": { + "defaultMessage": "Temel kimlik doğrulama gerekmez" + }, + "access-list.rule-source.placeholder": { + "defaultMessage": "192.168.1.100 veya 192.168.1.0/24 veya 2001:0db8::/32" + }, + "access-list.satisfy-any": { + "defaultMessage": "Herhangi Birini Karşıla" + }, + "access-list.subtitle": { + "defaultMessage": "{users} {users, plural, one {Kullanıcı} other {Kullanıcı}}, {rules} {rules, plural, one {Kural} other {Kural}} - Oluşturuldu: {date}" + }, + "access-lists": { + "defaultMessage": "Erişim Listeleri" + }, + "action.add": { + "defaultMessage": "Ekle" + }, + "action.add-location": { + "defaultMessage": "Konum Ekle" + }, + "action.allow": { + "defaultMessage": "İzin Ver" + }, + "action.close": { + "defaultMessage": "Kapat" + }, + "action.delete": { + "defaultMessage": "Sil" + }, + "action.deny": { + "defaultMessage": "Reddet" + }, + "action.disable": { + "defaultMessage": "Devre Dışı Bırak" + }, + "action.download": { + "defaultMessage": "İndir" + }, + "action.edit": { + "defaultMessage": "Düzenle" + }, + "action.enable": { + "defaultMessage": "Etkinleştir" + }, + "action.permissions": { + "defaultMessage": "İzinler" + }, + "action.renew": { + "defaultMessage": "Yenile" + }, + "action.view-details": { + "defaultMessage": "Detayları Görüntüle" + }, + "auditlogs": { + "defaultMessage": "Denetim Kayıtları" + }, + "auto": { + "defaultMessage": "Otomatik" + }, + "cancel": { + "defaultMessage": "İptal" + }, + "certificate": { + "defaultMessage": "Sertifika" + }, + "certificate.custom-certificate": { + "defaultMessage": "Sertifika" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Sertifika Anahtarı" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Ara Sertifika" + }, + "certificate.in-use": { + "defaultMessage": "Kullanımda" + }, + "certificate.none.subtitle": { + "defaultMessage": "Sertifika atanmamış" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Bu host HTTPS kullanmayacak" + }, + "certificate.none.title": { + "defaultMessage": "Yok" + }, + "certificate.not-in-use": { + "defaultMessage": "Kullanılmıyor" + }, + "certificate.renew": { + "defaultMessage": "Sertifikayı Yenile" + }, + "certificates": { + "defaultMessage": "Sertifikalar" + }, + "certificates.custom": { + "defaultMessage": "Özel Sertifika" + }, + "certificates.custom.warning": { + "defaultMessage": "Parola ile korumalı anahtar dosyaları desteklenmiyor." + }, + "certificates.dns.credentials": { + "defaultMessage": "Kimlik Bilgileri Dosya İçeriği" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Bu eklenti, sağlayıcınız için bir API token'ı veya diğer kimlik bilgilerini içeren bir yapılandırma dosyası gerektirir" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Bu veriler veritabanında ve bir dosyada düz metin olarak saklanacak!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Yayılma Saniyesi" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Eklentinin varsayılan değerini kullanmak için boş bırakın. DNS yayılması için beklenilecek saniye sayısı." + }, + "certificates.dns.provider": { + "defaultMessage": "DNS Sağlayıcı" + }, + "certificates.dns.provider.placeholder": { + "defaultMessage": "Bir Sağlayıcı Seçin..." + }, + "certificates.dns.warning": { + "defaultMessage": "Bu bölüm Certbot ve DNS eklentileri hakkında bazı bilgiler gerektirir. Lütfen ilgili eklenti dokümantasyonuna bakın." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Bu alan adında bir sunucu bulundu ancak Nginx Proxy Manager gibi görünmüyor. Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "site24x7.com ile iletişim hatası nedeniyle erişilebilirlik kontrolü başarısız oldu." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Bu alan adında kullanılabilir bir sunucu yok. Lütfen alan adınızın mevcut olduğundan ve NPM örneğinizin çalıştığı IP'ye işaret ettiğinden ve gerekirse yönlendiricinizde 80 portunun yönlendirildiğinden emin olun." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Sunucunuz erişilebilir ve sertifika oluşturma mümkün olmalı." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Bu alan adında bir sunucu bulundu ancak beklenmeyen bir durum kodu döndürdü {code}. Bu NPM sunucusu mu? Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Bu alan adında bir sunucu bulundu ancak beklenmeyen veri döndürdü. Bu NPM sunucusu mu? Lütfen alan adınızın NPM örneğinizin çalıştığı IP'ye işaret ettiğinden emin olun." + }, + "certificates.http.test-results": { + "defaultMessage": "Test Sonuçları" + }, + "certificates.http.warning": { + "defaultMessage": "Bu alan adları zaten bu kuruluma işaret edecek şekilde yapılandırılmış olmalıdır." + }, + "certificates.request.subtitle": { + "defaultMessage": "Let's Encrypt ile" + }, + "certificates.request.title": { + "defaultMessage": "Yeni Sertifika İste" + }, + "column.access": { + "defaultMessage": "Erişim" + }, + "column.authorization": { + "defaultMessage": "Yetkilendirme" + }, + "column.authorizations": { + "defaultMessage": "Yetkilendirmeler" + }, + "column.custom-locations": { + "defaultMessage": "Özel Konumlar" + }, + "column.destination": { + "defaultMessage": "Hedef" + }, + "column.details": { + "defaultMessage": "Detaylar" + }, + "column.email": { + "defaultMessage": "E-posta" + }, + "column.event": { + "defaultMessage": "Olay" + }, + "column.expires": { + "defaultMessage": "Sona Erer" + }, + "column.http-code": { + "defaultMessage": "HTTP Kodu" + }, + "column.incoming-port": { + "defaultMessage": "Gelen Port" + }, + "column.name": { + "defaultMessage": "Ad" + }, + "column.protocol": { + "defaultMessage": "Protokol" + }, + "column.provider": { + "defaultMessage": "Sağlayıcı" + }, + "column.roles": { + "defaultMessage": "Roller" + }, + "column.rules": { + "defaultMessage": "Kurallar" + }, + "column.satisfy": { + "defaultMessage": "Karşıla" + }, + "column.satisfy-all": { + "defaultMessage": "Tümü" + }, + "column.satisfy-any": { + "defaultMessage": "Herhangi Biri" + }, + "column.scheme": { + "defaultMessage": "Şema" + }, + "column.source": { + "defaultMessage": "Kaynak" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Durum" + }, + "created-on": { + "defaultMessage": "Oluşturuldu: {date}" + }, + "dashboard": { + "defaultMessage": "Kontrol Paneli" + }, + "dead-host": { + "defaultMessage": "404 Host" + }, + "dead-hosts": { + "defaultMessage": "404 Host'lar" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {404 Host} other {404 Host}}" + }, + "disabled": { + "defaultMessage": "Devre Dışı" + }, + "domain-names": { + "defaultMessage": "Alan Adları" + }, + "domain-names.max": { + "defaultMessage": "Maksimum {count} alan adı" + }, + "domain-names.placeholder": { + "defaultMessage": "Alan adı eklemek için yazmaya başlayın..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Bu tür için joker karakterler izin verilmez" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Bu CA için joker karakterler desteklenmiyor" + }, + "domains.force-ssl": { + "defaultMessage": "SSL'i Zorla" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS Etkin" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS Alt Alan Adları" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 Desteği" + }, + "domains.use-dns": { + "defaultMessage": "DNS Challenge Kullan" + }, + "email-address": { + "defaultMessage": "E-posta adresi" + }, + "empty-search": { + "defaultMessage": "Sonuç bulunamadı" + }, + "empty-subtitle": { + "defaultMessage": "Neden bir tane oluşturmuyorsunuz?" + }, + "enabled": { + "defaultMessage": "Etkin" + }, + "error.access.at-least-one": { + "defaultMessage": "Ya bir Yetkilendirme ya da bir Erişim Kuralı gereklidir" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Yetkilendirme Kullanıcı Adları benzersiz olmalıdır" + }, + "error.invalid-auth": { + "defaultMessage": "Geçersiz e-posta veya şifre" + }, + "error.invalid-domain": { + "defaultMessage": "Geçersiz alan adı: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Geçersiz e-posta adresi" + }, + "error.max-character-length": { + "defaultMessage": "Maksimum uzunluk {max} karakter{max, plural, one {} other {}}" + }, + "error.max-domains": { + "defaultMessage": "Çok fazla alan adı, maksimum {max}" + }, + "error.maximum": { + "defaultMessage": "Maksimum {max}" + }, + "error.min-character-length": { + "defaultMessage": "Minimum uzunluk {min} karakter{min, plural, one {} other {}}" + }, + "error.minimum": { + "defaultMessage": "Minimum {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Şifreler eşleşmelidir" + }, + "error.required": { + "defaultMessage": "Bu gereklidir" + }, + "expires.on": { + "defaultMessage": "Sona Erer: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Github'da Fork Yap" + }, + "host.flags.block-exploits": { + "defaultMessage": "Yaygın Saldırıları Engelle" + }, + "host.flags.cache-assets": { + "defaultMessage": "Varlıkları Önbelleğe Al" + }, + "host.flags.preserve-path": { + "defaultMessage": "Yolu Koru" + }, + "host.flags.protocols": { + "defaultMessage": "Protokoller" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets Desteği" + }, + "host.forward-port": { + "defaultMessage": "İletme Portu" + }, + "host.forward-scheme": { + "defaultMessage": "Şema" + }, + "hosts": { + "defaultMessage": "Host'lar" + }, + "http-only": { + "defaultMessage": "Sadece HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "DNS ile Let's Encrypt" + }, + "lets-encrypt-via-http": { + "defaultMessage": "HTTP ile Let's Encrypt" + }, + "loading": { + "defaultMessage": "Yükleniyor…" + }, + "login.title": { + "defaultMessage": "Hesabınıza giriş yapın" + }, + "nginx-config.label": { + "defaultMessage": "Özel Nginx Yapılandırması" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Kendi riskinizle özel Nginx yapılandırmanızı buraya girin!" + }, + "no-permission-error": { + "defaultMessage": "Bunu görüntüleme erişiminiz yok." + }, + "notfound.action": { + "defaultMessage": "Ana sayfaya götür" + }, + "notfound.content": { + "defaultMessage": "Üzgünüz, aradığınız sayfa bulunamadı" + }, + "notfound.title": { + "defaultMessage": "Hata… Bir hata sayfası buldunuz" + }, + "notification.error": { + "defaultMessage": "Hata" + }, + "notification.object-deleted": { + "defaultMessage": "{object} silindi" + }, + "notification.object-disabled": { + "defaultMessage": "{object} devre dışı bırakıldı" + }, + "notification.object-enabled": { + "defaultMessage": "{object} etkinleştirildi" + }, + "notification.object-renewed": { + "defaultMessage": "{object} yenilendi" + }, + "notification.object-saved": { + "defaultMessage": "{object} kaydedildi" + }, + "notification.success": { + "defaultMessage": "Başarılı" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "{object} Ekle" + }, + "object.delete": { + "defaultMessage": "{object} Sil" + }, + "object.delete.content": { + "defaultMessage": "Bu {object} öğesini silmek istediğinizden emin misiniz?" + }, + "object.edit": { + "defaultMessage": "{object} Düzenle" + }, + "object.empty": { + "defaultMessage": "Hiç {objects} yok" + }, + "object.event.created": { + "defaultMessage": "{object} oluşturuldu" + }, + "object.event.deleted": { + "defaultMessage": "{object} silindi" + }, + "object.event.disabled": { + "defaultMessage": "{object} devre dışı bırakıldı" + }, + "object.event.enabled": { + "defaultMessage": "{object} etkinleştirildi" + }, + "object.event.renewed": { + "defaultMessage": "{object} yenilendi" + }, + "object.event.updated": { + "defaultMessage": "{object} güncellendi" + }, + "offline": { + "defaultMessage": "Çevrimdışı" + }, + "online": { + "defaultMessage": "Çevrimiçi" + }, + "options": { + "defaultMessage": "Seçenekler" + }, + "password": { + "defaultMessage": "Şifre" + }, + "password.generate": { + "defaultMessage": "Rastgele şifre oluştur" + }, + "password.hide": { + "defaultMessage": "Şifreyi Gizle" + }, + "password.show": { + "defaultMessage": "Şifreyi Göster" + }, + "permissions.hidden": { + "defaultMessage": "Gizli" + }, + "permissions.manage": { + "defaultMessage": "Yönet" + }, + "permissions.view": { + "defaultMessage": "Sadece Görüntüle" + }, + "permissions.visibility.all": { + "defaultMessage": "Tüm Öğeler" + }, + "permissions.visibility.title": { + "defaultMessage": "Öğe Görünürlüğü" + }, + "permissions.visibility.user": { + "defaultMessage": "Sadece Oluşturulan Öğeler" + }, + "proxy-host": { + "defaultMessage": "Proxy Host" + }, + "proxy-host.forward-host": { + "defaultMessage": "İletme Host Adı / IP" + }, + "proxy-hosts": { + "defaultMessage": "Proxy Host'lar" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Host}}" + }, + "public": { + "defaultMessage": "Herkese Açık" + }, + "redirection-host": { + "defaultMessage": "Yönlendirme Host'u" + }, + "redirection-host.forward-domain": { + "defaultMessage": "İletme Alan Adı" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP Kodu" + }, + "redirection-hosts": { + "defaultMessage": "Yönlendirme Host'ları" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Yönlendirme Host'u} other {Yönlendirme Host'u}}" + }, + "redirection-hosts.http-code.300": { + "defaultMessage": "300 Çoklu Seçenek" + }, + "redirection-hosts.http-code.301": { + "defaultMessage": "301 Kalıcı olarak taşındı" + }, + "redirection-hosts.http-code.302": { + "defaultMessage": "302 Geçici olarak taşındı" + }, + "redirection-hosts.http-code.303": { + "defaultMessage": "303 Diğerini gör" + }, + "redirection-hosts.http-code.307": { + "defaultMessage": "307 Geçici yönlendirme" + }, + "redirection-hosts.http-code.308": { + "defaultMessage": "308 Kalıcı yönlendirme" + }, + "role.admin": { + "defaultMessage": "Yönetici" + }, + "role.standard-user": { + "defaultMessage": "Standart Kullanıcı" + }, + "save": { + "defaultMessage": "Kaydet" + }, + "setting": { + "defaultMessage": "Ayar" + }, + "settings": { + "defaultMessage": "Ayarlar" + }, + "settings.default-site": { + "defaultMessage": "Varsayılan Site" + }, + "settings.default-site.404": { + "defaultMessage": "404 Sayfası" + }, + "settings.default-site.444": { + "defaultMessage": "Yanıt Yok (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Tebrikler Sayfası" + }, + "settings.default-site.description": { + "defaultMessage": "Nginx bilinmeyen bir Host ile karşılaştığında ne gösterilecek" + }, + "settings.default-site.html": { + "defaultMessage": "Özel HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Yönlendir" + }, + "setup.preamble": { + "defaultMessage": "Yönetici hesabınızı oluşturarak başlayın." + }, + "setup.title": { + "defaultMessage": "Hoş Geldiniz!" + }, + "sign-in": { + "defaultMessage": "Giriş yap" + }, + "ssl-certificate": { + "defaultMessage": "SSL Sertifikası" + }, + "stream": { + "defaultMessage": "Akış" + }, + "stream.forward-host": { + "defaultMessage": "İletme Host'u" + }, + "stream.forward-host.placeholder": { + "defaultMessage": "example.com veya 10.0.0.1 veya 2001:db8:3333:4444:5555:6666:7777:8888" + }, + "stream.incoming-port": { + "defaultMessage": "Gelen Port" + }, + "streams": { + "defaultMessage": "Akışlar" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Akış} other {Akış}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "update-available": { + "defaultMessage": "Güncelleme Mevcut: {latestVersion}" + }, + "user": { + "defaultMessage": "Kullanıcı" + }, + "user.change-password": { + "defaultMessage": "Şifreyi Değiştir" + }, + "user.confirm-password": { + "defaultMessage": "Şifreyi Onayla" + }, + "user.current-password": { + "defaultMessage": "Mevcut Şifre" + }, + "user.edit-profile": { + "defaultMessage": "Profili Düzenle" + }, + "user.full-name": { + "defaultMessage": "Ad Soyad" + }, + "user.login-as": { + "defaultMessage": "{name} olarak giriş yap" + }, + "user.logout": { + "defaultMessage": "Çıkış Yap" + }, + "user.new-password": { + "defaultMessage": "Yeni Şifre" + }, + "user.nickname": { + "defaultMessage": "Takma Ad" + }, + "user.set-password": { + "defaultMessage": "Şifre Belirle" + }, + "user.set-permissions": { + "defaultMessage": "{name} için İzinleri Belirle" + }, + "user.switch-dark": { + "defaultMessage": "Karanlık moda geç" + }, + "user.switch-light": { + "defaultMessage": "Açık moda geç" + }, + "username": { + "defaultMessage": "Kullanıcı Adı" + }, + "users": { + "defaultMessage": "Kullanıcılar" + } +} diff --git a/frontend/src/locale/src/vi.json b/frontend/src/locale/src/vi.json new file mode 100644 index 0000000..32d26d5 --- /dev/null +++ b/frontend/src/locale/src/vi.json @@ -0,0 +1,659 @@ +{ + "access-list": { + "defaultMessage": "Danh sách truy cập" + }, + "access-list.access-count": { + "defaultMessage": "{count} quy tắc" + }, + "access-list.auth-count": { + "defaultMessage": "{count} người dùng" + }, + "access-list.help-rules-last": { + "defaultMessage": "Quy tắc từ chối tất cả này sẽ được thêm vào cuối khi tồn tại ít nhất 1 quy tắc" + }, + "access-list.help.rules-order": { + "defaultMessage": "Các quy tắc cho phép và từ chối sẽ được thực thi theo thứ tự được xác định." + }, + "access-list.pass-auth": { + "defaultMessage": "Chuyển xác thực lên thượng nguồn" + }, + "access-list.public": { + "defaultMessage": "Có thể truy cập công khai" + }, + "access-list.public.subtitle": { + "defaultMessage": "Không cần xác thực cơ bản" + }, + "access-list.satisfy-any": { + "defaultMessage": "Thỏa mãn điều kiện bất kỳ" + }, + "access-list.subtitle": { + "defaultMessage": "{users} người dùng, {rules} quy tắc - Tạo lúc: {date}" + }, + "access-lists": { + "defaultMessage": "Danh sách truy cập" + }, + "action.add": { + "defaultMessage": "Thêm" + }, + "action.add-location": { + "defaultMessage": "Thêm Vị trí" + }, + "action.close": { + "defaultMessage": "Đóng" + }, + "action.delete": { + "defaultMessage": "Xóa" + }, + "action.disable": { + "defaultMessage": "Tắt" + }, + "action.download": { + "defaultMessage": "Tải xuống" + }, + "action.edit": { + "defaultMessage": "Chỉnh sửa" + }, + "action.enable": { + "defaultMessage": "Bật" + }, + "action.permissions": { + "defaultMessage": "Quyền" + }, + "action.renew": { + "defaultMessage": "Gia hạn" + }, + "action.view-details": { + "defaultMessage": "Xem Chi tiết" + }, + "auditlogs": { + "defaultMessage": "Nhật ký kiểm tra" + }, + "cancel": { + "defaultMessage": "Hủy" + }, + "certificate": { + "defaultMessage": "Chứng chỉ" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificate (crt)" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Certificate Key" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Intermediate Certificate" + }, + "certificate.in-use": { + "defaultMessage": "Đang sử dụng" + }, + "certificate.none.subtitle": { + "defaultMessage": "Không có chứng chỉ nào được chỉ định" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Máy chủ này sẽ không sử dụng HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Không có" + }, + "certificate.not-in-use": { + "defaultMessage": "Không được dùng" + }, + "certificate.renew": { + "defaultMessage": "Gia hạn Chứng chỉ" + }, + "certificates": { + "defaultMessage": "Danh sách chứng chỉ" + }, + "certificates.custom": { + "defaultMessage": "Chứng chỉ tùy chỉnh" + }, + "certificates.custom.warning": { + "defaultMessage": "Các tệp chính được bảo vệ bằng cụm mật khẩu không được hỗ trợ." + }, + "certificates.dns.credentials": { + "defaultMessage": "Nội dung tệp thông tin xác thực" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Plugin này yêu cầu tệp cấu hình chứa mã thông báo API hoặc thông tin xác thực khác cho nhà cung cấp của bạn" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Dữ liệu này sẽ được lưu trữ dưới dạng bản rõ trong cơ sở dữ liệu và trong một tệp!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Thời gian lan truyền (Giây)" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Để trống để sử dụng giá trị mặc định của plugin. Số giây chờ truyền DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Nhà cung cấp DNS" + }, + "certificates.dns.warning": { + "defaultMessage": "Phần này yêu cầu một số kiến thức về Certbot và các plugin DNS của nó. Vui lòng tham khảo tài liệu plugin tương ứng." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Có một máy chủ được tìm thấy ở miền này nhưng có vẻ như nó không phải là NPM. Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Không thể kiểm tra khả năng truy cập do lỗi giao tiếp với site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Không có máy chủ có sẵn tại tên miền này. Vui lòng đảm bảo rằng miền của bạn tồn tại và trỏ đến IP nơi phiên bản NPM của bạn đang chạy và nếu cần, cổng 80 sẽ được chuyển tiếp trong bộ định tuyến của bạn." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Máy chủ của bạn có thể truy cập được và có thể tạo chứng chỉ." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Có một máy chủ được tìm thấy ở miền này nhưng nó trả về mã trạng thái không mong muốn {code}. Đây có phải là máy chủ NPM không? Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Có một máy chủ được tìm thấy ở miền này nhưng nó trả về một dữ liệu không mong muốn. Đây có phải là máy chủ NPM không? Vui lòng đảm bảo tên miền của bạn trỏ đến IP nơi phiên bản NPM của bạn đang chạy." + }, + "certificates.http.test-results": { + "defaultMessage": "Kết quả kiểm tra" + }, + "certificates.http.warning": { + "defaultMessage": "Các miền này phải được cấu hình sẵn để trỏ đến cài đặt này." + }, + "certificates.key-type": { + "defaultMessage": "Loại khóa" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA tương thích rộng rãi, ECDSA nhanh hơn và an toàn hơn nhưng có thể không được hỗ trợ bởi các hệ thống cũ" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "bằng Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Yêu cầu chứng chỉ mới" + }, + "column.access": { + "defaultMessage": "Truy cập" + }, + "column.authorization": { + "defaultMessage": "Ủy quyền" + }, + "column.authorizations": { + "defaultMessage": "Danh sách ủy quyền" + }, + "column.custom-locations": { + "defaultMessage": "Quy tắc đường dẫn tùy chỉnh (Vị trí)" + }, + "column.destination": { + "defaultMessage": "Mục tiêu" + }, + "column.details": { + "defaultMessage": "Chi tiết" + }, + "column.email": { + "defaultMessage": "Email" + }, + "column.event": { + "defaultMessage": "Sự kiện" + }, + "column.expires": { + "defaultMessage": "Hết hạn" + }, + "column.http-code": { + "defaultMessage": "HTTP Code" + }, + "column.incoming-port": { + "defaultMessage": "Cổng đến" + }, + "column.name": { + "defaultMessage": "Tên" + }, + "column.protocol": { + "defaultMessage": "Giao thức" + }, + "column.provider": { + "defaultMessage": "Nhà cung cấp" + }, + "column.roles": { + "defaultMessage": "Vai trò" + }, + "column.rules": { + "defaultMessage": "Quy tắc" + }, + "column.satisfy": { + "defaultMessage": "Thỏa mãn" + }, + "column.satisfy-all": { + "defaultMessage": "Tất cả" + }, + "column.satisfy-any": { + "defaultMessage": "Bất kì" + }, + "column.scheme": { + "defaultMessage": "Scheme" + }, + "column.source": { + "defaultMessage": "Nguồn" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Trạng thái" + }, + "created-on": { + "defaultMessage": "Đã tạo: {date}" + }, + "dashboard": { + "defaultMessage": "Bảng điều khiển" + }, + "dead-host": { + "defaultMessage": "Máy chủ 404" + }, + "dead-hosts": { + "defaultMessage": "Máy chủ 404" + }, + "dead-hosts.count": { + "defaultMessage": "Số trang lỗi {count}" + }, + "disabled": { + "defaultMessage": "Đã tắt" + }, + "domain-names": { + "defaultMessage": "Danh sách tên miền" + }, + "domain-names.max": { + "defaultMessage": "Tối đa {count} tên miền" + }, + "domain-names.placeholder": { + "defaultMessage": "Nhập tên miền vào đây..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Ký tự đại diện không được phép cho loại này" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Ký tự đại diện không được hỗ trợ cho CA này" + }, + "domains.force-ssl": { + "defaultMessage": "Bắt buộc SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "Bật HSTS" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Tên miền phụ HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Hỗ trợ HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Dùng thử thách DNS" + }, + "email-address": { + "defaultMessage": "Địa chỉ email" + }, + "empty-search": { + "defaultMessage": "Không có kết quả nào" + }, + "empty-subtitle": { + "defaultMessage": "Tại sao bạn không tạo một cái luôn?" + }, + "enabled": { + "defaultMessage": "Đã bật" + }, + "error.access.at-least-one": { + "defaultMessage": "Yêu cầu ít nhất một quy tắc ủy quyền hoặc truy cập" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Tên người dùng được ủy quyền phải là duy nhất" + }, + "error.invalid-auth": { + "defaultMessage": "Email hoặc Mật khẩu không hợp lệ" + }, + "error.invalid-domain": { + "defaultMessage": "Tên miền không hợp lệ: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Địa chỉ email không hợp lệ" + }, + "error.max-character-length": { + "defaultMessage": "Độ dài tối đa là {max} ký tự" + }, + "error.max-domains": { + "defaultMessage": "Quá nhiều tên miền, tối đa là {max}" + }, + "error.maximum": { + "defaultMessage": "Tối đa là {max}" + }, + "error.min-character-length": { + "defaultMessage": "Độ dài tối thiểu là {min} ký tự" + }, + "error.minimum": { + "defaultMessage": "Tối thiểu là {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Mật khẩu phải khớp" + }, + "error.required": { + "defaultMessage": "Điều này là bắt buộc" + }, + "expires.on": { + "defaultMessage": "Hết hạn: {date}" + }, + "footer.github-fork": { + "defaultMessage": "Fork dự án này trên Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Chặn các hoạt động khai thác phổ biến" + }, + "host.flags.cache-assets": { + "defaultMessage": "Cache tài nguyên" + }, + "host.flags.preserve-path": { + "defaultMessage": "Bảo toàn đường dẫn" + }, + "host.flags.protocols": { + "defaultMessage": "Giao thức" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Hỗ trợ Websockets" + }, + "host.forward-port": { + "defaultMessage": "Chuyển tiếp cổng" + }, + "host.forward-scheme": { + "defaultMessage": "Scheme" + }, + "hosts": { + "defaultMessage": "Máy chủ" + }, + "http-only": { + "defaultMessage": "HTTP Only" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt qua DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt qua HTTP" + }, + "loading": { + "defaultMessage": "Đang tải..." + }, + "login.title": { + "defaultMessage": "Đăng nhập vào tài khoản của bạn" + }, + "nginx-config.label": { + "defaultMessage": "Cấu hình Nginx tùy chỉnh" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Nhập cấu hình Nginx tùy chỉnh của bạn vào đây và bạn phải tự chịu rủi ro!" + }, + "no-permission-error": { + "defaultMessage": "Bạn không có quyền truy cập trang này." + }, + "notfound.action": { + "defaultMessage": "Về trang chủ" + }, + "notfound.content": { + "defaultMessage": "Chúng tôi xin lỗi nhưng trang bạn đang tìm kiếm không được tìm thấy" + }, + "notfound.title": { + "defaultMessage": "Rất tiếc… Bạn vừa tìm thấy một trang lỗi" + }, + "notification.error": { + "defaultMessage": "Lỗi" + }, + "notification.object-deleted": { + "defaultMessage": "{object} đã được xóa" + }, + "notification.object-disabled": { + "defaultMessage": "{object} đã được tắt" + }, + "notification.object-enabled": { + "defaultMessage": "{object} đã được bật" + }, + "notification.object-renewed": { + "defaultMessage": "{object} đã được làm mới" + }, + "notification.object-saved": { + "defaultMessage": "{object} đã được lưu" + }, + "notification.success": { + "defaultMessage": "Thành công" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Thêm {object}" + }, + "object.delete": { + "defaultMessage": "Xóa {object}" + }, + "object.delete.content": { + "defaultMessage": "Bạn có chắc muốn xóa {object} không?" + }, + "object.edit": { + "defaultMessage": "Chỉnh sửa {object}" + }, + "object.empty": { + "defaultMessage": "Không có {objects}" + }, + "object.event.created": { + "defaultMessage": "Đã tạo {object}" + }, + "object.event.deleted": { + "defaultMessage": "Đã xóa {object}" + }, + "object.event.disabled": { + "defaultMessage": "Đã tắt {object}" + }, + "object.event.enabled": { + "defaultMessage": "Đã bật {object}" + }, + "object.event.renewed": { + "defaultMessage": "Đã gia hạn {object}" + }, + "object.event.updated": { + "defaultMessage": "Đã cập nhật {object}" + }, + "offline": { + "defaultMessage": "Ngoại tuyến" + }, + "online": { + "defaultMessage": "Trực tuyến" + }, + "options": { + "defaultMessage": "Tùy chọn" + }, + "password": { + "defaultMessage": "Mật khẩu" + }, + "password.generate": { + "defaultMessage": "Tạo mật khẩu ngẫu nhiên" + }, + "password.hide": { + "defaultMessage": "Ẩn Mật khẩu" + }, + "password.show": { + "defaultMessage": "Hiện Mật khẩu" + }, + "permissions.hidden": { + "defaultMessage": "Ẩn" + }, + "permissions.manage": { + "defaultMessage": "Quản lý" + }, + "permissions.view": { + "defaultMessage": "Chỉ xem" + }, + "permissions.visibility.all": { + "defaultMessage": "Tất cả các mục" + }, + "permissions.visibility.title": { + "defaultMessage": "Khả năng hiển thị mục" + }, + "permissions.visibility.user": { + "defaultMessage": "Chỉ các mục đã tạo" + }, + "proxy-host": { + "defaultMessage": "Máy chủ proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Chuyển tiếp Hostname / IP" + }, + "proxy-hosts": { + "defaultMessage": "Máy chủ proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} máy chủ proxy" + }, + "public": { + "defaultMessage": "Công khai" + }, + "redirection-host": { + "defaultMessage": "Redirection Host" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Chuyển tiếp Tên miền" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP Code" + }, + "redirection-hosts": { + "defaultMessage": "Redirection Hosts" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} máy chủ chuyển hướng" + }, + "role.admin": { + "defaultMessage": "Quản trị viên" + }, + "role.standard-user": { + "defaultMessage": "Người dùng bình thường" + }, + "save": { + "defaultMessage": "Lưu" + }, + "setting": { + "defaultMessage": "Cài đặt" + }, + "settings": { + "defaultMessage": "Cài đặt" + }, + "settings.default-site": { + "defaultMessage": "Trang web mặc định" + }, + "settings.default-site.404": { + "defaultMessage": "Trang 404" + }, + "settings.default-site.444": { + "defaultMessage": "Không có phản hồi (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Trang chào mừng" + }, + "settings.default-site.description": { + "defaultMessage": "Hiển thị gì khi Nginx gặp phải Máy chủ không xác định" + }, + "settings.default-site.html": { + "defaultMessage": "HTML tùy chỉnh" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Chuyển hướng" + }, + "setup.preamble": { + "defaultMessage": "Bắt đầu bằng cách tạo tài khoản quản trị viên." + }, + "setup.title": { + "defaultMessage": "Chào mừng!" + }, + "sign-in": { + "defaultMessage": "Đăng nhập" + }, + "ssl-certificate": { + "defaultMessage": "Chứng chỉ SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Chuyển tiếp Host" + }, + "stream.incoming-port": { + "defaultMessage": "Cổng vào" + }, + "streams": { + "defaultMessage": "Danh sách các Stream" + }, + "streams.count": { + "defaultMessage": "Số Stream {count}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Kiểm tra" + }, + "update-available": { + "defaultMessage": "Cập nhật khả dụng: {latestVersion}" + }, + "user": { + "defaultMessage": "Người dùng" + }, + "user.change-password": { + "defaultMessage": "Đổi Mật khẩu" + }, + "user.confirm-password": { + "defaultMessage": "Xác nhận Mật khẩu" + }, + "user.current-password": { + "defaultMessage": "Mật khẩu hiện tại" + }, + "user.edit-profile": { + "defaultMessage": "Chỉnh sửa hồ sơ" + }, + "user.full-name": { + "defaultMessage": "Tên" + }, + "user.login-as": { + "defaultMessage": "Đăng nhập bằng {name}" + }, + "user.logout": { + "defaultMessage": "Đăng xuất" + }, + "user.new-password": { + "defaultMessage": "Mật khẩu mới" + }, + "user.nickname": { + "defaultMessage": "Tên hiển thị" + }, + "user.set-password": { + "defaultMessage": "Đặt Mật khẩu" + }, + "user.set-permissions": { + "defaultMessage": "Đặt quyền cho {name}" + }, + "user.switch-dark": { + "defaultMessage": "Chuyển sang chế độ tối" + }, + "user.switch-light": { + "defaultMessage": "Chuyển sang chế độ sáng" + }, + "username": { + "defaultMessage": "Tên người dùng" + }, + "users": { + "defaultMessage": "Danh sách người dùng" + } +} diff --git a/frontend/src/locale/src/zh.json b/frontend/src/locale/src/zh.json new file mode 100644 index 0000000..72494bb --- /dev/null +++ b/frontend/src/locale/src/zh.json @@ -0,0 +1,662 @@ +{ + "access-list": { + "defaultMessage": "通信规则" + }, + "access-list.access-count": { + "defaultMessage": "{count} 条规则" + }, + "access-list.auth-count": { + "defaultMessage": "{count} 个用户" + }, + "access-list.help-rules-last": { + "defaultMessage": "当至少存在1条规则时,此拒绝所有规则将被添加到最后" + }, + "access-list.help.rules-order": { + "defaultMessage": " 允许 (allow) 和禁止 (deny) 规则将按照它们定义的顺序执行。" + }, + "access-list.pass-auth": { + "defaultMessage": "将认证传递给上游" + }, + "access-list.public": { + "defaultMessage": "公开可访问" + }, + "access-list.public.subtitle": { + "defaultMessage": "无需基本认证" + }, + "access-list.satisfy-any": { + "defaultMessage": "满足任意条件" + }, + "access-list.subtitle": { + "defaultMessage": "{users} 个用户, {rules} 条规则 - 创建时间: {date}" + }, + "access-lists": { + "defaultMessage": "通信规则" + }, + "action.add": { + "defaultMessage": "添加" + }, + "action.add-location": { + "defaultMessage": "添加路径规则(Location)" + }, + "action.close": { + "defaultMessage": "关闭" + }, + "action.delete": { + "defaultMessage": "删除" + }, + "action.disable": { + "defaultMessage": "禁用" + }, + "action.download": { + "defaultMessage": "下载" + }, + "action.edit": { + "defaultMessage": "编辑" + }, + "action.enable": { + "defaultMessage": "启用" + }, + "action.permissions": { + "defaultMessage": "权限" + }, + "action.renew": { + "defaultMessage": "续期" + }, + "action.view-details": { + "defaultMessage": "查看详情" + }, + "auditlogs": { + "defaultMessage": "审计日志" + }, + "cancel": { + "defaultMessage": "取消" + }, + "certificate": { + "defaultMessage": "证书" + }, + "certificate.custom-certificate": { + "defaultMessage": "证书" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "证书密钥" + }, + "certificate.custom-intermediate": { + "defaultMessage": "中间证书" + }, + "certificate.in-use": { + "defaultMessage": "使用中" + }, + "certificate.none.subtitle": { + "defaultMessage": "未分配证书" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "此主机将不使用 HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "无" + }, + "certificate.not-in-use": { + "defaultMessage": "未使用" + }, + "certificate.renew": { + "defaultMessage": "续期证书" + }, + "certificates": { + "defaultMessage": "证书列表" + }, + "certificates.custom": { + "defaultMessage": "自定义证书" + }, + "certificates.custom.warning": { + "defaultMessage": "不支持受密码保护的密钥文件。" + }, + "certificates.dns.credentials": { + "defaultMessage": "凭据文件内容" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "此插件需要一个包含 API 令牌或提供商其他凭证的配置文件" + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "此数据将以明文形式存储在数据库和文件中!" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "传播时间 (秒)" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "留空以使用插件默认值。等待DNS传播的秒数。" + }, + "certificates.dns.provider": { + "defaultMessage": "DNS 提供商" + }, + "certificates.dns.warning": { + "defaultMessage": "本节需要您具备一些关于 Certbot 及其 DNS 插件的知识,请参阅相应插件的官方文档。" + }, + "certificates.http.reachability-404": { + "defaultMessage": "在此域名下找到了一个服务器,但它似乎不是 Nginx 代理管理器。请确保您的域名指向 NPM 实例运行的 IP 地址。" + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "由于与site24x7.com通信错误,无法检查可达性。" + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "此域名下没有可用的服务器。请确保您的域名存在并指向NPM实例运行的 IP 地址,如有必要,请在路由器中转发 80 端口。" + }, + "certificates.http.reachability-ok": { + "defaultMessage": "您的服务器可以访问,应该可以创建证书。" + }, + "certificates.http.reachability-other": { + "defaultMessage": "在此域名下找到了一个服务器,但它返回了意外的状态码 {code}。它是 NPM 服务器吗?请确保您的域名指向NPM实例运行的 IP 地址。" + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "在此域名下找到了一个服务器,但它返回了意外的数据。它是 NPM 服务器吗?请确保您的域名指向 NPM 实例运行的 IP 地址。" + }, + "certificates.http.test-results": { + "defaultMessage": "测试结果" + }, + "certificates.http.warning": { + "defaultMessage": "这些域名必须配置为指向本设备。" + }, + "certificates.key-type": { + "defaultMessage": "密钥类型" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA 兼容性更好,ECDSA 更快更安全但旧系统可能不支持" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "使用 Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "申请新证书" + }, + "column.access": { + "defaultMessage": "访问" + }, + "column.authorization": { + "defaultMessage": "授权" + }, + "column.authorizations": { + "defaultMessage": "授权列表" + }, + "column.custom-locations": { + "defaultMessage": "自定义路径规则 (Locations)" + }, + "column.destination": { + "defaultMessage": "目标" + }, + "column.details": { + "defaultMessage": "详情" + }, + "column.email": { + "defaultMessage": "邮箱" + }, + "column.event": { + "defaultMessage": "事件" + }, + "column.expires": { + "defaultMessage": "过期时间" + }, + "column.http-code": { + "defaultMessage": "访问" + }, + "column.incoming-port": { + "defaultMessage": "入站端口" + }, + "column.name": { + "defaultMessage": "名称" + }, + "column.protocol": { + "defaultMessage": "协议" + }, + "column.provider": { + "defaultMessage": "提供商" + }, + "column.roles": { + "defaultMessage": "角色" + }, + "column.rules": { + "defaultMessage": "规则" + }, + "column.satisfy": { + "defaultMessage": "满足" + }, + "column.satisfy-all": { + "defaultMessage": "全部" + }, + "column.satisfy-any": { + "defaultMessage": "任意" + }, + "column.scheme": { + "defaultMessage": "协议" + }, + "column.source": { + "defaultMessage": "来源" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "状态" + }, + "created-on": { + "defaultMessage": "创建时间: {date}" + }, + "dashboard": { + "defaultMessage": "仪表板" + }, + "dead-host": { + "defaultMessage": "错误页面" + }, + "dead-hosts": { + "defaultMessage": "错误页面列表" + }, + "dead-hosts.count": { + "defaultMessage": "{count} 个错误页面列表" + }, + "disabled": { + "defaultMessage": "已禁用" + }, + "domain-names": { + "defaultMessage": "域名" + }, + "domain-names.max": { + "defaultMessage": "{count} 个最多域名数量" + }, + "domain-names.placeholder": { + "defaultMessage": "开始输入以添加域名..." + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "此类型不允许使用通配符" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "此 CA 不支持通配符" + }, + "domains.advanced": { + "defaultMessage": "高级选项" + }, + "domains.force-ssl": { + "defaultMessage": "强制 SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS 已启用" + }, + "domains.hsts-subdomains": { + "defaultMessage": "HSTS 子域名" + }, + "domains.http2-support": { + "defaultMessage": "HTTP/2 支持" + }, + "domains.trust-forwarded-proto": { + "defaultMessage": "信任上游代理传递的协议类型头" + }, + "domains.use-dns": { + "defaultMessage": "使用DNS验证" + }, + "email-address": { + "defaultMessage": "邮箱地址" + }, + "empty-search": { + "defaultMessage": "未找到结果" + }, + "empty-subtitle": { + "defaultMessage": "为什么不由您来创建一个呢?" + }, + "enabled": { + "defaultMessage": "已启用" + }, + "error.access.at-least-one": { + "defaultMessage": "需要至少一个授权或访问规则" + }, + "error.access.duplicate-usernames": { + "defaultMessage": "授权用户名必须唯一" + }, + "error.invalid-auth": { + "defaultMessage": "无效的邮箱或密码" + }, + "error.invalid-domain": { + "defaultMessage": "无效的域名: {domain}" + }, + "error.invalid-email": { + "defaultMessage": "无效的邮箱地址" + }, + "error.max-character-length": { + "defaultMessage": "最大长度为 {max} 个字符" + }, + "error.max-domains": { + "defaultMessage": "域名过多,最多为 {max} 个" + }, + "error.maximum": { + "defaultMessage": "最大值为 {max}" + }, + "error.min-character-length": { + "defaultMessage": "最小长度为 {min} 个字符" + }, + "error.minimum": { + "defaultMessage": "最小值为 {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "密码必须匹配" + }, + "error.required": { + "defaultMessage": "此项为必填项" + }, + "expires.on": { + "defaultMessage": "过期时间: {date}" + }, + "footer.github-fork": { + "defaultMessage": "在 Github 上复刻 (Fork) 本项目" + }, + "host.flags.block-exploits": { + "defaultMessage": "阻止常见攻击" + }, + "host.flags.cache-assets": { + "defaultMessage": "缓存资源" + }, + "host.flags.preserve-path": { + "defaultMessage": "保留路径" + }, + "host.flags.protocols": { + "defaultMessage": "协议" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Websockets 支持" + }, + "host.forward-port": { + "defaultMessage": "转发端口" + }, + "host.forward-scheme": { + "defaultMessage": "协议" + }, + "hosts": { + "defaultMessage": "主机列表" + }, + "http-only": { + "defaultMessage": "仅 HTTP" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt DNS 验证" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt HTTP 验证" + }, + "loading": { + "defaultMessage": "加载中···" + }, + "login.title": { + "defaultMessage": "登录您的账户" + }, + "nginx-config.label": { + "defaultMessage": "自定义 Nginx 配置" + }, + "nginx-config.placeholder": { + "defaultMessage": "# 在此输入您的自定义 Nginx 配置,风险自负!" + }, + "no-permission-error": { + "defaultMessage": "您无权查看此内容。" + }, + "notfound.action": { + "defaultMessage": "返回首页" + }, + "notfound.content": { + "defaultMessage": "很抱歉,您要查找的页面未找到" + }, + "notfound.title": { + "defaultMessage": "糟糕...您刚刚找到了一个错误页面" + }, + "notification.error": { + "defaultMessage": "错误" + }, + "notification.object-deleted": { + "defaultMessage": "{object} 已被删除" + }, + "notification.object-disabled": { + "defaultMessage": "{object} 已被禁用" + }, + "notification.object-enabled": { + "defaultMessage": "{object} 已被启用" + }, + "notification.object-renewed": { + "defaultMessage": "{object} 已续期" + }, + "notification.object-saved": { + "defaultMessage": "{object} 已保存" + }, + "notification.success": { + "defaultMessage": "成功" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "添加 {object}" + }, + "object.delete": { + "defaultMessage": "删除 {object}" + }, + "object.delete.content": { + "defaultMessage": "您确定要删除 {object} 吗?" + }, + "object.edit": { + "defaultMessage": "编辑 {object}" + }, + "object.empty": { + "defaultMessage": "没有 {objects}" + }, + "object.event.created": { + "defaultMessage": "已创建 {object}" + }, + "object.event.deleted": { + "defaultMessage": "已删除 {object}" + }, + "object.event.disabled": { + "defaultMessage": "已禁用 {object}" + }, + "object.event.enabled": { + "defaultMessage": "已启用 {object}" + }, + "object.event.renewed": { + "defaultMessage": "已续期 {object}" + }, + "object.event.updated": { + "defaultMessage": "已更新 {object}" + }, + "offline": { + "defaultMessage": "离线" + }, + "online": { + "defaultMessage": "在线" + }, + "options": { + "defaultMessage": "选项" + }, + "password": { + "defaultMessage": "密码" + }, + "password.generate": { + "defaultMessage": "生成随机密码" + }, + "password.hide": { + "defaultMessage": "隐藏密码" + }, + "password.show": { + "defaultMessage": "显示密码" + }, + "permissions.hidden": { + "defaultMessage": "隐藏" + }, + "permissions.manage": { + "defaultMessage": "管理" + }, + "permissions.view": { + "defaultMessage": "仅查看" + }, + "permissions.visibility.all": { + "defaultMessage": "所有项目" + }, + "permissions.visibility.title": { + "defaultMessage": "项目可见性" + }, + "permissions.visibility.user": { + "defaultMessage": "仅创建的项目" + }, + "proxy-host": { + "defaultMessage": "代理服务" + }, + "proxy-host.forward-host": { + "defaultMessage": "转发主机名 / IP" + }, + "proxy-hosts": { + "defaultMessage": "代理服务列表" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} 个代理服务" + }, + "public": { + "defaultMessage": "公开" + }, + "redirection-host": { + "defaultMessage": "重定向主机" + }, + "redirection-host.forward-domain": { + "defaultMessage": "转发域名" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "HTTP 状态码" + }, + "redirection-hosts": { + "defaultMessage": "重定向主机列表" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} 个重定向主机" + }, + "role.admin": { + "defaultMessage": "管理员" + }, + "role.standard-user": { + "defaultMessage": "标准用户" + }, + "save": { + "defaultMessage": "保存" + }, + "setting": { + "defaultMessage": "设置" + }, + "settings": { + "defaultMessage": "设置列表" + }, + "settings.default-site": { + "defaultMessage": "默认站点" + }, + "settings.default-site.404": { + "defaultMessage": "错误页面" + }, + "settings.default-site.444": { + "defaultMessage": "无响应 (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "欢迎页面" + }, + "settings.default-site.description": { + "defaultMessage": "当 Nginx 遇到未知主机时显示什么" + }, + "settings.default-site.html": { + "defaultMessage": "自定义 HTML" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "重定向" + }, + "setup.preamble": { + "defaultMessage": "通过创建您的管理员账户开始使用。" + }, + "setup.title": { + "defaultMessage": "欢迎!" + }, + "sign-in": { + "defaultMessage": "登录" + }, + "ssl-certificate": { + "defaultMessage": "SSL 证书" + }, + "stream": { + "defaultMessage": "端口转发" + }, + "stream.forward-host": { + "defaultMessage": "转发主机" + }, + "stream.incoming-port": { + "defaultMessage": "入站端口" + }, + "streams": { + "defaultMessage": "端口转发列表" + }, + "streams.count": { + "defaultMessage": "{count} 个端口转发" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "测试" + }, + "user": { + "defaultMessage": "用户" + }, + "user.change-password": { + "defaultMessage": "修改密码" + }, + "user.confirm-password": { + "defaultMessage": "确认密码" + }, + "user.current-password": { + "defaultMessage": "当前密码" + }, + "user.edit-profile": { + "defaultMessage": "编辑资料" + }, + "user.full-name": { + "defaultMessage": "全名" + }, + "user.login-as": { + "defaultMessage": "登录用户 {name}" + }, + "user.logout": { + "defaultMessage": "退出登录" + }, + "user.new-password": { + "defaultMessage": "新密码" + }, + "user.nickname": { + "defaultMessage": "昵称" + }, + "user.set-password": { + "defaultMessage": "设置密码" + }, + "user.set-permissions": { + "defaultMessage": "为用户 {name} 设置权限" + }, + "user.switch-dark": { + "defaultMessage": "切换到深色模式" + }, + "user.switch-light": { + "defaultMessage": "切换到浅色模式" + }, + "username": { + "defaultMessage": "用户名" + }, + "users": { + "defaultMessage": "用户列表" + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..62c1c48 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "src/App.tsx"; + +import "@tabler/core/dist/css/tabler.min.css"; +import "@tabler/core/dist/js/tabler.min.js"; +import "./App.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/frontend/src/modals/AccessListModal.tsx b/frontend/src/modals/AccessListModal.tsx new file mode 100644 index 0000000..79537f5 --- /dev/null +++ b/frontend/src/modals/AccessListModal.tsx @@ -0,0 +1,292 @@ +import cn from "classnames"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import type { AccessList, AccessListClient, AccessListItem } from "src/api/backend"; +import { AccessClientFields, BasicAuthFields, Button, Loading } from "src/components"; +import { useAccessList, useSetAccessList } from "src/hooks"; +import { intl, T } from "src/locale"; +import { validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +const showAccessListModal = (id: number | "new") => { + EasyModal.show(AccessListModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "new"; +} +const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data, isLoading, error } = useAccessList(id, ["items", "clients"]); + const { mutate: setAccessList } = useSetAccessList(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const validate = (values: any): string | null => { + // either Auths or Clients must be defined + if (values.items?.length === 0 && values.clients?.length === 0) { + return intl.formatMessage({ id: "error.access.at-least-one" }); + } + + // ensure the items don't contain the same username twice + const usernames = values.items.map((i: any) => i.username); + const uniqueUsernames = Array.from(new Set(usernames)); + if (usernames.length !== uniqueUsernames.length) { + return intl.formatMessage({ id: "error.access.duplicate-usernames" }); + } + + return null; + }; + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + + const vErr = validate(values); + if (vErr) { + setErrorMsg(vErr); + return; + } + + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + // Filter out "items" to only use the "username" and "password" fields + payload.items = (values.items || []).map((i: AccessListItem) => ({ + username: i.username, + password: i.password, + })); + + // Filter out "clients" to only use the "directive" and "address" fields + payload.clients = (values.clients || []).map((i: AccessListClient) => ({ + directive: i.directive, + address: i.address, + })); + + setAccessList(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showObjectSuccess("access-list", "saved"); + remove(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + const toggleClasses = "form-check-input"; + const toggleEnabled = cn(toggleClasses, "bg-cyan"); + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {({ setFieldValue }: any) => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    + +
    +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.name ? ( +
    + {form.errors.name && form.touched.name + ? form.errors.name + : null} +
    + ) : null} +
    + )} +
    +
    +

    + +

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showAccessListModal }; diff --git a/frontend/src/modals/ChangePasswordModal.tsx b/frontend/src/modals/ChangePasswordModal.tsx new file mode 100644 index 0000000..48221e2 --- /dev/null +++ b/frontend/src/modals/ChangePasswordModal.tsx @@ -0,0 +1,170 @@ +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { updateAuth } from "src/api/backend"; +import { Button } from "src/components"; +import { intl, T } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +const showChangePasswordModal = (id: number | "me") => { + EasyModal.show(ChangePasswordModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "me"; +} +const ChangePasswordModal = EasyModal.create(({ id, visible, remove }: Props) => { + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (values.new !== values.confirm) { + setError(); + setSubmitting(false); + return; + } + + if (isSubmitting) return; + setIsSubmitting(true); + setError(null); + + try { + await updateAuth(id, values.new, values.current); + remove(); + } catch (err: any) { + setError(); + } + setIsSubmitting(false); + setSubmitting(false); + }; + + return ( + + + {() => ( +
    + + + + + + + setError(null)} dismissible> + {error} + +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.name ? ( +
    + {form.errors.current && form.touched.current + ? form.errors.current + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.new ? ( +
    + {form.errors.new && form.touched.new ? form.errors.new : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + {form.errors.confirm ? ( +
    + {form.errors.confirm && form.touched.confirm + ? form.errors.confirm + : null} +
    + ) : null} + +
    + )} +
    +
    +
    + + + + +
    + )} +
    +
    + ); +}); + +export { showChangePasswordModal }; diff --git a/frontend/src/modals/CustomCertificateModal.tsx b/frontend/src/modals/CustomCertificateModal.tsx new file mode 100644 index 0000000..deab1c5 --- /dev/null +++ b/frontend/src/modals/CustomCertificateModal.tsx @@ -0,0 +1,232 @@ +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { type Certificate, createCertificate, uploadCertificate, validateCertificate } from "src/api/backend"; +import { Button } from "src/components"; +import { T } from "src/locale"; +import { validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +const showCustomCertificateModal = () => { + EasyModal.show(CustomCertificateModal); +}; + +const CustomCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => { + const queryClient = useQueryClient(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + try { + const { niceName, provider, certificate, certificateKey, intermediateCertificate } = values; + const formData = new FormData(); + + formData.append("certificate", certificate); + formData.append("certificate_key", certificateKey); + if (intermediateCertificate !== null) { + formData.append("intermediate_certificate", intermediateCertificate); + } + + // Validate + await validateCertificate(formData); + + // Create certificate, as other without anything else + const cert = await createCertificate({ niceName, provider } as Certificate); + + // Upload the certificates to the created certificate + await uploadCertificate(cert.id, formData); + + // Success + showObjectSuccess("certificate", "saved"); + remove(); + } catch (err: any) { + setErrorMsg(); + } + + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + setIsSubmitting(false); + setSubmitting(false); + }; + + return ( + + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    +

    + + +

    + + {({ field, form }: any) => ( +
    + + + {form.errors.niceName ? ( +
    + {form.errors.niceName && form.touched.niceName + ? form.errors.niceName + : null} +
    + ) : null} +
    + )} +
    + + {({ field, form }: any) => ( +
    + + { + form.setFieldValue( + field.name, + event.currentTarget.files?.length + ? event.currentTarget.files[0] + : null, + ); + }} + /> + {form.errors.certificateKey ? ( +
    + {form.errors.certificateKey && form.touched.certificateKey + ? form.errors.certificateKey + : null} +
    + ) : null} +
    + )} +
    + + {({ field, form }: any) => ( +
    + + { + form.setFieldValue( + field.name, + event.currentTarget.files?.length + ? event.currentTarget.files[0] + : null, + ); + }} + /> + {form.errors.certificate ? ( +
    + {form.errors.certificate && form.touched.certificate + ? form.errors.certificate + : null} +
    + ) : null} +
    + )} +
    + + {({ field, form }: any) => ( +
    + + { + form.setFieldValue( + field.name, + event.currentTarget.files?.length + ? event.currentTarget.files[0] + : null, + ); + }} + /> + {form.errors.intermediateCertificate ? ( +
    + {form.errors.intermediateCertificate && + form.touched.intermediateCertificate + ? form.errors.intermediateCertificate + : null} +
    + ) : null} +
    + )} +
    +
    +
    +
    + + + + +
    + )} +
    +
    + ); +}); + +export { showCustomCertificateModal }; diff --git a/frontend/src/modals/DNSCertificateModal.tsx b/frontend/src/modals/DNSCertificateModal.tsx new file mode 100644 index 0000000..f062e24 --- /dev/null +++ b/frontend/src/modals/DNSCertificateModal.tsx @@ -0,0 +1,117 @@ +import { useQueryClient } from "@tanstack/react-query"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Form, Formik, Field } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { createCertificate } from "src/api/backend"; +import { Button, DNSProviderFields, DomainNamesField } from "src/components"; +import { T } from "src/locale"; +import { showObjectSuccess } from "src/notifications"; + +const showDNSCertificateModal = () => { + EasyModal.show(DNSCertificateModal); +}; + +const DNSCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => { + const queryClient = useQueryClient(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + try { + await createCertificate(values); + showObjectSuccess("certificate", "saved"); + remove(); + } catch (err: any) { + setErrorMsg(); + } + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + setIsSubmitting(false); + setSubmitting(false); + }; + + return ( + + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    + + + {({ field }: any) => ( +
    + + + + + +
    + )} +
    + +
    +
    +
    + + + + +
    + )} +
    +
    + ); +}); + +export { showDNSCertificateModal }; diff --git a/frontend/src/modals/DeadHostModal.tsx b/frontend/src/modals/DeadHostModal.tsx new file mode 100644 index 0000000..c9dce9a --- /dev/null +++ b/frontend/src/modals/DeadHostModal.tsx @@ -0,0 +1,174 @@ +import { IconSettings } from "@tabler/icons-react"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { + Button, + DomainNamesField, + Loading, + NginxConfigField, + SSLCertificateField, + SSLOptionsFields, +} from "src/components"; +import { useDeadHost, useSetDeadHost } from "src/hooks"; +import { T } from "src/locale"; +import { showObjectSuccess } from "src/notifications"; + +const showDeadHostModal = (id: number | "new") => { + EasyModal.show(DeadHostModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "new"; +} +const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data, isLoading, error } = useDeadHost(id); + const { mutate: setDeadHost } = useSetDeadHost(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + setDeadHost(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showObjectSuccess("dead-host", "saved"); + remove(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showDeadHostModal }; diff --git a/frontend/src/modals/DeleteConfirmModal.tsx b/frontend/src/modals/DeleteConfirmModal.tsx new file mode 100644 index 0000000..fb89f05 --- /dev/null +++ b/frontend/src/modals/DeleteConfirmModal.tsx @@ -0,0 +1,98 @@ +import { useQueryClient } from "@tanstack/react-query"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { Button } from "src/components"; +import { T } from "src/locale"; + +interface ShowProps { + title?: ReactNode; + tTitle?: string; + children: ReactNode; + onConfirm: () => Promise | void; + invalidations?: any[]; +} + +interface Props extends InnerModalProps, ShowProps {} + +const showDeleteConfirmModal = (props: ShowProps) => { + EasyModal.show(DeleteConfirmModal, props); +}; + +const DeleteConfirmModal = EasyModal.create( + ({ title, tTitle, children, onConfirm, invalidations, visible, remove }: Props) => { + const queryClient = useQueryClient(); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + setError(null); + try { + await onConfirm(); + remove(); + // invalidate caches as requested + invalidations?.forEach((inv) => { + queryClient.invalidateQueries({ queryKey: inv }); + }); + } catch (err: any) { + setError(); + } + setIsSubmitting(false); + }; + + return ( + + + {tTitle ? : title ? title : null} + + + setError(null)} dismissible> + {error} + +
    + + + + + +
    +
    {children}
    +
    + + + + +
    + ); + }, +); + +export { showDeleteConfirmModal }; diff --git a/frontend/src/modals/EventDetailsModal.tsx b/frontend/src/modals/EventDetailsModal.tsx new file mode 100644 index 0000000..7cf6848 --- /dev/null +++ b/frontend/src/modals/EventDetailsModal.tsx @@ -0,0 +1,72 @@ +import CodeEditor from "@uiw/react-textarea-code-editor"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components"; +import { useAuditLog } from "src/hooks"; +import { T } from "src/locale"; + +const showEventDetailsModal = (id: number) => { + EasyModal.show(EventDetailsModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number; +} +const EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data, isLoading, error } = useAuditLog(id); + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + <> + + + + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + + + + )} +
    + ); +}); + +export { showEventDetailsModal }; diff --git a/frontend/src/modals/HTTPCertificateModal.tsx b/frontend/src/modals/HTTPCertificateModal.tsx new file mode 100644 index 0000000..a06df2c --- /dev/null +++ b/frontend/src/modals/HTTPCertificateModal.tsx @@ -0,0 +1,218 @@ +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Form, Formik, Field } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { createCertificate, testHttpCertificate } from "src/api/backend"; +import { Button, DomainNamesField } from "src/components"; +import { T } from "src/locale"; +import { showObjectSuccess } from "src/notifications"; + +const showHTTPCertificateModal = () => { + EasyModal.show(HTTPCertificateModal); +}; + +const HTTPCertificateModal = EasyModal.create(({ visible, remove }: InnerModalProps) => { + const queryClient = useQueryClient(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [domains, setDomains] = useState([] as string[]); + const [isTesting, setIsTesting] = useState(false); + const [testResults, setTestResults] = useState(null as Record | null); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + try { + await createCertificate(values); + showObjectSuccess("certificate", "saved"); + remove(); + } catch (err: any) { + setErrorMsg(); + } + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + setIsSubmitting(false); + setSubmitting(false); + }; + + const handleTest = async () => { + setIsTesting(true); + setErrorMsg(null); + setTestResults(null); + try { + const result = await testHttpCertificate(domains); + setTestResults(result); + } catch (err: any) { + setErrorMsg(); + } + setIsTesting(false); + }; + + const parseTestResults = () => { + const elms = []; + for (const domain in testResults) { + const status = testResults[domain]; + if (status === "ok") { + elms.push( +

    + {domain}: +

    , + ); + } else { + if (status === "no-host") { + elms.push( +

    + {domain}: +

    , + ); + } else if (status === "failed") { + elms.push( +

    + {domain}: +

    , + ); + } else if (status === "404") { + elms.push( +

    + {domain}: +

    , + ); + } else if (status === "wrong-data") { + elms.push( +

    + {domain}: +

    , + ); + } else if (status.startsWith("other:")) { + const code = status.substring(6); + elms.push( +

    + {domain}: +

    , + ); + } else { + // This should never happen + elms.push( +

    + {domain}: ? +

    , + ); + } + } + } + + return <>{elms}; + }; + + return ( + + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    +

    + + +

    + { + setDomains(doms); + setTestResults(null); + }} + /> + + {({ field }: any) => ( +
    + + + + + +
    + )} +
    +
    + {testResults ? ( +
    +
    + +
    + {parseTestResults()} +
    + ) : null} +
    +
    + + +
    + + +
    +
    +
    + )} +
    +
    + ); +}); + +export { showHTTPCertificateModal }; diff --git a/frontend/src/modals/HelpModal.tsx b/frontend/src/modals/HelpModal.tsx new file mode 100644 index 0000000..f058161 --- /dev/null +++ b/frontend/src/modals/HelpModal.tsx @@ -0,0 +1,54 @@ +import cn from "classnames"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { useEffect, useState } from "react"; +import Modal from "react-bootstrap/Modal"; +import ReactMarkdown from "react-markdown"; +import { Button } from "src/components"; +import { getLocale, T } from "src/locale"; +import { getHelpFile } from "src/locale/src/HelpDoc"; + +interface Props extends InnerModalProps { + section: string; + color?: string; +} + +const showHelpModal = (section: string, color?: string) => { + EasyModal.show(HelpModal, { section, color }); +}; + +const HelpModal = EasyModal.create(({ section, color, visible, remove }: Props) => { + const [markdownText, setMarkdownText] = useState(""); + const lang = getLocale(true); + + useEffect(() => { + try { + const docFile = getHelpFile(lang, section) as any; + fetch(docFile) + .then((response) => response.text()) + .then(setMarkdownText); + } catch (ex: any) { + setMarkdownText(`**ERROR:** ${ex.message}`); + } + }, [lang, section]); + + return ( + + + {markdownText} + + + + + + ); +}); + +export { showHelpModal }; diff --git a/frontend/src/modals/PermissionsModal.module.css b/frontend/src/modals/PermissionsModal.module.css new file mode 100644 index 0000000..6ef4265 --- /dev/null +++ b/frontend/src/modals/PermissionsModal.module.css @@ -0,0 +1,4 @@ +.active { + border-color: var(--tblr-orange) !important; +} + diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx new file mode 100644 index 0000000..d363de9 --- /dev/null +++ b/frontend/src/modals/PermissionsModal.tsx @@ -0,0 +1,287 @@ +import { useQueryClient } from "@tanstack/react-query"; +import cn from "classnames"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { setPermissions } from "src/api/backend"; +import { Button, Loading } from "src/components"; +import { useUser } from "src/hooks"; +import { T } from "src/locale"; +import styles from "./PermissionsModal.module.css"; + +const showPermissionsModal = (id: number) => { + EasyModal.show(PermissionsModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number; +} +const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => { + const queryClient = useQueryClient(); + const [errorMsg, setErrorMsg] = useState(null); + const { data, isLoading, error } = useUser(id); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + try { + await setPermissions(id, values); + remove(); + queryClient.invalidateQueries({ queryKey: ["users"] }); + queryClient.invalidateQueries({ queryKey: ["user"] }); + } catch (err: any) { + setErrorMsg(); + } + setSubmitting(false); + setIsSubmitting(false); + }; + + const getClasses = (active: boolean) => { + return cn("btn", active ? styles.active : null, { + active, + "bg-orange-lt": active, + }); + }; + + // given the field and clicked permission, intelligently set the value, and + // other values that depends on it. + const handleChange = (form: any, field: any, perm: string) => { + if (field.name === "proxyHosts" && perm !== "hidden" && form.values.accessLists === "hidden") { + form.setFieldValue("accessLists", "view"); + } + // certs are required for proxy and redirection hosts, and streams + if ( + ["proxyHosts", "redirectionHosts", "deadHosts", "streams"].includes(field.name) && + perm !== "hidden" && + form.values.certificates === "hidden" + ) { + form.setFieldValue("certificates", "view"); + } + + form.setFieldValue(field.name, perm); + }; + + const getPermissionButtons = (field: any, form: any) => { + const isManage = field.value === "manage"; + const isView = field.value === "view"; + const isHidden = field.value === "hidden"; + + let hiddenDisabled = false; + if (field.name === "accessLists") { + hiddenDisabled = form.values.proxyHosts !== "hidden"; + } + if (field.name === "certificates") { + hiddenDisabled = + form.values.proxyHosts !== "hidden" || + form.values.redirectionHosts !== "hidden" || + form.values.deadHosts !== "hidden" || + form.values.streams !== "hidden"; + } + + return ( +
    +
    + handleChange(form, field, "manage")} + /> + + handleChange(form, field, "view")} + /> + + handleChange(form, field, "hidden")} + /> + +
    +
    + ); + }; + + const isAdmin = data?.roles.indexOf("admin") !== -1; + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    + + + {({ field, form }: any) => ( +
    + form.setFieldValue(field.name, "user")} + /> + + form.setFieldValue(field.name, "all")} + /> + +
    + )} +
    +
    + {!isAdmin && ( + <> +
    + + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
    +
    + + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
    +
    + + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
    +
    + + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
    +
    + + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
    +
    + + + {({ field, form }: any) => getPermissionButtons(field, form)} + +
    + + )} +
    + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showPermissionsModal }; diff --git a/frontend/src/modals/ProxyHostModal.tsx b/frontend/src/modals/ProxyHostModal.tsx new file mode 100644 index 0000000..3227be5 --- /dev/null +++ b/frontend/src/modals/ProxyHostModal.tsx @@ -0,0 +1,377 @@ +import { IconSettings } from "@tabler/icons-react"; +import cn from "classnames"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { + AccessField, + Button, + DomainNamesField, + HasPermission, + Loading, + LocationsFields, + NginxConfigField, + SSLCertificateField, + SSLOptionsFields, +} from "src/components"; +import { useProxyHost, useSetProxyHost, useUser } from "src/hooks"; +import { T } from "src/locale"; +import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions"; +import { validateNumber, validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +const showProxyHostModal = (id: number | "new") => { + EasyModal.show(ProxyHostModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "new"; +} +const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data: currentUser, isLoading: userIsLoading, error: userError } = useUser("me"); + const { data, isLoading, error } = useProxyHost(id); + const { mutate: setProxyHost } = useSetProxyHost(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + setProxyHost(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showObjectSuccess("proxy-host", "saved"); + remove(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + return ( + + {!isLoading && (error || userError) && ( + + {error?.message || userError?.message || "Unknown error"} + + )} + {isLoading || (userIsLoading && )} + {!isLoading && !userIsLoading && data && currentUser && ( + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    + +
    +
    +
    +
    + +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardScheme ? ( +
    + {form.errors.forwardScheme && + form.touched.forwardScheme + ? form.errors.forwardScheme + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardHost ? ( +
    + {form.errors.forwardHost && + form.touched.forwardHost + ? form.errors.forwardHost + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardPort ? ( +
    + {form.errors.forwardPort && + form.touched.forwardPort + ? form.errors.forwardPort + : null} +
    + ) : null} +
    + )} +
    +
    +
    + +
    +

    + +

    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + + + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showProxyHostModal }; diff --git a/frontend/src/modals/RedirectionHostModal.tsx b/frontend/src/modals/RedirectionHostModal.tsx new file mode 100644 index 0000000..1bd7877 --- /dev/null +++ b/frontend/src/modals/RedirectionHostModal.tsx @@ -0,0 +1,337 @@ +import { IconSettings } from "@tabler/icons-react"; +import cn from "classnames"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { + Button, + DomainNamesField, + Loading, + NginxConfigField, + SSLCertificateField, + SSLOptionsFields, +} from "src/components"; +import { useRedirectionHost, useSetRedirectionHost } from "src/hooks"; +import { T } from "src/locale"; +import { validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +const showRedirectionHostModal = (id: number | "new") => { + EasyModal.show(RedirectionHostModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "new"; +} +const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data, isLoading, error } = useRedirectionHost(id); + const { mutate: setRedirectionHost } = useSetRedirectionHost(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + setRedirectionHost(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showObjectSuccess("redirection-host", "saved"); + remove(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    + +
    +
    +
    +
    + +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardScheme ? ( +
    + {form.errors.forwardScheme && + form.touched.forwardScheme + ? form.errors.forwardScheme + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardDomainName ? ( +
    + {form.errors.forwardDomainName && + form.touched.forwardDomainName + ? form.errors.forwardDomainName + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardHttpCode ? ( +
    + {form.errors.forwardHttpCode && + form.touched.forwardHttpCode + ? form.errors.forwardHttpCode + : null} +
    + ) : null} +
    + )} +
    +
    +

    + +

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showRedirectionHostModal }; diff --git a/frontend/src/modals/RenewCertificateModal.tsx b/frontend/src/modals/RenewCertificateModal.tsx new file mode 100644 index 0000000..8f7cc63 --- /dev/null +++ b/frontend/src/modals/RenewCertificateModal.tsx @@ -0,0 +1,74 @@ +import { useQueryClient } from "@tanstack/react-query"; +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { type ReactNode, useEffect, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { renewCertificate } from "src/api/backend"; +import { Button, Loading } from "src/components"; +import { useCertificate } from "src/hooks"; +import { T } from "src/locale"; +import { showObjectSuccess } from "src/notifications"; + +interface Props extends InnerModalProps { + id: number; +} + +const showRenewCertificateModal = (id: number) => { + EasyModal.show(RenewCertificateModal, { id }); +}; + +const RenewCertificateModal = EasyModal.create(({ id, visible, remove }: Props) => { + const queryClient = useQueryClient(); + const { data, isLoading, error } = useCertificate(id); + const [errorMsg, setErrorMsg] = useState(null); + const [isFresh, setIsFresh] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!data || !isFresh || isSubmitting) return; + setIsFresh(false); + setIsSubmitting(true); + + renewCertificate(id) + .then(() => { + showObjectSuccess("certificate", "renewed"); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + remove(); + }) + .catch((err: any) => { + setErrorMsg(); + }) + .finally(() => { + setIsSubmitting(false); + }); + }, [id, data, isFresh, isSubmitting, remove, queryClient]); + + return ( + + + + + + + + + {errorMsg} + + {isLoading && } + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {data && isSubmitting && !errorMsg ?

    Please wait ...

    : null} +
    + + + +
    + ); +}); + +export { showRenewCertificateModal }; diff --git a/frontend/src/modals/SetPasswordModal.tsx b/frontend/src/modals/SetPasswordModal.tsx new file mode 100644 index 0000000..ef6d99b --- /dev/null +++ b/frontend/src/modals/SetPasswordModal.tsx @@ -0,0 +1,138 @@ +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { generate } from "generate-password-browser"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { updateAuth } from "src/api/backend"; +import { Button } from "src/components"; +import { intl, T } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +const showSetPasswordModal = (id: number) => { + EasyModal.show(SetPasswordModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number; +} +const SetPasswordModal = EasyModal.create(({ id, visible, remove }: Props) => { + const [error, setError] = useState(null); + const [showPassword, setShowPassword] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setError(null); + try { + await updateAuth(id, values.new); + remove(); + } catch (err: any) { + setError(); + } + setIsSubmitting(false); + setSubmitting(false); + }; + + return ( + + + {() => ( +
    + + + + + + + setError(null)} dismissible> + {error} + +
    + + {({ field, form }: any) => ( + <> +

    + + { + e.preventDefault(); + form.setFieldValue( + field.name, + generate({ + length: 12, + numbers: true, + }), + ); + setShowPassword(true); + }} + > + + {" "} + —{" "} + { + e.preventDefault(); + setShowPassword(!showPassword); + }} + > + + + +

    +
    + + + {form.errors.new ? ( +
    + {form.errors.new && form.touched.new ? form.errors.new : null} +
    + ) : null} +
    + + )} +
    +
    +
    + + + + +
    + )} +
    +
    + ); +}); + +export { showSetPasswordModal }; diff --git a/frontend/src/modals/StreamModal.tsx b/frontend/src/modals/StreamModal.tsx new file mode 100644 index 0000000..6d55348 --- /dev/null +++ b/frontend/src/modals/StreamModal.tsx @@ -0,0 +1,325 @@ +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { Button, Loading, SSLCertificateField, SSLOptionsFields } from "src/components"; +import { useSetStream, useStream } from "src/hooks"; +import { intl, T } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +const showStreamModal = (id: number | "new") => { + EasyModal.show(StreamModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "new"; +} +const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data, isLoading, error } = useStream(id); + const { mutate: setStream } = useSetStream(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + setStream(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showObjectSuccess("stream", "saved"); + remove(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {({ setFieldValue }: any) => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + + +
    +
    + +
    +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.incomingPort ? ( +
    + {form.errors.incomingPort && + form.touched.incomingPort + ? form.errors.incomingPort + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardingHost ? ( +
    + {form.errors.forwardingHost && + form.touched.forwardingHost + ? form.errors.forwardingHost + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.forwardingPort ? ( +
    + {form.errors.forwardingPort && + form.touched.forwardingPort + ? form.errors.forwardingPort + : null} +
    + ) : null} +
    + )} +
    +
    +
    +
    +

    + +

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showStreamModal }; diff --git a/frontend/src/modals/TwoFactorModal.tsx b/frontend/src/modals/TwoFactorModal.tsx new file mode 100644 index 0000000..b5dd480 --- /dev/null +++ b/frontend/src/modals/TwoFactorModal.tsx @@ -0,0 +1,368 @@ +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useCallback, useEffect, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { + disable2FA, + enable2FA, + get2FAStatus, + regenerateBackupCodes, + start2FASetup, +} from "src/api/backend"; +import { Button } from "src/components"; +import { T } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +type Step = "loading" | "status" | "setup" | "verify" | "backup" | "disable"; + +const showTwoFactorModal = (id: number | "me") => { + EasyModal.show(TwoFactorModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "me"; +} + +const TwoFactorModal = EasyModal.create(({ id, visible, remove }: Props) => { + const [error, setError] = useState(null); + const [step, setStep] = useState("loading"); + const [isEnabled, setIsEnabled] = useState(false); + const [backupCodesRemaining, setBackupCodesRemaining] = useState(0); + const [setupData, setSetupData] = useState<{ secret: string; otpauthUrl: string } | null>(null); + const [backupCodes, setBackupCodes] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + const loadStatus = useCallback(async () => { + try { + const status = await get2FAStatus(id); + setIsEnabled(status.enabled); + setBackupCodesRemaining(status.backupCodesRemaining); + setStep("status"); + } catch (err: any) { + setError(err.message || "Failed to load 2FA status"); + setStep("status"); + } + }, [id]); + + useEffect(() => { + loadStatus(); + }, [loadStatus]); + + const handleStartSetup = async () => { + setError(null); + setIsSubmitting(true); + try { + const data = await start2FASetup(id); + setSetupData(data); + setStep("setup"); + } catch (err: any) { + setError(err.message || "Failed to start 2FA setup"); + } + setIsSubmitting(false); + }; + + const handleVerify = async (values: { code: string }) => { + setError(null); + setIsSubmitting(true); + try { + const result = await enable2FA(id, values.code); + setBackupCodes(result.backupCodes); + setStep("backup"); + } catch (err: any) { + setError(err.message || "Failed to enable 2FA"); + } + setIsSubmitting(false); + }; + + const handleDisable = async (values: { code: string }) => { + setError(null); + setIsSubmitting(true); + try { + await disable2FA(id, values.code); + setIsEnabled(false); + setStep("status"); + } catch (err: any) { + setError(err.message || "Failed to disable 2FA"); + } + setIsSubmitting(false); + }; + + const handleRegenerateBackup = async (values: { code: string }) => { + setError(null); + setIsSubmitting(true); + try { + const result = await regenerateBackupCodes(id, values.code); + setBackupCodes(result.backupCodes); + setStep("backup"); + } catch (err: any) { + setError(err.message || "Failed to regenerate backup codes"); + } + setIsSubmitting(false); + }; + + const handleBackupDone = () => { + setIsEnabled(true); + setBackupCodes([]); + loadStatus(); + }; + + const renderContent = () => { + if (step === "loading") { + return ( +
    +
    + Loading... +
    +
    + ); + } + + if (step === "status") { + return ( +
    +
    +
    + + + + + {isEnabled ? : } + +
    + {isEnabled && ( +

    + +

    + )} +
    + {!isEnabled ? ( + + ) : ( +
    + + +
    + )} +
    + ); + } + + if (step === "setup" && setupData) { + return ( +
    +

    + +

    +
    + QR Code +
    + + + {() => ( +
    + + {({ field, form }: any) => ( + + )} + +
    + + +
    +
    + )} +
    +
    + ); + } + + if (step === "backup") { + return ( +
    + + + +
    +
    + {backupCodes.map((code, index) => ( +
    + {code} +
    + ))} +
    +
    + +
    + ); + } + + if (step === "disable") { + return ( +
    + + + + + {() => ( +
    + + {({ field, form }: any) => ( + + )} + +
    + + +
    +
    + )} +
    +
    + ); + } + + if (step === "verify") { + return ( +
    +

    + +

    + + {() => ( +
    + + {({ field, form }: any) => ( + + )} + +
    + + +
    +
    + )} +
    +
    + ); + } + + return null; + }; + + return ( + + + + + + + + setError(null)} dismissible> + {error} + + {renderContent()} + + + ); +}); + +export { showTwoFactorModal }; diff --git a/frontend/src/modals/UserModal.tsx b/frontend/src/modals/UserModal.tsx new file mode 100644 index 0000000..06bc38c --- /dev/null +++ b/frontend/src/modals/UserModal.tsx @@ -0,0 +1,246 @@ +import EasyModal, { type InnerModalProps } from "ez-modal-react"; +import { Field, Form, Formik } from "formik"; +import { useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { Button, Loading } from "src/components"; +import { useSetUser, useUser } from "src/hooks"; +import { intl, T } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +const showUserModal = (id: number | "me" | "new") => { + EasyModal.show(UserModal, { id }); +}; + +interface Props extends InnerModalProps { + id: number | "me" | "new"; +} +const UserModal = EasyModal.create(({ id, visible, remove }: Props) => { + const { data, isLoading, error } = useUser(id); + const { data: currentUser, isLoading: currentIsLoading } = useUser("me"); + const { mutate: setUser } = useSetUser(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + roles: [], + ...values, + }; + + if (data?.id === currentUser?.id) { + // Prevent user from locking themselves out + delete payload.isDisabled; + delete payload.roles; + } else if (payload.isAdmin) { + payload.roles = ["admin"]; + } + + // this isn't a real field, just for the form + delete payload.isAdmin; + + setUser(payload, { + onError: (err: any) => setErrorMsg(err.message), + onSuccess: () => { + showObjectSuccess("user", "saved"); + remove(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {(isLoading || currentIsLoading) && } + {!isLoading && !currentIsLoading && data && currentUser && ( + + {() => ( +
    + + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.name ? ( +
    + {form.errors.name && form.touched.name + ? form.errors.name + : null} +
    + ) : null} +
    + )} +
    +
    +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.nickname ? ( +
    + {form.errors.nickname && form.touched.nickname + ? form.errors.nickname + : null} +
    + ) : null} +
    + )} +
    +
    +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.email ? ( +
    + {form.errors.email && form.touched.email + ? form.errors.email + : null} +
    + ) : null} +
    + )} +
    +
    + {currentUser && data && currentUser?.id !== data?.id ? ( +
    +

    + +

    +
    +
    + +
    +
    + +
    +
    +
    + ) : null} +
    + + + + +
    + )} +
    + )} +
    + ); +}); + +export { showUserModal }; diff --git a/frontend/src/modals/WireGuardClientModal.tsx b/frontend/src/modals/WireGuardClientModal.tsx new file mode 100644 index 0000000..c55c67d --- /dev/null +++ b/frontend/src/modals/WireGuardClientModal.tsx @@ -0,0 +1,65 @@ +import EasyModal, { useModal } from "ez-modal-react"; +import { useState } from "react"; +import Modal from "react-bootstrap/Modal"; +import { Button } from "src/components"; + +const WireGuardClientModal = EasyModal.create(() => { + const modal = useModal(); + const [name, setName] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name.trim()) { + modal.resolve({ name: name.trim() }); + modal.hide(); + } + }; + + const handleClose = () => { + modal.resolve(null); + modal.hide(); + }; + + return ( + +
    { + e.stopPropagation(); + handleSubmit(e); + }}> + + New WireGuard Client + + +
    + + setName(e.target.value)} + autoFocus + required + /> +
    + A friendly name to identify this client. +
    +
    +
    + + + + +
    +
    + ); +}); + +export default WireGuardClientModal; diff --git a/frontend/src/modals/WireGuardQRModal.tsx b/frontend/src/modals/WireGuardQRModal.tsx new file mode 100644 index 0000000..017c717 --- /dev/null +++ b/frontend/src/modals/WireGuardQRModal.tsx @@ -0,0 +1,83 @@ +import EasyModal, { useModal } from "ez-modal-react"; +import { useEffect, useState } from "react"; +import Modal from "react-bootstrap/Modal"; +import { Button } from "src/components"; +import AuthStore from "src/modules/AuthStore"; + +interface WireGuardQRModalProps { + clientId: number; + clientName: string; +} + +const WireGuardQRModal = EasyModal.create((props: WireGuardQRModalProps) => { + const modal = useModal(); + const [svgContent, setSvgContent] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchQR = async () => { + try { + const headers: Record = {}; + if (AuthStore.token) { + headers.Authorization = `Bearer ${AuthStore.token.token}`; + } + const res = await fetch( + `/api/wireguard/client/${props.clientId}/qrcode.svg`, + { headers }, + ); + if (!res.ok) { + throw new Error("Failed to load QR code"); + } + const svg = await res.text(); + setSvgContent(svg); + } catch (err: any) { + setError(err.message || "Failed to load QR code"); + } finally { + setLoading(false); + } + }; + fetchQR(); + }, [props.clientId]); + + return ( + modal.hide()} centered size="sm" backdrop="static"> + + QR Code: {props.clientName} + + + {loading && ( +
    +
    +
    Loading QR Code...
    +
    + )} + {error && ( +
    +

    {error}

    + + QR code generation requires qrencode to be installed in the + container. You can still download the configuration file. + +
    + )} + {!loading && !error && svgContent && ( +
    + )} +
    + Scan this QR code with the WireGuard app on your device. +
    + + + + + + ); +}); + +export default WireGuardQRModal; diff --git a/frontend/src/modals/index.ts b/frontend/src/modals/index.ts new file mode 100644 index 0000000..a06a0c0 --- /dev/null +++ b/frontend/src/modals/index.ts @@ -0,0 +1,17 @@ +export * from "./AccessListModal"; +export * from "./ChangePasswordModal"; +export * from "./CustomCertificateModal"; +export * from "./DeadHostModal"; +export * from "./DeleteConfirmModal"; +export * from "./DNSCertificateModal"; +export * from "./EventDetailsModal"; +export * from "./HelpModal"; +export * from "./HTTPCertificateModal"; +export * from "./PermissionsModal"; +export * from "./ProxyHostModal"; +export * from "./RedirectionHostModal"; +export * from "./RenewCertificateModal"; +export * from "./SetPasswordModal"; +export * from "./StreamModal"; +export * from "./TwoFactorModal"; +export * from "./UserModal"; diff --git a/frontend/src/modules/AuthStore.ts b/frontend/src/modules/AuthStore.ts new file mode 100644 index 0000000..9978aaa --- /dev/null +++ b/frontend/src/modules/AuthStore.ts @@ -0,0 +1,95 @@ +import { getUnixTime, parseISO } from "date-fns"; +import type { TokenResponse } from "src/api/backend"; + +export const TOKEN_KEY = "authentications"; + +export class AuthStore { + // Get all tokens from stack + get tokens() { + const t = localStorage.getItem(TOKEN_KEY); + let tokens = []; + if (t !== null) { + try { + tokens = JSON.parse(t); + } catch (e) { + console.error("Failed to parse tokens from localStorage", e); + } + } + return tokens; + } + + // Get last token from stack + get token() { + const t = this.tokens; + if (t.length) { + return t[t.length - 1]; + } + return null; + } + + // Get expires from last token + get expires() { + const t = this.token; + if (t && typeof t.expires !== "undefined") { + const expires = Number(t.expires); + if (expires && !Number.isNaN(expires)) { + return expires; + } + } + return null; + } + + // Filter out invalid tokens and return true if we find one that is valid + // hasActiveToken() { + // const t = this.tokens; + // return t.length > 0; + // } + // Start from the END of the stack and work backwards + hasActiveToken() { + const t = this.tokens; + if (!t.length) { + return false; + } + + const now = Math.round(Date.now() / 1000); + const oneMinuteBuffer = 60; + for (let i = t.length - 1; i >= 0; i--) { + const dte = getUnixTime(parseISO(t[i].expires)); + const valid = dte - oneMinuteBuffer > now; + if (valid) { + return true; + } + this.drop(); + } + return false; + } + + // Set a single token on the stack + set({ token, expires }: TokenResponse) { + localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }])); + } + + // Add a token to the END of the stack + add({ token, expires }: TokenResponse) { + const t = this.tokens; + t.push({ token, expires }); + localStorage.setItem(TOKEN_KEY, JSON.stringify(t)); + } + + // Drop a token from the END of the stack + drop() { + const t = this.tokens; + t.splice(-1, 1); + localStorage.setItem(TOKEN_KEY, JSON.stringify(t)); + } + + clear() { + localStorage.removeItem(TOKEN_KEY); + } + + count() { + return this.tokens.length; + } +} + +export default new AuthStore(); diff --git a/frontend/src/modules/Permissions.ts b/frontend/src/modules/Permissions.ts new file mode 100644 index 0000000..2d78421 --- /dev/null +++ b/frontend/src/modules/Permissions.ts @@ -0,0 +1,49 @@ +import type { UserPermissions } from "src/api/backend"; + +export const ADMIN = "admin"; +export const VISIBILITY = "visibility"; +export const PROXY_HOSTS = "proxyHosts"; +export const REDIRECTION_HOSTS = "redirectionHosts"; +export const DEAD_HOSTS = "deadHosts"; +export const STREAMS = "streams"; +export const CERTIFICATES = "certificates"; +export const ACCESS_LISTS = "accessLists"; + +export const MANAGE = "manage"; +export const VIEW = "view"; +export const HIDDEN = "hidden"; + +export const ALL = "all"; +export const USER = "user"; + +export type Section = + | typeof ADMIN + | typeof VISIBILITY + | typeof PROXY_HOSTS + | typeof REDIRECTION_HOSTS + | typeof DEAD_HOSTS + | typeof STREAMS + | typeof CERTIFICATES + | typeof ACCESS_LISTS; + +export type Permission = typeof MANAGE | typeof VIEW; + +const hasPermission = ( + section: Section, + perm: Permission, + userPerms: UserPermissions | undefined, + roles: string[] | undefined, +): boolean => { + if (!userPerms) return false; + if (isAdmin(roles)) return true; + const acceptable = [MANAGE, perm]; + // @ts-expect-error 7053 + const v = typeof userPerms[section] !== "undefined" ? userPerms[section] : HIDDEN; + return acceptable.indexOf(v) !== -1; +}; + +const isAdmin = (roles: string[] | undefined): boolean => { + return roles?.includes("admin") || false; +}; + +export { hasPermission, isAdmin }; diff --git a/frontend/src/modules/Validations.tsx b/frontend/src/modules/Validations.tsx new file mode 100644 index 0000000..3b29081 --- /dev/null +++ b/frontend/src/modules/Validations.tsx @@ -0,0 +1,103 @@ +import { intl } from "src/locale"; + +const validateString = (minLength = 0, maxLength = 0) => { + if (minLength <= 0 && maxLength <= 0) { + // this doesn't require translation + console.error("validateString() must be called with a min or max or both values in order to work!"); + } + + return (value: string): string | undefined => { + if (minLength && (typeof value === "undefined" || !value.length)) { + return intl.formatMessage({ id: "error.required" }); + } + if (minLength && value.length < minLength) { + return intl.formatMessage({ id: "error.min-character-length" }, { min: minLength }); + } + if (maxLength && (typeof value === "undefined" || value.length > maxLength)) { + return intl.formatMessage({ id: "error.max-character-length" }, { max: maxLength }); + } + }; +}; + +const validateNumber = (min = -1, max = -1) => { + if (min === -1 && max === -1) { + // this doesn't require translation + console.error("validateNumber() must be called with a min or max or both values in order to work!"); + } + + return (value: string): string | undefined => { + const int: number = +value; + if (min > -1 && !int) { + return intl.formatMessage({ id: "error.required" }); + } + if (min > -1 && int < min) { + return intl.formatMessage({ id: "error.minimum" }, { min }); + } + if (max > -1 && int > max) { + return intl.formatMessage({ id: "error.maximum" }, { max }); + } + }; +}; + +const validateEmail = () => { + return (value: string): string | undefined => { + if (!value.length) { + return intl.formatMessage({ id: "error.required" }); + } + if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+$/i.test(value)) { + return intl.formatMessage({ id: "error.invalid-email" }); + } + }; +}; + +const validateDomain = (allowWildcards = false) => { + return (d: string): boolean => { + const dom = d.trim().toLowerCase(); + + if (dom.length < 3) { + return false; + } + + // Prevent wildcards + if (!allowWildcards && dom.indexOf("*") !== -1) { + return false; + } + + // Prevent duplicate * in domain + if ((dom.match(/\*/g) || []).length > 1) { + return false; + } + + // Prevent some invalid characters + if ((dom.match(/(@|,|!|&|\$|#|%|\^|\(|\))/g) || []).length > 0) { + return false; + } + + // This will match *.com type domains, + return dom.match(/\*\.[^.]+$/m) === null; + }; +}; + +const validateDomains = (allowWildcards = false, maxDomains?: number) => { + const vDom = validateDomain(allowWildcards); + + return (value?: string[]): string | undefined => { + if (!value?.length) { + return intl.formatMessage({ id: "error.required" }); + } + + // Deny if the list of domains is hit + if (maxDomains && value?.length >= maxDomains) { + return intl.formatMessage({ id: "error.max-domains" }, { max: maxDomains }); + } + + // validate each domain + for (let i = 0; i < value?.length; i++) { + if (!vDom(value[i])) { + return intl.formatMessage({ id: "error.invalid-domain" }, { domain: value[i] }); + } + } + }; +}; + +export { validateEmail, validateNumber, validateString, validateDomains, validateDomain }; diff --git a/frontend/src/notifications/Msg.module.css b/frontend/src/notifications/Msg.module.css new file mode 100644 index 0000000..24c5592 --- /dev/null +++ b/frontend/src/notifications/Msg.module.css @@ -0,0 +1,14 @@ +.toaster { + padding: 0; + background: transparent !important; + box-shadow: none !important; + border: none !important; + + &.toast { + border-radius: 0; + box-shadow: none; + font-size: 14px; + padding: 16px 24px; + background: transparent; + } +} diff --git a/frontend/src/notifications/Msg.tsx b/frontend/src/notifications/Msg.tsx new file mode 100644 index 0000000..b68b867 --- /dev/null +++ b/frontend/src/notifications/Msg.tsx @@ -0,0 +1,36 @@ +import { IconCheck, IconExclamationCircle } from "@tabler/icons-react"; +import cn from "classnames"; +import type { ReactNode } from "react"; + +function Msg({ data }: any) { + const cns = cn("toast", "show", data.type || null); + + let icon: ReactNode = null; + switch (data.type) { + case "success": + icon = ; + break; + case "error": + icon = ; + break; + } + + return ( +
    + {data.title && ( +
    + {icon} {data.title} +
    + )} +
    {data.message}
    +
    + ); +} +export { Msg }; diff --git a/frontend/src/notifications/helpers.tsx b/frontend/src/notifications/helpers.tsx new file mode 100644 index 0000000..07517e4 --- /dev/null +++ b/frontend/src/notifications/helpers.tsx @@ -0,0 +1,38 @@ +import { toast } from "react-toastify"; +import { intl } from "src/locale"; +import { Msg } from "./Msg"; +import styles from "./Msg.module.css"; + +const showSuccess = (message: string) => { + toast(Msg, { + className: styles.toaster, + data: { + type: "success", + title: intl.formatMessage({ id: "notification.success" }), + message, + }, + }); +}; + +const showError = (message: string) => { + toast(, { + data: { + type: "error", + title: intl.formatMessage({ id: "notification.error" }), + message, + }, + }); +}; + +const showObjectSuccess = (obj: string, action: string) => { + showSuccess( + intl.formatMessage( + { + id: `notification.object-${action}`, + }, + { object: intl.formatMessage({ id: obj }) }, + ), + ); +}; + +export { showSuccess, showError, showObjectSuccess }; diff --git a/frontend/src/notifications/index.ts b/frontend/src/notifications/index.ts new file mode 100644 index 0000000..d4e09d7 --- /dev/null +++ b/frontend/src/notifications/index.ts @@ -0,0 +1 @@ +export * from "./helpers"; diff --git a/frontend/src/pages/Access/Table.tsx b/frontend/src/pages/Access/Table.tsx new file mode 100644 index 0000000..433c077 --- /dev/null +++ b/frontend/src/pages/Access/Table.tsx @@ -0,0 +1,144 @@ +import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { AccessList } from "src/api/backend"; +import { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; +import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions"; + +interface Props { + data: AccessList[]; + isFiltered?: boolean; + isFetching?: boolean; + onEdit?: (id: number) => void; + onDelete?: (id: number) => void; + onNew?: () => void; +} +export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onNew }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row.owner, { + id: "owner", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "name", + header: intl.formatMessage({ id: "column.name" }), + cell: (info: any) => ( + + ), + }), + columnHelper.accessor((row: any) => row.items, { + id: "items", + header: intl.formatMessage({ id: "column.authorization" }), + cell: (info: any) => , + }), + columnHelper.accessor((row: any) => row.clients, { + id: "clients", + header: intl.formatMessage({ id: "column.access" }), + cell: (info: any) => , + }), + columnHelper.accessor((row: any) => row.satisfyAny, { + id: "satisfyAny", + header: intl.formatMessage({ id: "column.satisfy" }), + cell: (info: any) => , + }), + columnHelper.accessor((row: any) => row.proxyHostCount, { + id: "proxyHostCount", + header: intl.formatMessage({ id: "proxy-hosts" }), + cell: (info: any) => , + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onEdit?.(info.row.original.id); + }} + > + + + + + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onEdit, onDelete], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Access/TableWrapper.tsx b/frontend/src/pages/Access/TableWrapper.tsx new file mode 100644 index 0000000..1e028f4 --- /dev/null +++ b/frontend/src/pages/Access/TableWrapper.tsx @@ -0,0 +1,104 @@ +import { IconHelp, IconSearch } from "@tabler/icons-react"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteAccessList } from "src/api/backend"; +import { Button, HasPermission, LoadingPage } from "src/components"; +import { useAccessLists } from "src/hooks"; +import { T } from "src/locale"; +import { showAccessListModal, showDeleteConfirmModal, showHelpModal } from "src/modals"; +import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions"; +import { showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const [search, setSearch] = useState(""); + const { isFetching, isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleDelete = async (id: number) => { + await deleteAccessList(id); + showObjectSuccess("access-list", "deleted"); + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter((item) => { + return item.name.toLowerCase().includes(search); + }); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    + +
    +
    + {data?.length ? ( +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + ) : null} + + + {data?.length ? ( + + ) : null} + +
    +
    +
    +
    + showAccessListModal(id)} + onDelete={(id: number) => + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["access-lists"], ["access-list", id]], + children: , + }) + } + onNew={() => showAccessListModal("new")} + /> + + + ); +} diff --git a/frontend/src/pages/Access/index.tsx b/frontend/src/pages/Access/index.tsx new file mode 100644 index 0000000..9c4624c --- /dev/null +++ b/frontend/src/pages/Access/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { ACCESS_LISTS, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const Access = () => { + return ( + + + + ); +}; + +export default Access; diff --git a/frontend/src/pages/AuditLog/Table.tsx b/frontend/src/pages/AuditLog/Table.tsx new file mode 100644 index 0000000..f61809a --- /dev/null +++ b/frontend/src/pages/AuditLog/Table.tsx @@ -0,0 +1,70 @@ +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { AuditLog } from "src/api/backend"; +import { EventFormatter, GravatarFormatter } from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; + +interface Props { + data: AuditLog[]; + isFetching?: boolean; + onSelectItem?: (id: number) => void; +} +export default function Table({ data, isFetching, onSelectItem }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: AuditLog) => row.user, { + id: "user.avatar", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: AuditLog) => row, { + id: "objectType", + header: intl.formatMessage({ id: "column.event" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onSelectItem], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ; +} diff --git a/frontend/src/pages/AuditLog/TableWrapper.tsx b/frontend/src/pages/AuditLog/TableWrapper.tsx new file mode 100644 index 0000000..a7d3967 --- /dev/null +++ b/frontend/src/pages/AuditLog/TableWrapper.tsx @@ -0,0 +1,36 @@ +import Alert from "react-bootstrap/Alert"; +import { LoadingPage } from "src/components"; +import { useAuditLogs } from "src/hooks"; +import { T } from "src/locale"; +import { showEventDetailsModal } from "src/modals"; +import Table from "./Table"; + +export default function TableWrapper() { + const { isFetching, isLoading, isError, error, data } = useAuditLogs(["user"]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    +
    +
    +
    + + + ); +} diff --git a/frontend/src/pages/AuditLog/index.tsx b/frontend/src/pages/AuditLog/index.tsx new file mode 100644 index 0000000..4bdc2d0 --- /dev/null +++ b/frontend/src/pages/AuditLog/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { ADMIN, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const AuditLog = () => { + return ( + + + + ); +}; + +export default AuditLog; diff --git a/frontend/src/pages/Certificates/Table.tsx b/frontend/src/pages/Certificates/Table.tsx new file mode 100644 index 0000000..7eaeb77 --- /dev/null +++ b/frontend/src/pages/Certificates/Table.tsx @@ -0,0 +1,235 @@ +import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { Certificate } from "src/api/backend"; +import { + CertificateInUseFormatter, + DateFormatter, + DomainsFormatter, + EmptyData, + GravatarFormatter, + HasPermission, +} from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; +import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals"; +import { CERTIFICATES, MANAGE } from "src/modules/Permissions"; + +interface Props { + data: Certificate[]; + isFiltered?: boolean; + isFetching?: boolean; + onDelete?: (id: number) => void; + onRenew?: (id: number) => void; + onDownload?: (id: number) => void; +} +export default function Table({ data, isFetching, onDelete, onRenew, onDownload, isFiltered }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row.owner, { + id: "owner", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "domainNames", + header: intl.formatMessage({ id: "column.name" }), + cell: (info: any) => { + const value = info.getValue(); + return ( + + ); + }, + }), + columnHelper.accessor((row: any) => row, { + id: "provider", + header: intl.formatMessage({ id: "column.provider" }), + cell: (info: any) => { + const r = info.getValue(); + if (r.provider === "letsencrypt") { + if (r.meta?.dnsChallenge && r.meta?.dnsProvider) { + return ( + <> + – {r.meta?.dnsProvider} + + ); + } + return ; + } + if (r.provider === "other") { + return ; + } + return ; + }, + }), + columnHelper.accessor((row: any) => row.expiresOn, { + id: "expiresOn", + header: intl.formatMessage({ id: "column.expires" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row, { + id: "proxyHosts", + header: intl.formatMessage({ id: "column.status" }), + cell: (info: any) => { + const r = info.getValue(); + return ( + + ); + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onRenew?.(info.row.original.id); + }} + > + + + + + { + e.preventDefault(); + onDownload?.(info.row.original.id); + }} + > + + + + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onDelete, onRenew, onDownload], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + const customAddBtn = ( +
    + + + ); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Certificates/TableWrapper.tsx b/frontend/src/pages/Certificates/TableWrapper.tsx new file mode 100644 index 0000000..14dfc41 --- /dev/null +++ b/frontend/src/pages/Certificates/TableWrapper.tsx @@ -0,0 +1,161 @@ +import { IconHelp, IconSearch } from "@tabler/icons-react"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteCertificate, downloadCertificate } from "src/api/backend"; +import { Button, HasPermission, LoadingPage } from "src/components"; +import { useCertificates } from "src/hooks"; +import { T } from "src/locale"; +import { + showCustomCertificateModal, + showDeleteConfirmModal, + showDNSCertificateModal, + showHelpModal, + showHTTPCertificateModal, + showRenewCertificateModal, +} from "src/modals"; +import { CERTIFICATES, MANAGE } from "src/modules/Permissions"; +import { showError, showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const [search, setSearch] = useState(""); + const { isFetching, isLoading, isError, error, data } = useCertificates([ + "owner", + "dead_hosts", + "proxy_hosts", + "redirection_hosts", + "streams", + ]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleDelete = async (id: number) => { + await deleteCertificate(id); + showObjectSuccess("certificate", "deleted"); + }; + + const handleDownload = async (id: number) => { + try { + await downloadCertificate(id); + } catch (err: any) { + showError(err.message); + } + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter( + (item) => + item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) || + item.niceName.toLowerCase().includes(search), + ); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    +
    +
    + {data?.length ? ( +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + ) : null} + + + {data?.length ? ( + +
    +
    +
    +
    + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["certificates"], ["certificate", id]], + children: , + }) + } + /> + + + ); +} diff --git a/frontend/src/pages/Certificates/index.tsx b/frontend/src/pages/Certificates/index.tsx new file mode 100644 index 0000000..130fb5a --- /dev/null +++ b/frontend/src/pages/Certificates/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { CERTIFICATES, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const Certificates = () => { + return ( + + + + ); +}; + +export default Certificates; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx new file mode 100644 index 0000000..5cb6486 --- /dev/null +++ b/frontend/src/pages/Dashboard/index.tsx @@ -0,0 +1,132 @@ +import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react"; +import { useNavigate } from "react-router-dom"; +import { HasPermission } from "src/components"; +import { useHostReport } from "src/hooks"; +import { T } from "src/locale"; +import { DEAD_HOSTS, PROXY_HOSTS, REDIRECTION_HOSTS, STREAMS, VIEW } from "src/modules/Permissions"; + +const Dashboard = () => { + const { data: hostReport } = useHostReport(); + const navigate = useNavigate(); + + return ( + + ); +}; + +export default Dashboard; diff --git a/frontend/src/pages/Login/index.module.css b/frontend/src/pages/Login/index.module.css new file mode 100644 index 0000000..d033dc0 --- /dev/null +++ b/frontend/src/pages/Login/index.module.css @@ -0,0 +1,3 @@ +.logo { + width: 200px; +} diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx new file mode 100644 index 0000000..ebf7eeb --- /dev/null +++ b/frontend/src/pages/Login/index.tsx @@ -0,0 +1,204 @@ +import { Field, Form, Formik } from "formik"; +import { useEffect, useRef, useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { Button, LocalePicker, Page, ThemeSwitcher } from "src/components"; +import { useAuthState } from "src/context"; +import { useHealth } from "src/hooks"; +import { intl, T } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; +import styles from "./index.module.css"; + +function TwoFactorForm() { + const codeRef = useRef(null); + const [formErr, setFormErr] = useState(""); + const { verifyTwoFactor, cancelTwoFactor } = useAuthState(); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + setFormErr(""); + try { + await verifyTwoFactor(values.code); + } catch (err) { + if (err instanceof Error) { + setFormErr(err.message); + } + } + setSubmitting(false); + }; + + useEffect(() => { + codeRef.current?.focus(); + }, []); + + return ( + <> +

    + +

    +

    + +

    + {formErr !== "" && {formErr}} + + {({ isSubmitting }) => ( +
    +
    + + {({ field, form }: any) => ( + + )} + +
    +
    + + +
    + + )} +
    + + ); +} + +function LoginForm() { + const emailRef = useRef(null); + const [formErr, setFormErr] = useState(""); + const { login } = useAuthState(); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + setFormErr(""); + try { + await login(values.email, values.password); + } catch (err) { + if (err instanceof Error) { + setFormErr(err.message); + } + } + setSubmitting(false); + }; + + useEffect(() => { + emailRef.current?.focus(); + }, []); + + return ( + <> +

    + +

    + {formErr !== "" && {formErr}} + + {({ isSubmitting }) => ( +
    +
    + + {({ field, form }: any) => ( + + )} + +
    +
    + + {({ field, form }: any) => ( + <> + + + )} + +
    +
    + +
    + + )} +
    + + ); +} + +export default function Login() { + const { twoFactorChallenge } = useAuthState(); + const health = useHealth(); + + const getVersion = () => { + if (!health.data) { + return ""; + } + const v = health.data.version; + return `v${v.major}.${v.minor}.${v.revision}`; + }; + + return ( + +
    +
    + Nginx Proxy Manager +
    + + +
    +
    +
    +
    + {twoFactorChallenge ? : } +
    +
    +
    {getVersion()}
    +
    +
    + ); +} diff --git a/frontend/src/pages/Nginx/DeadHosts/Table.tsx b/frontend/src/pages/Nginx/DeadHosts/Table.tsx new file mode 100644 index 0000000..f8a1a27 --- /dev/null +++ b/frontend/src/pages/Nginx/DeadHosts/Table.tsx @@ -0,0 +1,158 @@ +import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { DeadHost } from "src/api/backend"; +import { + CertificateFormatter, + DomainsFormatter, + EmptyData, + GravatarFormatter, + HasPermission, + TrueFalseFormatter, +} from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; +import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions"; + +interface Props { + data: DeadHost[]; + isFiltered?: boolean; + isFetching?: boolean; + onEdit?: (id: number) => void; + onDelete?: (id: number) => void; + onDisableToggle?: (id: number, enabled: boolean) => void; + onNew?: () => void; +} +export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row.owner, { + id: "owner", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "domainNames", + header: intl.formatMessage({ id: "column.source" }), + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + }), + columnHelper.accessor((row: any) => row.certificate, { + id: "certificate", + header: intl.formatMessage({ id: "column.ssl" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.enabled, { + id: "enabled", + header: intl.formatMessage({ id: "column.status" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onEdit?.(info.row.original.id); + }} + > + + + + + { + e.preventDefault(); + onDisableToggle?.(info.row.original.id, !info.row.original.enabled); + }} + > + + + + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onDelete, onEdit, onDisableToggle], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx b/frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx new file mode 100644 index 0000000..07dd131 --- /dev/null +++ b/frontend/src/pages/Nginx/DeadHosts/TableWrapper.tsx @@ -0,0 +1,110 @@ +import { IconHelp, IconSearch } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteDeadHost, toggleDeadHost } from "src/api/backend"; +import { Button, HasPermission, LoadingPage } from "src/components"; +import { useDeadHosts } from "src/hooks"; +import { T } from "src/locale"; +import { showDeadHostModal, showDeleteConfirmModal, showHelpModal } from "src/modals"; +import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions"; +import { showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleDelete = async (id: number) => { + await deleteDeadHost(id); + showObjectSuccess("dead-host", "deleted"); + }; + + const handleDisableToggle = async (id: number, enabled: boolean) => { + await toggleDeadHost(id, enabled); + queryClient.invalidateQueries({ queryKey: ["dead-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["dead-host", id] }); + showObjectSuccess("dead-host", enabled ? "enabled" : "disabled"); + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter((item) => { + return item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)); + }); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    + +
    +
    + {data?.length ? ( +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + ) : null} + + + {data?.length ? ( + + ) : null} + +
    +
    +
    +
    +
    showDeadHostModal(id)} + onDelete={(id: number) => + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["dead-hosts"], ["dead-host", id]], + children: , + }) + } + onDisableToggle={handleDisableToggle} + onNew={() => showDeadHostModal("new")} + /> + + + ); +} diff --git a/frontend/src/pages/Nginx/DeadHosts/index.tsx b/frontend/src/pages/Nginx/DeadHosts/index.tsx new file mode 100644 index 0000000..0464b7a --- /dev/null +++ b/frontend/src/pages/Nginx/DeadHosts/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { DEAD_HOSTS, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const DeadHosts = () => { + return ( + + + + ); +}; + +export default DeadHosts; diff --git a/frontend/src/pages/Nginx/ProxyHosts/Table.tsx b/frontend/src/pages/Nginx/ProxyHosts/Table.tsx new file mode 100644 index 0000000..9d58b26 --- /dev/null +++ b/frontend/src/pages/Nginx/ProxyHosts/Table.tsx @@ -0,0 +1,174 @@ +import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { ProxyHost } from "src/api/backend"; +import { + AccessListFormatter, + CertificateFormatter, + DomainsFormatter, + EmptyData, + GravatarFormatter, + HasPermission, + TrueFalseFormatter, +} from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; +import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions"; + +interface Props { + data: ProxyHost[]; + isFiltered?: boolean; + isFetching?: boolean; + onEdit?: (id: number) => void; + onDelete?: (id: number) => void; + onDisableToggle?: (id: number, enabled: boolean) => void; + onNew?: () => void; +} +export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row.owner, { + id: "owner", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "domainNames", + header: intl.formatMessage({ id: "column.source" }), + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + }), + columnHelper.accessor((row: any) => row, { + id: "forwardHost", + header: intl.formatMessage({ id: "column.destination" }), + cell: (info: any) => { + const value = info.getValue(); + return `${value.forwardScheme}://${value.forwardHost}:${value.forwardPort}`; + }, + }), + columnHelper.accessor((row: any) => row.certificate, { + id: "certificate", + header: intl.formatMessage({ id: "column.ssl" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.accessList, { + id: "accessList", + header: intl.formatMessage({ id: "column.access" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.enabled, { + id: "enabled", + header: intl.formatMessage({ id: "column.status" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onEdit?.(info.row.original.id); + }} + > + + + + + { + e.preventDefault(); + onDisableToggle?.(info.row.original.id, !info.row.original.enabled); + }} + > + + + + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onEdit, onDisableToggle, onDelete], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx new file mode 100644 index 0000000..68af43e --- /dev/null +++ b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx @@ -0,0 +1,116 @@ +import { IconHelp, IconSearch } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteProxyHost, toggleProxyHost } from "src/api/backend"; +import { Button, HasPermission, LoadingPage } from "src/components"; +import { useProxyHosts } from "src/hooks"; +import { T } from "src/locale"; +import { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from "src/modals"; +import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions"; +import { showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const { isFetching, isLoading, isError, error, data } = useProxyHosts(["owner", "access_list", "certificate"]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleDelete = async (id: number) => { + await deleteProxyHost(id); + showObjectSuccess("proxy-host", "deleted"); + }; + + const handleDisableToggle = async (id: number, enabled: boolean) => { + await toggleProxyHost(id, enabled); + queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["proxy-host", id] }); + showObjectSuccess("proxy-host", enabled ? "enabled" : "disabled"); + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter( + (item) => + item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) || + item.forwardHost.toLowerCase().includes(search) || + `${item.forwardPort}`.includes(search), + ); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    +
    +
    + {data?.length ? ( +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + ) : null} + + + {data?.length ? ( + + ) : null} + +
    +
    +
    +
    +
    showProxyHostModal(id)} + onDelete={(id: number) => + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["proxy-hosts"], ["proxy-host", id]], + children: , + }) + } + onDisableToggle={handleDisableToggle} + onNew={() => showProxyHostModal("new")} + /> + + + ); +} diff --git a/frontend/src/pages/Nginx/ProxyHosts/index.tsx b/frontend/src/pages/Nginx/ProxyHosts/index.tsx new file mode 100644 index 0000000..d20658e --- /dev/null +++ b/frontend/src/pages/Nginx/ProxyHosts/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { PROXY_HOSTS, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const ProxyHosts = () => { + return ( + + + + ); +}; + +export default ProxyHosts; diff --git a/frontend/src/pages/Nginx/RedirectionHosts/Table.tsx b/frontend/src/pages/Nginx/RedirectionHosts/Table.tsx new file mode 100644 index 0000000..6ac4152 --- /dev/null +++ b/frontend/src/pages/Nginx/RedirectionHosts/Table.tsx @@ -0,0 +1,179 @@ +import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { RedirectionHost } from "src/api/backend"; +import { + CertificateFormatter, + DomainsFormatter, + EmptyData, + GravatarFormatter, + HasPermission, + TrueFalseFormatter, +} from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; +import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions"; + +interface Props { + data: RedirectionHost[]; + isFiltered?: boolean; + isFetching?: boolean; + onEdit?: (id: number) => void; + onDelete?: (id: number) => void; + onDisableToggle?: (id: number, enabled: boolean) => void; + onNew?: () => void; +} +export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row.owner, { + id: "owner", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "domainNames", + header: intl.formatMessage({ id: "column.source" }), + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + }), + columnHelper.accessor((row: any) => row.forwardHttpCode, { + id: "forwardHttpCode", + header: intl.formatMessage({ id: "column.http-code" }), + cell: (info: any) => { + return info.getValue(); + }, + }), + columnHelper.accessor((row: any) => row.forwardScheme, { + id: "forwardScheme", + header: intl.formatMessage({ id: "column.scheme" }), + cell: (info: any) => { + return info.getValue().toUpperCase(); + }, + }), + columnHelper.accessor((row: any) => row.forwardDomainName, { + id: "forwardDomainName", + header: intl.formatMessage({ id: "column.destination" }), + cell: (info: any) => { + return info.getValue(); + }, + }), + columnHelper.accessor((row: any) => row.certificate, { + id: "certificate", + header: intl.formatMessage({ id: "column.ssl" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.enabled, { + id: "enabled", + header: intl.formatMessage({ id: "column.status" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onEdit?.(info.row.original.id); + }} + > + + + + + { + e.preventDefault(); + onDisableToggle?.(info.row.original.id, !info.row.original.enabled); + }} + > + + + + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onEdit, onDisableToggle, onDelete], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx b/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx new file mode 100644 index 0000000..382b733 --- /dev/null +++ b/frontend/src/pages/Nginx/RedirectionHosts/TableWrapper.tsx @@ -0,0 +1,116 @@ +import { IconHelp, IconSearch } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend"; +import { Button, HasPermission, LoadingPage } from "src/components"; +import { useRedirectionHosts } from "src/hooks"; +import { T } from "src/locale"; +import { showDeleteConfirmModal, showHelpModal, showRedirectionHostModal } from "src/modals"; +import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions"; +import { showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const { isFetching, isLoading, isError, error, data } = useRedirectionHosts(["owner", "certificate"]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleDelete = async (id: number) => { + await deleteRedirectionHost(id); + showObjectSuccess("redirection-host", "deleted"); + }; + + const handleDisableToggle = async (id: number, enabled: boolean) => { + await toggleRedirectionHost(id, enabled); + queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] }); + queryClient.invalidateQueries({ queryKey: ["redirection-host", id] }); + showObjectSuccess("redirection-host", enabled ? "enabled" : "disabled"); + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter((item) => { + return ( + item.domainNames.some((domain: string) => domain.toLowerCase().includes(search)) || + item.forwardDomainName.toLowerCase().includes(search) + ); + }); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    +
    +
    + {data?.length ? ( +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + ) : null} + + + {data?.length ? ( + + ) : null} + +
    +
    +
    +
    +
    showRedirectionHostModal(id)} + onDelete={(id: number) => + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["redirection-hosts"], ["redirection-host", id]], + children: , + }) + } + onDisableToggle={handleDisableToggle} + onNew={() => showRedirectionHostModal("new")} + /> + + + ); +} diff --git a/frontend/src/pages/Nginx/RedirectionHosts/index.tsx b/frontend/src/pages/Nginx/RedirectionHosts/index.tsx new file mode 100644 index 0000000..32bf244 --- /dev/null +++ b/frontend/src/pages/Nginx/RedirectionHosts/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { REDIRECTION_HOSTS, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const RedirectionHosts = () => { + return ( + + + + ); +}; + +export default RedirectionHosts; diff --git a/frontend/src/pages/Nginx/Streams/Table.tsx b/frontend/src/pages/Nginx/Streams/Table.tsx new file mode 100644 index 0000000..4b9ff7d --- /dev/null +++ b/frontend/src/pages/Nginx/Streams/Table.tsx @@ -0,0 +1,187 @@ +import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { Stream } from "src/api/backend"; +import { + CertificateFormatter, + EmptyData, + GravatarFormatter, + HasPermission, + TrueFalseFormatter, + ValueWithDateFormatter, +} from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; +import { MANAGE, STREAMS } from "src/modules/Permissions"; + +interface Props { + data: Stream[]; + isFiltered?: boolean; + isFetching?: boolean; + onEdit?: (id: number) => void; + onDelete?: (id: number) => void; + onDisableToggle?: (id: number, enabled: boolean) => void; + onNew?: () => void; +} +export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, onDisableToggle, onNew }: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row.owner, { + id: "owner", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "incomingPort", + header: intl.formatMessage({ id: "column.incoming-port" }), + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + }), + columnHelper.accessor((row: any) => row, { + id: "forwardHttpCode", + header: intl.formatMessage({ id: "column.destination" }), + cell: (info: any) => { + const value = info.getValue(); + return `${value.forwardingHost}:${value.forwardingPort}`; + }, + }), + columnHelper.accessor((row: any) => row, { + id: "tcpForwarding", + header: intl.formatMessage({ id: "column.protocol" }), + cell: (info: any) => { + const value = info.getValue(); + return ( + <> + {value.tcpForwarding ? ( + + + + ) : null} + {value.udpForwarding ? ( + + + + ) : null} + + ); + }, + }), + columnHelper.accessor((row: any) => row.certificate, { + id: "certificate", + header: intl.formatMessage({ id: "column.ssl" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.enabled, { + id: "enabled", + header: intl.formatMessage({ id: "column.status" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onEdit?.(info.row.original.id); + }} + > + + + + + { + e.preventDefault(); + onDisableToggle?.(info.row.original.id, !info.row.original.enabled); + }} + > + + + + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [columnHelper, onEdit, onDisableToggle, onDelete], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Nginx/Streams/TableWrapper.tsx b/frontend/src/pages/Nginx/Streams/TableWrapper.tsx new file mode 100644 index 0000000..ec9a8d4 --- /dev/null +++ b/frontend/src/pages/Nginx/Streams/TableWrapper.tsx @@ -0,0 +1,114 @@ +import { IconHelp, IconSearch } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteStream, toggleStream } from "src/api/backend"; +import { Button, HasPermission, LoadingPage } from "src/components"; +import { useStreams } from "src/hooks"; +import { T } from "src/locale"; +import { showDeleteConfirmModal, showHelpModal, showStreamModal } from "src/modals"; +import { MANAGE, STREAMS } from "src/modules/Permissions"; +import { showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const [_deleteId, _setDeleteIdd] = useState(0); + const { isFetching, isLoading, isError, error, data } = useStreams(["owner", "certificate"]); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleDelete = async (id: number) => { + await deleteStream(id); + showObjectSuccess("stream", "deleted"); + }; + + const handleDisableToggle = async (id: number, enabled: boolean) => { + await toggleStream(id, enabled); + queryClient.invalidateQueries({ queryKey: ["streams"] }); + queryClient.invalidateQueries({ queryKey: ["stream", id] }); + showObjectSuccess("stream", enabled ? "enabled" : "disabled"); + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter((item) => { + return ( + `${item.incomingPort}`.includes(search) || + `${item.forwardingPort}`.includes(search) || + item.forwardingHost.includes(search) + ); + }); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    +
    +
    + {data?.length ? ( +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + ) : null} + + + {data?.length ? ( + + ) : null} + +
    +
    +
    +
    +
    showStreamModal(id)} + onDelete={(id: number) => + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["streams"], ["stream", id]], + children: , + }) + } + onDisableToggle={handleDisableToggle} + onNew={() => showStreamModal("new")} + /> + + + ); +} diff --git a/frontend/src/pages/Nginx/Streams/index.tsx b/frontend/src/pages/Nginx/Streams/index.tsx new file mode 100644 index 0000000..31c4f63 --- /dev/null +++ b/frontend/src/pages/Nginx/Streams/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { STREAMS, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const Streams = () => { + return ( + + + + ); +}; + +export default Streams; diff --git a/frontend/src/pages/Settings/DefaultSite.tsx b/frontend/src/pages/Settings/DefaultSite.tsx new file mode 100644 index 0000000..1ee1cb5 --- /dev/null +++ b/frontend/src/pages/Settings/DefaultSite.tsx @@ -0,0 +1,269 @@ +import CodeEditor from "@uiw/react-textarea-code-editor"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import { Button, Loading } from "src/components"; +import { useSetSetting, useSetting } from "src/hooks"; +import { intl, T } from "src/locale"; +import { validateString } from "src/modules/Validations"; +import { showObjectSuccess } from "src/notifications"; + +export default function DefaultSite() { + const { data, isLoading, error } = useSetting("default-site"); + const { mutate: setSetting } = useSetSetting(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const payload = { + id: "default-site", + value: values.value, + meta: { + redirect: values.redirect, + html: values.html, + }, + }; + + setSetting(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showObjectSuccess("setting", "saved"); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + if (!isLoading && error) { + return ( +
    +
    + + {error.message} + +
    +
    + ); + } + + if (isLoading) { + return ( +
    +
    + +
    +
    + ); + } + + return ( + + {({ values }) => ( +
    +
    + setErrorMsg(null)} dismissible> + {errorMsg} + + + {({ field, form }: any) => ( +
    + +
    + + + + + +
    +
    + )} +
    + {values.value === "redirect" && ( + + {({ field, form }: any) => ( +
    + +
    + + {form.errors.redirect ? ( +
    + {form.errors.redirect && form.touched.redirect + ? form.errors.redirect + : null} +
    + ) : null} +
    +
    + )} +
    + )} + {values.value === "html" && ( + + {({ field, form }: any) => ( +
    + +
    + + {form.errors.html ? ( +
    + {form.errors.html && form.touched.html ? form.errors.html : null} +
    + ) : null} +
    +
    + )} +
    + )} +
    +
    +
    + +
    +
    + + )} +
    + ); +} diff --git a/frontend/src/pages/Settings/Layout.tsx b/frontend/src/pages/Settings/Layout.tsx new file mode 100644 index 0000000..a0a77db --- /dev/null +++ b/frontend/src/pages/Settings/Layout.tsx @@ -0,0 +1,40 @@ +import { T } from "src/locale"; +import DefaultSite from "./DefaultSite"; + +export default function Layout() { + // Taken from https://preview.tabler.io/settings.html + // Refer to that when updating this content + + return ( +
    +
    +
    +
    +
    +

    + +

    +
    +
    +
    + +
    + +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx new file mode 100644 index 0000000..7904cbc --- /dev/null +++ b/frontend/src/pages/Settings/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { ADMIN, VIEW } from "src/modules/Permissions"; +import Layout from "./Layout"; + +const Settings = () => { + return ( + + + + ); +}; + +export default Settings; diff --git a/frontend/src/pages/Setup/index.module.css b/frontend/src/pages/Setup/index.module.css new file mode 100644 index 0000000..16f8477 --- /dev/null +++ b/frontend/src/pages/Setup/index.module.css @@ -0,0 +1,10 @@ +.logo { + width: 200px; +} + +.helperBtns { + position: absolute; + top: 10px; + right: 10px; + z-index: 1000; +} diff --git a/frontend/src/pages/Setup/index.tsx b/frontend/src/pages/Setup/index.tsx new file mode 100644 index 0000000..4becf5f --- /dev/null +++ b/frontend/src/pages/Setup/index.tsx @@ -0,0 +1,196 @@ +import { useQueryClient } from "@tanstack/react-query"; +import cn from "classnames"; +import { Field, Form, Formik } from "formik"; +import { useState } from "react"; +import { Alert } from "react-bootstrap"; +import { createUser } from "src/api/backend"; +import { Button, LocalePicker, Page, ThemeSwitcher } from "src/components"; +import { useAuthState } from "src/context"; +import { intl, T } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; +import styles from "./index.module.css"; + +interface Payload { + name: string; + email: string; + password: string; +} + +export default function Setup() { + const queryClient = useQueryClient(); + const { login } = useAuthState(); + const [errorMsg, setErrorMsg] = useState(null); + + const onSubmit = async (values: Payload, { setSubmitting }: any) => { + setErrorMsg(null); + + // Set a nickname, which is the first word of the name + const nickname = values.name.split(" ")[0]; + + const { password, ...payload } = { + ...values, + ...{ + nickname, + auth: { + type: "password", + secret: values.password, + }, + }, + }; + + try { + const user = await createUser(payload, true); + if (user?.id) { + try { + await login(user.email, password); + // Trigger a Health change + await queryClient.refetchQueries({ queryKey: ["health"] }); + // window.location.reload(); + } catch (err: any) { + setErrorMsg(err.message); + } + } else { + setErrorMsg("cannot_create_user"); + } + } catch (err: any) { + setErrorMsg(err.message); + } + setSubmitting(false); + }; + + return ( + +
    + + +
    +
    +
    + Nginx Proxy Manager +
    +
    + setErrorMsg(null)} dismissible> + {errorMsg} + + + {({ isSubmitting }) => ( +
    +
    +

    + +

    +

    + +

    +
    +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.name ? ( +
    + {form.errors.name && form.touched.name + ? form.errors.name + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.email ? ( +
    + {form.errors.email && form.touched.email + ? form.errors.email + : null} +
    + ) : null} +
    + )} +
    +
    +
    + + {({ field, form }: any) => ( +
    + + + {form.errors.password ? ( +
    + {form.errors.password && form.touched.password + ? form.errors.password + : null} +
    + ) : null} +
    + )} +
    +
    +
    +
    + +
    + + )} +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Users/Table.tsx b/frontend/src/pages/Users/Table.tsx new file mode 100644 index 0000000..d07fc9b --- /dev/null +++ b/frontend/src/pages/Users/Table.tsx @@ -0,0 +1,245 @@ +import { + IconDotsVertical, + IconEdit, + IconLock, + IconLogin2, + IconPower, + IconShield, + IconTrash, +} from "@tabler/icons-react"; +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useMemo } from "react"; +import type { User } from "src/api/backend"; +import { + EmailFormatter, + EmptyData, + GravatarFormatter, + RolesFormatter, + TrueFalseFormatter, + ValueWithDateFormatter, +} from "src/components"; +import { TableLayout } from "src/components/Table/TableLayout"; +import { intl, T } from "src/locale"; + +interface Props { + data: User[]; + isFiltered?: boolean; + isFetching?: boolean; + currentUserId?: number; + onEditUser?: (id: number) => void; + onEditPermissions?: (id: number) => void; + onSetPassword?: (id: number) => void; + onDeleteUser?: (id: number) => void; + onDisableToggle?: (id: number, enabled: boolean) => void; + onNewUser?: () => void; + onLoginAs?: (id: number) => void; +} +export default function Table({ + data, + isFiltered, + isFetching, + currentUserId, + onEditUser, + onEditPermissions, + onSetPassword, + onDeleteUser, + onDisableToggle, + onNewUser, + onLoginAs, +}: Props) { + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor((row: any) => row, { + id: "avatar", + cell: (info: any) => { + const value = info.getValue(); + return ; + }, + meta: { + className: "w-1", + }, + }), + columnHelper.accessor((row: any) => row, { + id: "name", + header: intl.formatMessage({ id: "column.name" }), + cell: (info: any) => { + const value = info.getValue(); + // Hack to reuse domains formatter + return ( + + ); + }, + }), + columnHelper.accessor((row: any) => row.email, { + id: "email", + header: intl.formatMessage({ id: "column.email" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.roles, { + id: "roles", + header: intl.formatMessage({ id: "column.roles" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.accessor((row: any) => row.isDisabled, { + id: "isDisabled", + header: intl.formatMessage({ id: "column.status" }), + cell: (info: any) => { + return ; + }, + }), + columnHelper.display({ + id: "id", + cell: (info: any) => { + return ( + + +
    + + + + { + e.preventDefault(); + onEditUser?.(info.row.original.id); + }} + > + + + + {currentUserId !== info.row.original.id ? ( + <> + { + e.preventDefault(); + onEditPermissions?.(info.row.original.id); + }} + > + + + + { + e.preventDefault(); + onSetPassword?.(info.row.original.id); + }} + > + + + + { + e.preventDefault(); + onDisableToggle?.(info.row.original.id, info.row.original.isDisabled); + }} + > + + + + {info.row.original.isDisabled ? ( +
    + + +
    + ) : ( + { + e.preventDefault(); + onLoginAs?.(info.row.original.id); + }} + > + + + + )} + + + ); + }, + meta: { + className: "text-end w-1", + }, + }), + ], + [ + columnHelper, + currentUserId, + onEditUser, + onDisableToggle, + onDeleteUser, + onEditPermissions, + onSetPassword, + onLoginAs, + ], + ); + + const tableInstance = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + rowCount: data.length, + meta: { + isFetching, + }, + enableSortingRemoval: false, + }); + + return ( + + } + /> + ); +} diff --git a/frontend/src/pages/Users/TableWrapper.tsx b/frontend/src/pages/Users/TableWrapper.tsx new file mode 100644 index 0000000..461ed6a --- /dev/null +++ b/frontend/src/pages/Users/TableWrapper.tsx @@ -0,0 +1,123 @@ +import { IconSearch } from "@tabler/icons-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import Alert from "react-bootstrap/Alert"; +import { deleteUser, toggleUser } from "src/api/backend"; +import { Button, LoadingPage } from "src/components"; +import { useAuthState } from "src/context"; +import { useUser, useUsers } from "src/hooks"; +import { T } from "src/locale"; +import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals"; +import { showError, showObjectSuccess } from "src/notifications"; +import Table from "./Table"; + +export default function TableWrapper() { + const queryClient = useQueryClient(); + const { loginAs } = useAuthState(); + const [search, setSearch] = useState(""); + const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]); + const { data: currentUser } = useUser("me"); + + if (isLoading) { + return ; + } + + if (isError) { + return {error?.message || "Unknown error"}; + } + + const handleLoginAs = async (id: number) => { + try { + await loginAs(id); + } catch (err) { + if (err instanceof Error) { + showError(err.message); + } + } + }; + + const handleDelete = async (id: number) => { + await deleteUser(id); + showObjectSuccess("user", "deleted"); + }; + + const handleDisableToggle = async (id: number, enabled: boolean) => { + await toggleUser(id, enabled); + queryClient.invalidateQueries({ queryKey: ["users"] }); + queryClient.invalidateQueries({ queryKey: ["user", id] }); + showObjectSuccess("user", enabled ? "enabled" : "disabled"); + }; + + let filtered = null; + if (search && data) { + filtered = data?.filter((item) => { + return ( + item.name.toLowerCase().includes(search) || + item.nickname.toLowerCase().includes(search) || + item.email.toLowerCase().includes(search) + ); + }); + } else if (search !== "") { + // this can happen if someone deletes the last item while searching + setSearch(""); + } + + return ( +
    +
    +
    +
    +
    +
    +

    + +

    +
    + {data?.length ? ( +
    +
    +
    + + + + setSearch(e.target.value.toLowerCase().trim())} + /> +
    + + +
    +
    + ) : null} +
    +
    +
    showUserModal(id)} + onEditPermissions={(id: number) => showPermissionsModal(id)} + onSetPassword={(id: number) => showSetPasswordModal(id)} + onDeleteUser={(id: number) => + showDeleteConfirmModal({ + title: , + onConfirm: () => handleDelete(id), + invalidations: [["users"], ["user", id]], + children: , + }) + } + onDisableToggle={handleDisableToggle} + onNewUser={() => showUserModal("new")} + onLoginAs={handleLoginAs} + /> + + + ); +} diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx new file mode 100644 index 0000000..53b4366 --- /dev/null +++ b/frontend/src/pages/Users/index.tsx @@ -0,0 +1,13 @@ +import { HasPermission } from "src/components"; +import { ADMIN, VIEW } from "src/modules/Permissions"; +import TableWrapper from "./TableWrapper"; + +const Users = () => { + return ( + + + + ); +}; + +export default Users; diff --git a/frontend/src/pages/WireGuard/index.tsx b/frontend/src/pages/WireGuard/index.tsx new file mode 100644 index 0000000..dde3db1 --- /dev/null +++ b/frontend/src/pages/WireGuard/index.tsx @@ -0,0 +1,292 @@ +import { + IconPlus, + IconDownload, + IconQrcode, + IconPlayerPlay, + IconPlayerPause, + IconTrash, + IconNetwork, +} from "@tabler/icons-react"; +import EasyModal from "ez-modal-react"; +import { useState } from "react"; +import { downloadWgConfig } from "src/api/backend/wireguard"; +import { Loading } from "src/components"; +import { + useWgClients, + useWgInterface, + useCreateWgClient, + useDeleteWgClient, + useToggleWgClient, +} from "src/hooks/useWireGuard"; +import WireGuardClientModal from "src/modals/WireGuardClientModal"; +import WireGuardQRModal from "src/modals/WireGuardQRModal"; + +function formatBytes(bytes: number | null): string { + if (bytes === null || bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +} + +function timeAgo(date: string | null): string { + if (!date) return "Never"; + const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; +} + +function WireGuard() { + const { data: clients, isLoading: clientsLoading } = useWgClients(); + const { data: wgInterface, isLoading: ifaceLoading } = useWgInterface(); + const createClient = useCreateWgClient(); + const deleteClient = useDeleteWgClient(); + const toggleClient = useToggleWgClient(); + const [filter, setFilter] = useState(""); + + if (clientsLoading || ifaceLoading) { + return ; + } + + const filteredClients = clients?.filter( + (c) => + !filter || + c.name.toLowerCase().includes(filter.toLowerCase()) || + c.ipv4Address.includes(filter), + ); + + const handleNewClient = async () => { + const result = (await EasyModal.show(WireGuardClientModal)) as any; + if (result && result.name) { + createClient.mutate({ name: result.name }); + } + }; + + const handleDelete = async (id: number, name: string) => { + if (window.confirm(`Are you sure you want to delete client "${name}"?`)) { + deleteClient.mutate(id); + } + }; + + const handleToggle = (id: number, currentlyEnabled: boolean) => { + toggleClient.mutate({ id, enabled: !currentlyEnabled }); + }; + + const handleQR = (id: number, name: string) => { + EasyModal.show(WireGuardQRModal, { clientId: id, clientName: name }); + }; + + const handleDownload = (id: number, name: string) => { + const cleanName = name.replace(/[^a-zA-Z0-9_.-]/g, "-").substring(0, 32); + downloadWgConfig(id, cleanName); + }; + + return ( +
    + {/* Interface Info Card */} +
    +
    +
    +

    + + WireGuard VPN +

    +
    +
    +
    + + {wgInterface && ( +
    +
    +
    +
    +
    + Interface +
    {wgInterface.name}
    +
    +
    +
    +
    + Public Key +
    + {wgInterface.publicKey} +
    +
    +
    +
    +
    + Address +
    {wgInterface.ipv4Cidr}
    +
    +
    +
    +
    + Port +
    {wgInterface.listenPort}
    +
    +
    +
    +
    + DNS +
    {wgInterface.dns}
    +
    +
    +
    +
    +
    + )} + + {/* Clients Card */} +
    +
    +
    +
    +

    + Clients ({clients?.length || 0}) +

    +
    +
    + setFilter(e.target.value)} + style={{ width: 200 }} + /> +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + {filteredClients?.map((client) => { + const isConnected = + client.latestHandshakeAt && + Date.now() - new Date(client.latestHandshakeAt).getTime() < + 3 * 60 * 1000; + return ( + + + + + + + + + + ); + })} + {(!filteredClients || filteredClients.length === 0) && ( + + + + )} + +
    StatusNameIP AddressLast HandshakeTransfer ↓Transfer ↑Actions
    + + {!client.enabled + ? "Disabled" + : isConnected + ? "Connected" + : "Idle"} + + {client.name} + {client.ipv4Address} + {timeAgo(client.latestHandshakeAt)}{formatBytes(client.transferRx)}{formatBytes(client.transferTx)} +
    + + + + +
    +
    + {filter + ? "No clients match your filter" + : "No WireGuard clients yet. Click 'New Client' to create one."} +
    +
    +
    +
    + ); +} + +export default WireGuard; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..fb86d45 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + "baseUrl": ".", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "src/*": [ + "./src/*" + ], + "test/*": [ + "./test/*" + ] + } + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..eca6668 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..1e21d90 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,61 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import checker from "vite-plugin-checker"; +import tsconfigPaths from "vite-tsconfig-paths"; +import "vitest/config"; +import { execFile } from "node:child_process"; + +const runLocaleScripts = () => { + execFile("yarn", ["locale-compile"], (error, stdout, _stderr) => { + if (error) { + throw error; + } + console.log(stdout); + execFile("yarn", ["locale-sort"], (error, stdout, _stderr) => { + if (error) { + throw error; + } + console.log(stdout); + }); + }); +}; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + { + name: 'run-on-start', + configureServer(_server) { + runLocaleScripts(); + }, + }, + { + name: "trigger-on-reload", + configureServer(server) { + server.watcher.on("change", (file) => { + if (file.includes("locale/src")) { + console.log(`File changed: ${file}, running locale scripts...`); + runLocaleScripts(); + } + }); + }, + }, + react(), + checker({ + // e.g. use TypeScript check + typescript: true, + }), + tsconfigPaths(), + ], + server: { + host: true, + port: 5173, + strictPort: true, + allowedHosts: true, + }, + test: { + environment: "happy-dom", + setupFiles: ["./vitest-setup.js"], + }, + assetsInclude: ["**/*.md", "**/*.png", "**/*.svg"], +}); diff --git a/frontend/vitest-setup.js b/frontend/vitest-setup.js new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/frontend/vitest-setup.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 0000000..46f0675 --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,2759 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adobe/css-tools@^4.4.0": + version "4.4.4" + resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-plugin-utils@^7.27.1": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@biomejs/biome@^2.4.5", "@biomejs/biome@>=1.7": + version "2.4.5" + resolved "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.5.tgz" + integrity sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "2.4.5" + "@biomejs/cli-darwin-x64" "2.4.5" + "@biomejs/cli-linux-arm64" "2.4.5" + "@biomejs/cli-linux-arm64-musl" "2.4.5" + "@biomejs/cli-linux-x64" "2.4.5" + "@biomejs/cli-linux-x64-musl" "2.4.5" + "@biomejs/cli-win32-arm64" "2.4.5" + "@biomejs/cli-win32-x64" "2.4.5" + +"@biomejs/cli-win32-x64@2.4.5": + version "2.4.5" + resolved "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.5.tgz" + integrity sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw== + +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.3.3" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.14.0", "@emotion/cache@^11.4.0": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.8.1": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.2" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== + +"@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + +"@esbuild/win32-x64@0.27.3": + version "0.27.3" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz" + integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== + +"@floating-ui/core@^1.7.4": + version "1.7.4" + resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz" + integrity sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@^1.0.1": + version "1.7.5" + resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz" + integrity sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg== + dependencies: + "@floating-ui/core" "^1.7.4" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + +"@formatjs/cli@^6.13.0": + version "6.13.0" + resolved "https://registry.npmjs.org/@formatjs/cli/-/cli-6.13.0.tgz" + integrity sha512-bl4+FNg7S6RPNa9cSAE8HqdXu84n7LpzDdkDAPqS0sk58XNbY/1Le6GdWqCKzELWX+FhI58gyZtZecmWsZ+Bhg== + +"@formatjs/ecma402-abstract@3.1.1": + version "3.1.1" + resolved "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz" + integrity sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q== + dependencies: + "@formatjs/fast-memoize" "3.1.0" + "@formatjs/intl-localematcher" "0.8.1" + decimal.js "^10.6.0" + tslib "^2.8.1" + +"@formatjs/fast-memoize@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz" + integrity sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg== + dependencies: + tslib "^2.8.1" + +"@formatjs/icu-messageformat-parser@3.5.1": + version "3.5.1" + resolved "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz" + integrity sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA== + dependencies: + "@formatjs/ecma402-abstract" "3.1.1" + "@formatjs/icu-skeleton-parser" "2.1.1" + tslib "^2.8.1" + +"@formatjs/icu-skeleton-parser@2.1.1": + version "2.1.1" + resolved "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz" + integrity sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q== + dependencies: + "@formatjs/ecma402-abstract" "3.1.1" + tslib "^2.8.1" + +"@formatjs/intl-localematcher@0.8.1": + version "0.8.1" + resolved "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz" + integrity sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA== + dependencies: + "@formatjs/fast-memoize" "3.1.0" + tslib "^2.8.1" + +"@formatjs/intl@4.1.2": + version "4.1.2" + resolved "https://registry.npmjs.org/@formatjs/intl/-/intl-4.1.2.tgz" + integrity sha512-V60fNY/X/7zqmRffr7qPwscGmVGYDmlKF069mSQ2a/7fE22q602NtIfOQY8vzRA63Gr/O/U6vjRVBHMabrnA9A== + dependencies: + "@formatjs/ecma402-abstract" "3.1.1" + "@formatjs/fast-memoize" "3.1.0" + "@formatjs/icu-messageformat-parser" "3.5.1" + intl-messageformat "11.1.2" + tslib "^2.8.1" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@parcel/watcher-win32-x64@2.5.6": + version "2.5.6" + resolved "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz" + integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw== + +"@parcel/watcher@^2.4.1": + version "2.5.6" + resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz" + integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ== + dependencies: + detect-libc "^2.0.3" + is-glob "^4.0.3" + node-addon-api "^7.0.0" + picomatch "^4.0.3" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.6" + "@parcel/watcher-darwin-arm64" "2.5.6" + "@parcel/watcher-darwin-x64" "2.5.6" + "@parcel/watcher-freebsd-x64" "2.5.6" + "@parcel/watcher-linux-arm-glibc" "2.5.6" + "@parcel/watcher-linux-arm-musl" "2.5.6" + "@parcel/watcher-linux-arm64-glibc" "2.5.6" + "@parcel/watcher-linux-arm64-musl" "2.5.6" + "@parcel/watcher-linux-x64-glibc" "2.5.6" + "@parcel/watcher-linux-x64-musl" "2.5.6" + "@parcel/watcher-win32-arm64" "2.5.6" + "@parcel/watcher-win32-ia32" "2.5.6" + "@parcel/watcher-win32-x64" "2.5.6" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@react-aria/ssr@^3.5.0": + version "3.9.10" + resolved "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz" + integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@restart/hooks@^0.4.9": + version "0.4.16" + resolved "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz" + integrity sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w== + dependencies: + dequal "^2.0.3" + +"@restart/hooks@^0.5.0": + version "0.5.1" + resolved "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz" + integrity sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q== + dependencies: + dequal "^2.0.3" + +"@restart/ui@^1.9.4": + version "1.9.4" + resolved "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz" + integrity sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA== + dependencies: + "@babel/runtime" "^7.26.0" + "@popperjs/core" "^2.11.8" + "@react-aria/ssr" "^3.5.0" + "@restart/hooks" "^0.5.0" + "@types/warning" "^3.0.3" + dequal "^2.0.3" + dom-helpers "^5.2.0" + uncontrollable "^8.0.4" + warning "^4.0.3" + +"@rolldown/pluginutils@1.0.0-rc.3": + version "1.0.0-rc.3" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz" + integrity sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q== + +"@rollup/rollup-win32-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz" + integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA== + +"@rollup/rollup-win32-x64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz" + integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== + +"@standard-schema/spec@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + +"@swc/helpers@^0.5.0": + version "0.5.18" + resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz" + integrity sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ== + dependencies: + tslib "^2.8.0" + +"@tabler/core@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@tabler/core/-/core-1.4.0.tgz" + integrity sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw== + dependencies: + "@popperjs/core" "^2.11.8" + bootstrap "5.3.7" + +"@tabler/icons-react@^3.38.0": + version "3.38.0" + resolved "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.38.0.tgz" + integrity sha512-kR5wv+m4+GgmnSszg3rQd6SrTFAQ/XnQC/yTwIfuRJSfqB12KoIC7fPbIijFgOHTFlBN5DARnN0IVrR7KYG6/A== + dependencies: + "@tabler/icons" "3.38.0" + +"@tabler/icons@3.38.0": + version "3.38.0" + resolved "https://registry.npmjs.org/@tabler/icons/-/icons-3.38.0.tgz" + integrity sha512-FdETQSpQ3lN7BEjEUzjKhsfTDCamrvMDops4HEMphTm3DmkIFpThoODn8XXZ8Q9MhjshIvphIYVHHB7zpq167w== + +"@tanstack/query-core@5.90.20": + version "5.90.20" + resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz" + integrity sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg== + +"@tanstack/query-devtools@5.93.0": + version "5.93.0" + resolved "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz" + integrity sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg== + +"@tanstack/react-query-devtools@^5.91.3": + version "5.91.3" + resolved "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz" + integrity sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA== + dependencies: + "@tanstack/query-devtools" "5.93.0" + +"@tanstack/react-query@^5.90.20", "@tanstack/react-query@^5.90.21": + version "5.90.21" + resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz" + integrity sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg== + dependencies: + "@tanstack/query-core" "5.90.20" + +"@tanstack/react-table@^8.21.3": + version "8.21.3" + resolved "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz" + integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== + dependencies: + "@tanstack/table-core" "8.21.3" + +"@tanstack/table-core@8.21.3": + version "8.21.3" + resolved "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz" + integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== + +"@testing-library/dom@^10.0.0", "@testing-library/dom@^10.4.1": + version "10.4.1" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^6.9.1": + version "6.9.1" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz" + integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + picocolors "^1.1.1" + redent "^3.0.0" + +"@testing-library/react@^16.3.2": + version "16.3.2" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz" + integrity sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g== + dependencies: + "@babel/runtime" "^7.12.5" + +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + +"@types/country-flag-icons@^1.2.2": + version "1.2.2" + resolved "https://registry.npmjs.org/@types/country-flag-icons/-/country-flag-icons-1.2.2.tgz" + integrity sha512-CefEn/J336TBDp7NX8JqzlDtCBOsm8M3r1Li0gEOt0HOMHF1XemNyrx9lSHjsafcb1yYWybU0N8ZAXuyCaND0w== + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.7" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz" + integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== + dependencies: + hoist-non-react-statics "^3.3.0" + +"@types/humps@^2.0.6": + version "2.0.6" + resolved "https://registry.npmjs.org/@types/humps/-/humps-2.0.6.tgz" + integrity sha512-Fagm1/a/1J9gDKzGdtlPmmTN5eSw/aaTzHtj740oSfo+MODsSY2WglxMmhTdOglC8nxqUhGGQ+5HfVtBvxo3Kg== + +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@*", "@types/node@^20.0.0 || ^22.0.0 || >=24.0.0", "@types/node@^20.19.0 || >=22.12.0", "@types/node@>=20.0.0": + version "25.3.0" + resolved "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz" + integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A== + dependencies: + undici-types "~7.18.0" + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prismjs@^1.0.0": + version "1.26.6" + resolved "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz" + integrity sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw== + +"@types/prop-types@^15.7.12": + version "15.7.15" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + +"@types/react-dom@^18.0.0 || ^19.0.0", "@types/react-dom@^19.2.3": + version "19.2.3" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz" + integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== + +"@types/react-table@^7.7.20": + version "7.7.20" + resolved "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.20.tgz" + integrity sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.6": + version "4.4.12" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + +"@types/react@*", "@types/react@^18.0.0 || ^19.0.0", "@types/react@^19.2.0", "@types/react@^19.2.14", "@types/react@>=16.14.8", "@types/react@>=16.9.11", "@types/react@>=18", "@types/react@19": + version "19.2.14" + resolved "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz" + integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== + dependencies: + csstype "^3.2.2" + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/unist@^2": + version "2.0.11" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +"@types/warning@^3.0.3": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz" + integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== + +"@types/whatwg-mimetype@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz" + integrity sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA== + +"@types/ws@^8.18.1": + version "8.18.1" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + +"@uiw/react-textarea-code-editor@^3.1.1": + version "3.1.1" + resolved "https://registry.npmjs.org/@uiw/react-textarea-code-editor/-/react-textarea-code-editor-3.1.1.tgz" + integrity sha512-AERRbp/d85vWR+UPgsB5hEgerNXuyszdmhWl2fV2H2jN63jgOobwEnjIpb76Vwy8SaGa/AdehaoJX2XZgNXtJA== + dependencies: + "@babel/runtime" "^7.18.6" + rehype "~13.0.0" + rehype-prism-plus "2.0.0" + +"@ungap/structured-clone@^1.0.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@vitejs/plugin-react@^5.1.4": + version "5.1.4" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz" + integrity sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA== + dependencies: + "@babel/core" "^7.29.0" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-rc.3" + "@types/babel__core" "^7.20.5" + react-refresh "^0.18.0" + +"@vitest/expect@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz" + integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ== + dependencies: + "@standard-schema/spec" "^1.0.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" + chai "^6.2.1" + tinyrainbow "^3.0.3" + +"@vitest/mocker@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz" + integrity sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ== + dependencies: + "@vitest/spy" "4.0.18" + estree-walker "^3.0.3" + magic-string "^0.30.21" + +"@vitest/pretty-format@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz" + integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw== + dependencies: + tinyrainbow "^3.0.3" + +"@vitest/runner@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz" + integrity sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw== + dependencies: + "@vitest/utils" "4.0.18" + pathe "^2.0.3" + +"@vitest/snapshot@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz" + integrity sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA== + dependencies: + "@vitest/pretty-format" "4.0.18" + magic-string "^0.30.21" + pathe "^2.0.3" + +"@vitest/spy@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz" + integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw== + +"@vitest/utils@4.0.18": + version "4.0.18" + resolved "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz" + integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA== + dependencies: + "@vitest/pretty-format" "4.0.18" + tinyrainbow "^3.0.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +aria-query@^5.0.0, aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +baseline-browser-mapping@^2.9.0: + version "2.10.0" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz" + integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== + +bootstrap@5.3.7: + version "5.3.7" + resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz" + integrity sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw== + +browserslist@^4.24.0, "browserslist@>= 4.21.0": + version "4.28.1" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz" + integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== + dependencies: + baseline-browser-mapping "^2.9.0" + caniuse-lite "^1.0.30001759" + electron-to-chromium "^1.5.263" + node-releases "^2.0.27" + update-browserslist-db "^1.2.0" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001759: + version "1.0.30001770" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz" + integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chai@^6.2.1: + version "6.2.2" + resolved "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +chokidar@^4.0.0, chokidar@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +classnames@^2.3.2, classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +country-flag-icons@^1.6.15: + version "1.6.15" + resolved "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.15.tgz" + integrity sha512-92HoA8l6DluEidku8tKBftjuFRj4Rv3zDW1lXxCuNnqAxhUSkvso9gM/Afj4F5BnK+wneHIe3ydI+s+4NA29/Q== + +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + +csstype@^3.0.2, csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decimal.js@^10.6.0: + version "10.6.0" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== + +decode-named-character-reference@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz" + integrity sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q== + dependencies: + character-entities "^2.0.0" + +decode-uri-component@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz" + integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== + +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + +dequal@^2.0.0, dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + +dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +electron-to-chromium@^1.5.263: + version "1.5.286" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz" + integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A== + +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + +entities@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +es-module-lexer@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +esbuild@^0.27.0: + version "0.27.3" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz" + integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +expect-type@^1.2.2: + version "1.3.0" + resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz" + integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +ez-modal-react@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/ez-modal-react/-/ez-modal-react-1.0.5.tgz" + integrity sha512-/A8yLK54tpmWCMkW8Pwqc2xxspmimGOOw/m+1Y+tNtUIheuDHhLynHP1Q0utciJEGDAK849aQcd+6DrJ88hggQ== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +filter-obj@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz" + integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +formik@^2.4.9: + version "2.4.9" + resolved "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz" + integrity sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^2.0.0" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +generate-password-browser@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/generate-password-browser/-/generate-password-browser-1.1.0.tgz" + integrity sha512-qsQve0rVbCqGqAfKgZwjxKUfI1d1nyd22dz+kE8gn1iw1LxGkR+Slsl79XXfm2wxuK27IkopTs5KXcOEQnhg0w== + dependencies: + buffer "^6.0.3" + randombytes "^2.0.5" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +happy-dom@*, happy-dom@^20.8.3: + version "20.8.3" + resolved "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.3.tgz" + integrity sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ== + dependencies: + "@types/node" ">=20.0.0" + "@types/whatwg-mimetype" "^3.0.2" + "@types/ws" "^8.18.1" + entities "^7.0.1" + whatwg-mimetype "^3.0.0" + ws "^8.18.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hast-util-from-html@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz" + integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.1.0" + hast-util-from-parse5 "^8.0.0" + parse5 "^7.0.0" + vfile "^6.0.0" + vfile-message "^4.0.0" + +hast-util-from-parse5@^8.0.0: + version "8.0.3" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^3.0.0: + version "3.1.1" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz" + integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA== + dependencies: + "@types/hast" "^2.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-to-html@^9.0.0: + version "9.0.5" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz" + integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.6" + resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-to-string@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz" + integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^7.0.0: + version "7.2.0" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz" + integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^3.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +html-url-attributes@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz" + integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +humps@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz" + integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g== + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +immutable@^5.0.2: + version "5.1.4" + resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz" + integrity sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inline-style-parser@0.2.7: + version "0.2.7" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz" + integrity sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA== + +intl-messageformat@11.1.2: + version "11.1.2" + resolved "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz" + integrity sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg== + dependencies: + "@formatjs/ecma402-abstract" "3.1.1" + "@formatjs/fast-memoize" "3.1.0" + "@formatjs/icu-messageformat-parser" "3.5.1" + tslib "^2.8.1" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lodash-es@^4.17.21: + version "4.17.23" + resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz" + integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash@^4.17.21: + version "4.17.23" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +loose-envify@^1.0.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz" + integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.1" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.2" + resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + +micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +micromark@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== + +npm-run-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz" + integrity sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA== + dependencies: + path-key "^4.0.0" + unicorn-magic "^0.3.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +obug@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz" + integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-numeric-range@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz" + integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== + +parse5@^7.0.0: + version "7.3.0" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^1.1.1, picocolors@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +"picomatch@^3 || ^4", picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +postcss-simple-vars@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz" + integrity sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A== + +postcss@^8.2.1, postcss@^8.5.6, postcss@^8.5.8: + version "8.5.8" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz" + integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +prop-types-extra@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz" + integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== + dependencies: + react-is "^16.3.2" + warning "^4.0.0" + +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + +query-string@^9.3.1: + version "9.3.1" + resolved "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz" + integrity sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw== + dependencies: + decode-uri-component "^0.4.1" + filter-obj "^5.1.0" + split-on-first "^3.0.0" + +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +react-bootstrap@^2.10.10: + version "2.10.10" + resolved "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz" + integrity sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ== + dependencies: + "@babel/runtime" "^7.24.7" + "@restart/hooks" "^0.4.9" + "@restart/ui" "^1.9.4" + "@types/prop-types" "^15.7.12" + "@types/react-transition-group" "^4.4.6" + classnames "^2.3.2" + dom-helpers "^5.2.1" + invariant "^2.2.4" + prop-types "^15.8.1" + prop-types-extra "^1.1.0" + react-transition-group "^4.4.5" + uncontrollable "^7.2.1" + warning "^4.0.3" + +"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19.2.4, react-dom@>=16.14.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.9.0, react-dom@>=18, react-dom@>16.8.0: + version "19.2.4" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz" + integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== + dependencies: + scheduler "^0.27.0" + +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-intl@^8.1.3: + version "8.1.3" + resolved "https://registry.npmjs.org/react-intl/-/react-intl-8.1.3.tgz" + integrity sha512-eL1/d+uQdnapirynOGAriW0K9uAoyarjRGL3V9LaTRuohNSvPgCfJX06EZl5M52h/Hu7Gz7A1sD7dNHcos1lNg== + dependencies: + "@formatjs/ecma402-abstract" "3.1.1" + "@formatjs/icu-messageformat-parser" "3.5.1" + "@formatjs/intl" "4.1.2" + "@types/hoist-non-react-statics" "^3.3.1" + hoist-non-react-statics "^3.3.2" + intl-messageformat "11.1.2" + tslib "^2.8.1" + +react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-markdown@^10.1.0: + version "10.1.0" + resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz" + integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +react-refresh@^0.18.0: + version "0.18.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz" + integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== + +react-router-dom@^7.13.1: + version "7.13.1" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz" + integrity sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw== + dependencies: + react-router "7.13.1" + +react-router@7.13.1: + version "7.13.1" + resolved "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz" + integrity sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + +react-select@^5.10.2: + version "5.10.2" + resolved "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz" + integrity sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.2.0" + +react-toastify@^11.0.5: + version "11.0.5" + resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz" + integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== + dependencies: + clsx "^2.1.1" + +react-transition-group@^4.3.0, react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react@^18 || ^19", "react@^18.0.0 || ^19.0.0", react@^19.2.4, "react@>= 16", react@>=0.14.0, react@>=15.0.0, react@>=16.14.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.9.0, react@>=18, react@>16.8.0, react@19: + version "19.2.4" + resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz" + integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +refractor@^4.8.0: + version "4.9.0" + resolved "https://registry.npmjs.org/refractor/-/refractor-4.9.0.tgz" + integrity sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og== + dependencies: + "@types/hast" "^2.0.0" + "@types/prismjs" "^1.0.0" + hastscript "^7.0.0" + parse-entities "^4.0.0" + +rehype-parse@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz" + integrity sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag== + dependencies: + "@types/hast" "^3.0.0" + hast-util-from-html "^2.0.0" + unified "^11.0.0" + +rehype-prism-plus@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz" + integrity sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ== + dependencies: + hast-util-to-string "^3.0.0" + parse-numeric-range "^1.3.0" + refractor "^4.8.0" + rehype-parse "^9.0.0" + unist-util-filter "^5.0.0" + unist-util-visit "^5.0.0" + +rehype-stringify@^10.0.0: + version "10.0.1" + resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz" + integrity sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA== + dependencies: + "@types/hast" "^3.0.0" + hast-util-to-html "^9.0.0" + unified "^11.0.0" + +rehype@~13.0.0: + version "13.0.2" + resolved "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz" + integrity sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A== + dependencies: + "@types/hast" "^3.0.0" + rehype-parse "^9.0.0" + rehype-stringify "^10.0.0" + unified "^11.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0: + version "11.1.2" + resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz" + integrity sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.19.0: + version "1.22.11" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^4.43.0: + version "4.59.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz" + integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.59.0" + "@rollup/rollup-android-arm64" "4.59.0" + "@rollup/rollup-darwin-arm64" "4.59.0" + "@rollup/rollup-darwin-x64" "4.59.0" + "@rollup/rollup-freebsd-arm64" "4.59.0" + "@rollup/rollup-freebsd-x64" "4.59.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" + "@rollup/rollup-linux-arm-musleabihf" "4.59.0" + "@rollup/rollup-linux-arm64-gnu" "4.59.0" + "@rollup/rollup-linux-arm64-musl" "4.59.0" + "@rollup/rollup-linux-loong64-gnu" "4.59.0" + "@rollup/rollup-linux-loong64-musl" "4.59.0" + "@rollup/rollup-linux-ppc64-gnu" "4.59.0" + "@rollup/rollup-linux-ppc64-musl" "4.59.0" + "@rollup/rollup-linux-riscv64-gnu" "4.59.0" + "@rollup/rollup-linux-riscv64-musl" "4.59.0" + "@rollup/rollup-linux-s390x-gnu" "4.59.0" + "@rollup/rollup-linux-x64-gnu" "4.59.0" + "@rollup/rollup-linux-x64-musl" "4.59.0" + "@rollup/rollup-openbsd-x64" "4.59.0" + "@rollup/rollup-openharmony-arm64" "4.59.0" + "@rollup/rollup-win32-arm64-msvc" "4.59.0" + "@rollup/rollup-win32-ia32-msvc" "4.59.0" + "@rollup/rollup-win32-x64-gnu" "4.59.0" + "@rollup/rollup-win32-x64-msvc" "4.59.0" + fsevents "~2.3.2" + +rooks@^9.5.0: + version "9.5.0" + resolved "https://registry.npmjs.org/rooks/-/rooks-9.5.0.tgz" + integrity sha512-AtmaX8yjQkJAW7EXW+UU481bpGwuk455hjD/aEUuy7N7VjvXlNmO8BErQ+jEUQp1DRA/PTWonv+Dq1nEkJdgkw== + dependencies: + fast-deep-equal "^3.1.3" + lodash.debounce "^4.0.8" + raf "^3.4.1" + use-sync-external-store "^1.4.0" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +sass@^1.70.0, sass@^1.97.3: + version "1.97.3" + resolved "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz" + integrity sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg== + dependencies: + chokidar "^4.0.0" + immutable "^5.0.2" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0": + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +split-on-first@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz" + integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== + +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +std-env@^3.10.0: + version "3.10.0" + resolved "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +style-to-js@^1.0.0: + version "1.1.21" + resolved "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz" + integrity sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ== + dependencies: + style-to-object "1.0.14" + +style-to-object@1.0.14: + version "1.0.14" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz" + integrity sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw== + dependencies: + inline-style-parser "0.2.7" + +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz" + integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== + +tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + +tinyrainbow@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz" + integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== + +tmp@^0.2.5: + version "0.2.5" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + +tsconfck@^3.0.3: + version "3.1.6" + resolved "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz" + integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== + +tslib@^2.0.0, tslib@^2.8.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +typescript@*, typescript@^5.0.0, typescript@^5.6.0, typescript@>4.0.0, typescript@5.9.3: + version "5.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +uncontrollable@^7.2.1: + version "7.2.1" + resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" + integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== + dependencies: + "@babel/runtime" "^7.6.3" + "@types/react" ">=16.9.11" + invariant "^2.2.4" + react-lifecycles-compat "^3.0.4" + +uncontrollable@^8.0.4: + version "8.0.4" + resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz" + integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== + +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + +unicorn-magic@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz" + integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA== + +unified@^11.0.0: + version "11.0.5" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unist-util-filter@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz" + integrity sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +unist-util-is@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.2" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz" + integrity sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +update-browserslist-db@^1.2.0: + version "1.2.3" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +use-isomorphic-layout-effect@^1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz" + integrity sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA== + +use-sync-external-store@^1.4.0: + version "1.6.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +vite-plugin-checker@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.12.0.tgz" + integrity sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg== + dependencies: + "@babel/code-frame" "^7.27.1" + chokidar "^4.0.3" + npm-run-path "^6.0.0" + picocolors "^1.1.1" + picomatch "^4.0.3" + tiny-invariant "^1.3.3" + tinyglobby "^0.2.15" + vscode-uri "^3.1.0" + +vite-tsconfig-paths@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz" + integrity sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +vite@*, "vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^6.0.0 || ^7.0.0", "vite@^6.0.0 || ^7.0.0-0", vite@^7.3.1, vite@>=5.4.21: + version "7.3.1" + resolved "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz" + integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== + dependencies: + esbuild "^0.27.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^4.0.18: + version "4.0.18" + resolved "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz" + integrity sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ== + dependencies: + "@vitest/expect" "4.0.18" + "@vitest/mocker" "4.0.18" + "@vitest/pretty-format" "4.0.18" + "@vitest/runner" "4.0.18" + "@vitest/snapshot" "4.0.18" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" + es-module-lexer "^1.7.0" + expect-type "^1.2.2" + magic-string "^0.30.21" + obug "^2.1.1" + pathe "^2.0.3" + picomatch "^4.0.3" + std-env "^3.10.0" + tinybench "^2.9.0" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.0.3" + vite "^6.0.0 || ^7.0.0" + why-is-node-running "^2.3.0" + +vscode-uri@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz" + integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ== + +warning@^4.0.0, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + +ws@^8.18.3: + version "8.19.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz" + integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.4.2: + version "2.8.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz" + integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== + +zwitch@^2.0.0, zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..ed89101 --- /dev/null +++ b/install.sh @@ -0,0 +1,191 @@ +#!/bin/bash + +# Configuration +INSTALL_DIR="/opt/d3v-npmwg" +DOCKER_COMPOSE_YML="$INSTALL_DIR/docker-compose.yml" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +function check_root() { + if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root (use sudo).${NC}" + exit 1 + fi +} + +function check_dependencies() { + if ! command -v docker &> /dev/null; then + echo -e "${YELLOW}Docker is not installed. Installing Docker...${NC}" + curl -fsSL https://get.docker.com -o get-docker.sh + sh get-docker.sh + rm get-docker.sh + fi + + if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then + echo -e "${YELLOW}Docker Compose is not installed. Please install Docker Compose first.${NC}" + exit 1 + fi +} + +function get_docker_compose_cmd() { + if docker compose version &> /dev/null; then + echo "docker compose" + else + echo "docker-compose" + fi +} + +function install_npm_wg() { + check_root + check_dependencies + + if [ -d "$INSTALL_DIR" ]; then + echo -e "${YELLOW}Installation directory ($INSTALL_DIR) already exists. Do you want to update instead?${NC}" + return + fi + + read -p "Enter your server's public IP or Domain for WireGuard (WG_HOST): " WG_HOST + if [ -z "$WG_HOST" ]; then + echo -e "${RED}WG_HOST cannot be empty. Aborting.${NC}" + return + fi + + mkdir -p "$INSTALL_DIR" + + # Create docker-compose.yml + cat < "$DOCKER_COMPOSE_YML" +version: "3.8" +services: + npm-wg: + image: xtcnet/npm-wg:latest # NOTE: Update with actual Docker Hub image if pushed, or leave as npm-wg:latest if built locally. Assuming docker hub image is available or they build it. + # Wait, the README uses npm-wg:latest. + container_name: npm-wg + restart: unless-stopped + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + ports: + - "80:80" # HTTP + - "81:81" # Admin UI + - "443:443" # HTTPS + - "51820:51820/udp" # WireGuard + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + - ./wireguard:/etc/wireguard + environment: + WG_HOST: "$WG_HOST" +EOF + # Fix the image name + sed -i 's/xtcnet\/npm-wg:latest/npm-wg:latest/g' "$DOCKER_COMPOSE_YML" + + echo -e "${GREEN}Docker compose file created at $DOCKER_COMPOSE_YML${NC}" + cd "$INSTALL_DIR" || exit + + local dc_cmd=$(get_docker_compose_cmd) + $dc_cmd up -d + + echo -e "${GREEN}NPM-WG installed and started successfully!${NC}" + echo -e "${YELLOW}Web UI: http://:81${NC}" + echo -e "Wait a minute for the first boot, then follow the setup wizard on the Web UI to create your admin account." +} + +function uninstall_npm_wg() { + check_root + echo -e "${RED}WARNING: This will completely remove NPM-WG and all its data!${NC}" + read -p "Are you sure? (y/N): " confirm + if [[ "$confirm" == [yY] || "$confirm" == [yY][eE][sS] ]]; then + if [ -d "$INSTALL_DIR" ]; then + cd "$INSTALL_DIR" || exit + local dc_cmd=$(get_docker_compose_cmd) + $dc_cmd down -v + cd / + rm -rf "$INSTALL_DIR" + echo -e "${GREEN}NPM-WG uninstalled completely.${NC}" + else + echo -e "${YELLOW}NPM-WG is not installed in $INSTALL_DIR.${NC}" + fi + fi +} + +function reset_password() { + check_root + + if ! docker ps | grep -q npm-wg; then + echo -e "${RED}Container npm-wg is not running or not found. Please start it first.${NC}" + return + fi + + echo -e "${YELLOW}Resetting admin password...${NC}" + # Setting password to 'changeme' + # HASH for 'changeme' + local HASH="\$2y\$10\$k1r.q/q.T5lPqG3y8H148ei/i.k9K.cI.1s/Q/8Fz/5e.d.f4n.6e" + + docker exec -it npm-wg /bin/sh -c "sqlite3 /data/database.sqlite \"UPDATE user SET is_deleted=0 WHERE id=1;\"" + docker exec -it npm-wg /bin/sh -c "sqlite3 /data/database.sqlite \"UPDATE auth SET secret='${HASH}' WHERE user_id=1;\"" + docker exec -it npm-wg /bin/sh -c "sqlite3 /data/database.sqlite \"UPDATE user SET email='admin@example.com' WHERE id=1;\"" + + echo -e "${GREEN}Password has been reset successfully!${NC}" + echo -e "Login Email: admin@example.com" + echo -e "Password: changeme" + echo -e "${YELLOW}Please log in and change your password immediately!${NC}" +} + +function update_npm_wg() { + check_root + if [ ! -d "$INSTALL_DIR" ]; then + echo -e "${RED}NPM-WG is not installed in $INSTALL_DIR.${NC}" + return + fi + + echo -e "${YELLOW}Updating NPM-WG...${NC}" + cd "$INSTALL_DIR" || exit + + local dc_cmd=$(get_docker_compose_cmd) + $dc_cmd pull + $dc_cmd up -d + + echo -e "${GREEN}NPM-WG updated successfully!${NC}" + docker image prune -f +} + +function menu() { + while true; do + echo -e "\n${GREEN}=== D3V-NPMWG Installation Manager ===${NC}" + echo "1. Install D3V-NPMWG (Cài đặt)" + echo "2. Uninstall D3V-NPMWG (Gỡ cài đặt)" + echo "3. Reset Web Admin Password (Đặt lại mật khẩu)" + echo "4. Update D3V-NPMWG (Cập nhật phiên bản mới)" + echo "5. Exit (Thoát)" + read -p "Select an option (1-5): " choice + + case $choice in + 1) install_npm_wg ;; + 2) uninstall_npm_wg ;; + 3) reset_password ;; + 4) update_npm_wg ;; + 5) echo -e "${GREEN}Goodbye!${NC}"; exit 0 ;; + *) echo -e "${RED}Invalid option. Please try again.${NC}" ;; + esac + done +} + +# Run menu if no arguments, else run the command +if [ \$# -eq 0 ]; then + menu +else + case \$1 in + install) install_npm_wg ;; + uninstall) uninstall_npm_wg ;; + reset) reset_password ;; + update) update_npm_wg ;; + *) echo -e "Usage: \$0 {install|uninstall|reset|update}" ;; + esac +fi diff --git a/scripts/.common.sh b/scripts/.common.sh new file mode 100644 index 0000000..2b59e6e --- /dev/null +++ b/scripts/.common.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Colors +BLUE='\E[1;34m' +CYAN='\E[1;36m' +GREEN='\E[1;32m' +RED='\E[1;31m' +RESET='\E[0m' +YELLOW='\E[1;33m' + +export BLUE CYAN GREEN RED RESET YELLOW + +# Docker Compose +COMPOSE_PROJECT_NAME="npm2dev" +COMPOSE_FILE="docker/docker-compose.dev.yml" + +export COMPOSE_FILE COMPOSE_PROJECT_NAME + +# $1: container_name +get_container_ip () { + local container_name=$1 + local container + local ip + container=$(docker compose ps --all -q "${container_name}" | tail -n1) + ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container") + echo "$ip" +} diff --git a/scripts/build-project.sh b/scripts/build-project.sh new file mode 100644 index 0000000..4244497 --- /dev/null +++ b/scripts/build-project.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +echo "=== Building NPM-WG ===" + +# 1. Build frontend +echo "[1/2] Building frontend..." +cd frontend +# Install dependencies (requires yarn or npm/pnpm) +if command -v yarn &> /dev/null; then + yarn install + yarn locale-compile + yarn build +elif command -v npm &> /dev/null; then + npm install + npm run locale-compile + npm run build +else + echo "Error: Neither yarn nor npm found. Please install Node.js." + exit 1 +fi +cd .. + +# 2. Build Docker image +echo "[2/2] Building Docker image..." +# Make sure to include the trailing dot! +docker build -t npm-wg -f docker/Dockerfile . + +echo "=== Build Complete ===" +echo "You can now run the container with:" +echo "docker run -d --name npm-wg --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p 80:80 -p 81:81 -p 443:443 -p 51820:51820/udp -v npm-wg-data:/data npm-wg:latest" diff --git a/scripts/buildx b/scripts/buildx new file mode 100644 index 0000000..8838290 --- /dev/null +++ b/scripts/buildx @@ -0,0 +1,35 @@ +#!/bin/bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +echo -e "${BLUE}❯ ${CYAN}Building docker multiarch: ${YELLOW}${*}${RESET}" + +cd "${DIR}/.." || exit 1 + +# determine commit if not already set +if [ "$BUILD_COMMIT" == "" ]; then + BUILD_COMMIT=$(git log -n 1 --format=%h) +fi + +# Buildx Builder +docker buildx create --name "${BUILDX_NAME:-npm}" || echo +docker buildx use "${BUILDX_NAME:-npm}" + +docker buildx build \ + --build-arg BUILD_VERSION="${BUILD_VERSION:-dev}" \ + --build-arg BUILD_COMMIT="${BUILD_COMMIT:-notset}" \ + --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \ + --build-arg GOPROXY="${GOPROXY:-}" \ + --build-arg GOPRIVATE="${GOPRIVATE:-}" \ + --platform linux/amd64,linux/arm64 \ + --progress plain \ + --pull \ + -f docker/Dockerfile \ + $@ \ + . + +rc=$? +docker buildx rm "${BUILDX_NAME:-npm}" +echo -e "${BLUE}❯ ${GREEN}Multiarch build Complete${RESET}" +exit $rc diff --git a/scripts/ci/frontend-build b/scripts/ci/frontend-build new file mode 100644 index 0000000..8b66a13 --- /dev/null +++ b/scripts/ci/frontend-build @@ -0,0 +1,24 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/../.common.sh" + +DOCKER_IMAGE=nginxproxymanager/nginx-full:certbot-node + +# Ensure docker exists +if hash docker 2>/dev/null; then + docker pull "${DOCKER_IMAGE}" + cd "${DIR}/../.." + echo -e "${BLUE}❯ ${CYAN}Building Frontend ...${RESET}" + + docker run --rm \ + -e CI=true \ + -e NODE_OPTIONS=--openssl-legacy-provider \ + -v "$(pwd)/frontend:/app/frontend" \ + -w /app/frontend "${DOCKER_IMAGE}" \ + sh -c "yarn install && yarn lint && yarn locale-compile && yarn vitest run --no-color && yarn build && chown -R $(id -u):$(id -g) /app/frontend" + + echo -e "${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}" +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/ci/fulltest-cypress b/scripts/ci/fulltest-cypress new file mode 100644 index 0000000..54fa052 --- /dev/null +++ b/scripts/ci/fulltest-cypress @@ -0,0 +1,91 @@ +#!/bin/bash +set -e + +STACK="${1:-sqlite}" + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# remember this is running in "ci" folder.. + +# Some defaults for running this script outside of CI +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-npm_local_fulltest}" +export IMAGE="${IMAGE:-nginx-proxy-manager}" +export BRANCH_LOWER="${BRANCH_LOWER:-unknown}" +export BUILD_NUMBER="${BUILD_NUMBER:-0000}" + +if [ "${COMPOSE_FILE:-}" = "" ]; then + export COMPOSE_FILE="docker/docker-compose.ci.yml:docker/docker-compose.ci.${STACK}.yml" +fi + +# Colors +BLUE='\E[1;34m' +RED='\E[1;31m' +CYAN='\E[1;36m' +GREEN='\E[1;32m' +RESET='\E[0m' +YELLOW='\E[1;33m' + +export BLUE CYAN GREEN RESET YELLOW + +echo -e "${BLUE}❯ ${CYAN}Starting fullstack cypress testing ...${RESET}" +echo -e "${BLUE}❯ $(docker compose config)${RESET}" + +# $1: container_name +get_container_ip () { + local container_name=$1 + local container + local ip + container=$(docker compose ps --all -q "${container_name}" | tail -n1) + ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container") + echo "$ip" +} + +# Bring up a stack, in steps so we can inject IPs everywhere +docker compose up -d pdns pdns-db +PDNS_IP=$(get_container_ip "pdns") +echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}" + +# adjust the dnsrouter config +LOCAL_DNSROUTER_CONFIG="$DIR/../../docker/dev/dnsrouter-config.json" +rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp" +# IMPORTANT: changes to dnsrouter-config.json will affect this line: +jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp" + +docker compose up -d dnsrouter +DNSROUTER_IP=$(get_container_ip "dnsrouter") +echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}" + +if [ "${DNSROUTER_IP:-}" = "" ]; then + echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}" + exit 1 +fi + +# mount the resolver +LOCAL_RESOLVE="$DIR/../../docker/dev/resolv.conf" +rm -rf "${LOCAL_RESOLVE}" +printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}" + +# bring up all remaining containers, except cypress! +docker compose up -d --remove-orphans stepca squid +docker compose pull db-mysql || true # ok to fail +docker compose pull db-postgres || true # ok to fail +docker compose pull authentik authentik-redis authentik-ldap || true # ok to fail +docker compose up -d --remove-orphans --pull=never fullstack + +# wait for main container to be healthy +bash "$DIR/../wait-healthy" "$(docker compose ps --all -q fullstack)" 120 + +# Run tests +rm -rf "$DIR/../../test/results" +docker compose up --build cypress + +# Get results +docker cp -L "$(docker compose ps --all -q cypress):/test/results" "$DIR/../../test/" +docker cp -L "$(docker compose ps --all -q fullstack):/data/logs" "$DIR/../../test/results/" + +if [ "$2" = "cleanup" ]; then + echo -e "${BLUE}❯ ${CYAN}Cleaning up containers ...${RESET}" + docker compose down --remove-orphans --volumes -t 30 +fi + +echo -e "${BLUE}❯ ${GREEN}Fullstack cypress testing complete${RESET}" + diff --git a/scripts/ci/test-and-build b/scripts/ci/test-and-build new file mode 100644 index 0000000..a0de341 --- /dev/null +++ b/scripts/ci/test-and-build @@ -0,0 +1,30 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/../.common.sh" + +TESTING_IMAGE=nginxproxymanager/nginx-full:certbot-node +docker pull "${TESTING_IMAGE}" + +# Test +echo -e "${BLUE}❯ ${CYAN}Testing backend ...${RESET}" +docker run --rm \ + -v "$(pwd)/backend:/app" \ + -w /app \ + "${TESTING_IMAGE}" \ + sh -c 'yarn install && yarn lint . && rm -rf node_modules' +echo -e "${BLUE}❯ ${GREEN}Testing Complete${RESET}" + +# Build +echo -e "${BLUE}❯ ${CYAN}Building ...${RESET}" +docker build --pull --no-cache --compress \ + -t "${IMAGE:-nginx-proxy-manager}:${BRANCH_LOWER:-unknown}-ci-${BUILD_NUMBER:-0000}" \ + -f docker/Dockerfile \ + --progress=plain \ + --build-arg TARGETPLATFORM=linux/amd64 \ + --build-arg BUILDPLATFORM=linux/amd64 \ + --build-arg BUILD_VERSION="${BUILD_VERSION:-unknown}" \ + --build-arg BUILD_COMMIT="${BUILD_COMMIT:-unknown}" \ + --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \ + . +echo -e "${BLUE}❯ ${GREEN}Building Complete${RESET}" diff --git a/scripts/cypress-dev b/scripts/cypress-dev new file mode 100644 index 0000000..f3749f8 --- /dev/null +++ b/scripts/cypress-dev @@ -0,0 +1,13 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Ensure docker exists +if hash docker 2>/dev/null; then + cd "${DIR}/.." + rm -rf "$DIR/../test/results" + docker compose up --build cypress +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/destroy-dev b/scripts/destroy-dev new file mode 100644 index 0000000..b3ead38 --- /dev/null +++ b/scripts/destroy-dev @@ -0,0 +1,13 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Make sure docker exists +if hash docker 2>/dev/null; then + cd "${DIR}/.." + echo -e "${BLUE}❯ ${CYAN}Destroying Dev Stack ...${RESET}" + docker compose down --remove-orphans --volumes +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/docs-build b/scripts/docs-build new file mode 100644 index 0000000..bc166f6 --- /dev/null +++ b/scripts/docs-build @@ -0,0 +1,14 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Ensure docker exists +if hash docker 2>/dev/null; then + cd "${DIR}/.." + echo -e "${BLUE}❯ ${CYAN}Building Docs ...${RESET}" + docker run --rm -e CI=true -v "$(pwd)/docs:/app/docs" -w /app/docs node:alpine sh -c "yarn set version berry && yarn install && yarn build && chown -R $(id -u):$(id -g) /app/docs" + echo -e "${BLUE}❯ ${GREEN}Building Docs Complete${RESET}" +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/docs-upload b/scripts/docs-upload new file mode 100644 index 0000000..75d44a6 --- /dev/null +++ b/scripts/docs-upload @@ -0,0 +1,52 @@ +#!/bin/bash + +# Note: This script is designed to be run inside CI builds + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +echo -e "${BLUE}❯ ${CYAN}Uploading docs in: ${YELLOW}$1${RESET}" + +cd "$1" || exit 1 +ALL_FILES=$(find . -follow) + +for FILE in $ALL_FILES +do + # remove preceding ./ + FILE=$(echo "$FILE" | sed -E "s/\.\///g") + echo '=======================================' + echo "FILE: $FILE" + + if [[ -d $FILE ]]; then + echo "Skipping $FILE because it's a directory" + elif [[ -f $FILE ]]; then + PARAM_STRING="--guess-mime-type" + EXT="${FILE##*.}" + if [ "$EXT" == "css" ]; then + PARAM_STRING="-mtext/css" + elif [ "$EXT" == "js" ]; then + PARAM_STRING="-mapplication/javascript" + elif [[ "$EXT" == "html" ]]; then + PARAM_STRING="-mtext/html" + elif [[ "$EXT" == "png" ]]; then + PARAM_STRING="-mimage/png" + elif [[ "$EXT" == "jpg" ]]; then + PARAM_STRING="-mimage/jpg" + elif [[ "$EXT" == "svg" ]]; then + PARAM_STRING="-mimage/svg+xml" + fi + + DEST_FOLDER=$(dirname "$FILE") + if [ "$DEST_FOLDER" == "." ]; then + DEST_FOLDER= + else + DEST_FOLDER="${DEST_FOLDER}/" + fi + + echo s3cmd -v -f -P "$PARAM_STRING" --add-header="Cache-Control:public,max-age=604800" sync "$FILE" "s3://$S3_BUCKET/$DEST_FOLDER" + s3cmd -v -f -P "$PARAM_STRING" --add-header="Cache-Control:public,max-age=604800" sync "$FILE" "s3://$S3_BUCKET/$DEST_FOLDER" + rc=$?; if [ $rc != 0 ]; then exit $rc; fi + fi +done + +echo -e "${BLUE}❯ ${GREEN}Upload Complete${RESET}" diff --git a/scripts/start-dev b/scripts/start-dev new file mode 100644 index 0000000..c561ac9 --- /dev/null +++ b/scripts/start-dev @@ -0,0 +1,62 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Ensure docker exists +if hash docker 2>/dev/null; then + cd "${DIR}/.." + echo -e "${BLUE}❯ ${CYAN}Starting Dev Stack ...${RESET}" + echo -e "${BLUE}❯ $(docker compose config)${RESET}" + + # Bring up a stack, in steps so we can inject IPs everywhere + docker compose up -d pdns pdns-db + PDNS_IP=$(get_container_ip "pdns") + echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}" + + # adjust the dnsrouter config + LOCAL_DNSROUTER_CONFIG="$DIR/../docker/dev/dnsrouter-config.json" + rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp" + # IMPORTANT: changes to dnsrouter-config.json will affect this line: + jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp" + + docker compose up -d dnsrouter + DNSROUTER_IP=$(get_container_ip "dnsrouter") + echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}" + + if [ "${DNSROUTER_IP:-}" = "" ]; then + echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}" + exit 1 + fi + + # mount the resolver + LOCAL_RESOLVE="$DIR/../docker/dev/resolv.conf" + rm -rf "${LOCAL_RESOLVE}" + printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}" + + # bring up all remaining containers, except cypress! + docker compose up -d --remove-orphans stepca squid + docker compose pull db db-postgres authentik-redis authentik authentik-worker authentik-ldap + docker compose build --pull --parallel fullstack + docker compose up -d --remove-orphans fullstack + docker compose up -d --remove-orphans swagger + + # wait for main container to be healthy + bash "$DIR/wait-healthy" "$(docker compose ps --all -q fullstack)" 120 + + echo "" + echo -e "${CYAN}Admin UI: http://127.0.0.1:3081${RESET}" + echo -e "${CYAN}Nginx: http://127.0.0.1:3080${RESET}" + echo -e "${CYAN}Swagger Doc: http://127.0.0.1:3001${RESET}" + echo "" + + if [ "$1" == "-f" ]; then + echo -e "${BLUE}❯ ${YELLOW}Following Backend Container:${RESET}" + docker logs -f npm2dev.core + else + echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:" + echo " docker logs -f npm2dev.core" + fi +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/stop-dev b/scripts/stop-dev new file mode 100644 index 0000000..283b45b --- /dev/null +++ b/scripts/stop-dev @@ -0,0 +1,13 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Make sure docker exists +if hash docker 2>/dev/null; then + cd "${DIR}/.." + echo -e "${BLUE}❯ ${CYAN}Stopping Dev Stack ...${RESET}" + docker compose down --remove-orphans +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/wait-healthy b/scripts/wait-healthy new file mode 100644 index 0000000..503d6a9 --- /dev/null +++ b/scripts/wait-healthy @@ -0,0 +1,33 @@ +#!/bin/bash + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +if [ "$1" == "" ]; then + echo "Waits for a docker container to be healthy." + echo "Usage: $0 docker-container" + exit 1 +fi + +SERVICE=$1 +LOOPCOUNT=0 +HEALTHY= +LIMIT=${2:-90} + +echo -e "${BLUE}❯ ${CYAN}Waiting for healthy: ${YELLOW}${SERVICE}${RESET}" + +until [ "${HEALTHY}" = "healthy" ]; do + echo -n "." + sleep 1 + HEALTHY="$(docker inspect -f '{{.State.Health.Status}}' $SERVICE)" + ((LOOPCOUNT++)) + + if [ "$LOOPCOUNT" == "$LIMIT" ]; then + echo -e "${BLUE}❯ ${RED}Timed out waiting for healthy${RESET}" + docker logs --tail 50 "$SERVICE" + exit 1 + fi +done + +echo "" +echo -e "${BLUE}❯ ${GREEN}Healthy!${RESET}" diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 0000000..5ea6a2a --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,76 @@ +{ + "env": { + "browser": true, + "es6": true, + "cypress/globals": true + }, + "extends": [ + "eslint:recommended", + "plugin:cypress/recommended" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": [ + "cypress", + "chai-friendly", + "align-assignments" + ], + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "key-spacing": [ + "error", + { + "align": "value" + } + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "func-call-spacing": [ + "error", + "never" + ], + "keyword-spacing": [ + "error", + { + "before": true + } + ], + "no-irregular-whitespace": "error", + "cypress/no-assigning-return-values": "error", + "cypress/no-unnecessary-waiting": "warn", + "no-unused-expressions": 0, + "chai-friendly/no-unused-expressions": 2, + "align-assignments/align-assignments": [ + 2, + { + "requiresOnly": false + } + ] + } +} diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..bd6b6b9 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,2 @@ +results/* +cypress/results/* diff --git a/test/.prettierrc b/test/.prettierrc new file mode 100644 index 0000000..e19dc93 --- /dev/null +++ b/test/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 160, + "tabWidth": 4, + "useTabs": true, + "semi": true, + "singleQuote": true, + "bracketSpacing": true, + "jsxBracketSameLine": true, + "trailingComma": "all", + "proseWrap": "always" +} diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..63a2453 --- /dev/null +++ b/test/README.md @@ -0,0 +1,46 @@ +# Cypress Test Suite + +## Running Locally + +``` +cd nginxproxymanager/test +yarn install +yarn run cypress +``` + +## VS Code + +Editor settings are not committed to the repository, typically because each developer has their own settings. Below is a list of common setting that may help, +so feel free to try them or ignore them, you are a strong independent developer. You can add settings to either "user" or "workspace" but we recommend using +"workspace" as each project is different. + +### ESLint + +The ESLint extension only works on JavaScript files by default, so add the following to your workspace settings and reload VSCode. + +``` +"eslint.autoFixOnSave": true, +"eslint.validate": [ + { "language": "javascript", "autoFix": true }, + "html" +] +``` + +> NOTE: If you've also set the editor.formatOnSave option to true in your settings.json, you'll need to add the following config to prevent running 2 formatting +> commands on save for JavaScript and TypeScript files: + +``` +"editor.formatOnSave": true, +"[javascript]": { + "editor.formatOnSave": false, +}, +"[javascriptreact]": { + "editor.formatOnSave": false, +}, +"[typescript]": { + "editor.formatOnSave": false, +}, +"[typescriptreact]": { + "editor.formatOnSave": false, +}, +``` diff --git a/test/cypress/Dockerfile b/test/cypress/Dockerfile new file mode 100644 index 0000000..bd82108 --- /dev/null +++ b/test/cypress/Dockerfile @@ -0,0 +1,23 @@ +FROM cypress/included:15.11.0 + +# Disable Cypress CLI colors +ENV FORCE_COLOR=0 +ENV NO_COLOR=1 + +# testssl.sh and mkcert +RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz" -O /tmp/testssl.tgz -q \ + && tar -xzf /tmp/testssl.tgz -C /tmp \ + && mv /tmp/testssl.sh-3.2rc4 /testssl \ + && rm /tmp/testssl.tgz \ + && apt-get update \ + && apt-get install -y bsdmainutils curl dnsutils \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \ + && chmod +x /bin/mkcert + +COPY --chown=1000 ./test /test +WORKDIR /test +RUN yarn install && yarn cache clean +ENTRYPOINT [] +CMD ["cypress", "run"] diff --git a/test/cypress/config/ci.mjs b/test/cypress/config/ci.mjs new file mode 100644 index 0000000..2597c19 --- /dev/null +++ b/test/cypress/config/ci.mjs @@ -0,0 +1,23 @@ +import { defineConfig } from 'cypress'; +import pluginSetup from '../plugins/index.mjs'; + +export default defineConfig({ + requestTimeout: 30000, + defaultCommandTimeout: 20000, + reporter: "cypress-multi-reporters", + reporterOptions: { + configFile: "multi-reporter.json" + }, + video: true, + videosFolder: "results/videos", + screenshotsFolder: "results/screenshots", + e2e: { + setupNodeEvents(on, config) { + return pluginSetup(on, config); + }, + env: { + swaggerBase: `{{baseUrl}}/api/schema?ts=${Date.now()}`, + }, + baseUrl: "http://fullstack:81", + } +}); diff --git a/test/cypress/config/dev.mjs b/test/cypress/config/dev.mjs new file mode 100644 index 0000000..f32d973 --- /dev/null +++ b/test/cypress/config/dev.mjs @@ -0,0 +1,23 @@ +import { defineConfig } from 'cypress'; +import pluginSetup from '../plugins/index.mjs'; + +export default defineConfig({ + requestTimeout: 30000, + defaultCommandTimeout: 20000, + reporter: "cypress-multi-reporters", + reporterOptions: { + configFile: "multi-reporter.json" + }, + video: true, + videosFolder: "results/videos", + screenshotsFolder: "results/screenshots", + e2e: { + setupNodeEvents(on, config) { + return pluginSetup(on, config); + }, + env: { + swaggerBase: `{{baseUrl}}/api/schema?ts=${Date.now()}`, + }, + baseUrl: "http://127.0.0.1:3081", + } +}); diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js new file mode 100644 index 0000000..00da38b --- /dev/null +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -0,0 +1,98 @@ +/// + +describe('Certificates endpoints', () => { + let token; + let certID; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Validate custom certificate', () => { + cy.task('backendApiPostFiles', { + token: token, + path: '/api/nginx/certificates/validate', + files: { + certificate: 'test.example.com.pem', + certificate_key: 'test.example.com-key.pem', + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/nginx/certificates/validate', data); + expect(data).to.have.property('certificate'); + expect(data).to.have.property('certificate_key'); + }); + }); + + it('Custom certificate lifecycle', () => { + // Create custom cert + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/certificates', + data: { + provider: "other", + nice_name: "Test Certificate", + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data); + expect(data).to.have.property('id'); + certID = data.id; + + // Upload files + cy.task('backendApiPostFiles', { + token: token, + path: `/api/nginx/certificates/${certID}/upload`, + files: { + certificate: 'test.example.com.pem', + certificate_key: 'test.example.com-key.pem', + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data); + expect(data).to.have.property('certificate'); + expect(data).to.have.property('certificate_key'); + + // Get all certs + cy.task('backendApiGet', { + token: token, + path: '/api/nginx/certificates?expand=owner' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data); + expect(data.length).to.be.greaterThan(0); + + // Delete cert + cy.task('backendApiDelete', { + token: token, + path: `/api/nginx/certificates/${certID}` + }).then((data) => { + cy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data); + expect(data).to.be.equal(true); + }); + }); + }); + }); + }); + + it('Request Certificate - CVE-2024-46256/CVE-2024-46257', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/certificates', + data: { + domain_names: ['test.com"||echo hello-world||\\\\n test.com"'], + meta: { + dns_challenge: false, + }, + provider: 'letsencrypt', + }, + returnOnError: true, + }).then((data) => { + cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data); + expect(data).to.have.property('error'); + expect(data.error).to.have.property('message'); + expect(data.error).to.have.property('code'); + expect(data.error.code).to.equal(400); + expect(data.error.message).to.contain('data/domain_names/0 must match pattern'); + }); + }); +}); diff --git a/test/cypress/e2e/api/Dashboard.cy.js b/test/cypress/e2e/api/Dashboard.cy.js new file mode 100644 index 0000000..8da332a --- /dev/null +++ b/test/cypress/e2e/api/Dashboard.cy.js @@ -0,0 +1,26 @@ +/// + +describe('Dashboard endpoints', () => { + let token; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to get host counts', () => { + cy.task('backendApiGet', { + token: token, + path: '/api/reports/hosts' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/reports/hosts', data); + expect(data).to.have.property('dead'); + expect(data).to.have.property('proxy'); + expect(data).to.have.property('redirection'); + expect(data).to.have.property('stream'); + }); + }); + +}); diff --git a/test/cypress/e2e/api/FullCertProvision.cy.js b/test/cypress/e2e/api/FullCertProvision.cy.js new file mode 100644 index 0000000..2c7d9ff --- /dev/null +++ b/test/cypress/e2e/api/FullCertProvision.cy.js @@ -0,0 +1,59 @@ +/// + +describe('Full Certificate Provisions', () => { + let token; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to create new http certificate', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/certificates', + data: { + domain_names: [ + 'website1.example.com' + ], + meta: { + dns_challenge: false + }, + provider: 'letsencrypt' + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data.provider).to.be.equal('letsencrypt'); + }); + }); + + it('Should be able to create new DNS certificate with Powerdns', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/certificates', + data: { + domain_names: [ + 'website2.example.com' + ], + meta: { + dns_challenge: true, + dns_provider: 'powerdns', + dns_provider_credentials: 'dns_powerdns_api_url = http://ns1.pdns:8081\r\ndns_powerdns_api_key = npm', + propagation_seconds: 5, + }, + provider: 'letsencrypt' + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data.provider).to.be.equal('letsencrypt'); + expect(data.meta.dns_provider).to.be.equal('powerdns'); + }); + }); + +}); diff --git a/test/cypress/e2e/api/Health.cy.js b/test/cypress/e2e/api/Health.cy.js new file mode 100644 index 0000000..0469c70 --- /dev/null +++ b/test/cypress/e2e/api/Health.cy.js @@ -0,0 +1,28 @@ +/// + +describe('Basic API checks', () => { + it('Should return a valid health payload', () => { + cy.task('backendApiGet', { + path: '/api/', + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('get', 200, '/', data); + }); + }); + + it('Should return a valid schema payload', () => { + cy.task('backendApiGet', { + path: `/api/schema?ts=${Date.now()}`, + }).then((data) => { + expect(data.openapi).to.be.equal('3.1.0'); + }); + }); + + it('Should return a valid payload for version check', () => { + cy.task('backendApiGet', { + path: `/api/version/check?ts=${Date.now()}`, + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/version/check', data); + }); + }); +}); diff --git a/test/cypress/e2e/api/Ldap.cy.js b/test/cypress/e2e/api/Ldap.cy.js new file mode 100644 index 0000000..64e1773 --- /dev/null +++ b/test/cypress/e2e/api/Ldap.cy.js @@ -0,0 +1,65 @@ +/// + +describe('LDAP with Authentik', () => { + let _token; + if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + _token = tok; + + // cy.task('backendApiPut', { + // token: token, + // path: '/api/settings/ldap-auth', + // data: { + // value: { + // host: 'authentik-ldap:3389', + // base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io', + // user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io', + // email_property: 'mail', + // name_property: 'sn', + // self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))', + // auto_create_user: true + // } + // } + // }).then((data) => { + // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + // expect(data.result).to.have.property('id'); + // expect(data.result.id).to.be.greaterThan(0); + // }); + + // cy.task('backendApiPut', { + // token: token, + // path: '/api/settings/auth-methods', + // data: { + // value: [ + // 'local', + // 'ldap' + // ] + // } + // }).then((data) => { + // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + // expect(data.result).to.have.property('id'); + // expect(data.result.id).to.be.greaterThan(0); + // }); + }); + }); + + it.skip('Should log in with LDAP', () => { + // cy.task('backendApiPost', { + // token: token, + // path: '/api/auth', + // data: { + // // Authentik LDAP creds: + // type: 'ldap', + // identity: 'cypress', + // secret: 'fqXBfUYqHvYqiwBHWW7f' + // } + // }).then((data) => { + // cy.validateSwaggerSchema('post', 200, '/auth', data); + // expect(data.result).to.have.property('token'); + // }); + }); + } +}); diff --git a/test/cypress/e2e/api/OAuth.cy.js b/test/cypress/e2e/api/OAuth.cy.js new file mode 100644 index 0000000..c5c819f --- /dev/null +++ b/test/cypress/e2e/api/OAuth.cy.js @@ -0,0 +1,97 @@ +/// + +describe('OAuth with Authentik', () => { + let _token; + if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { + + before(() => { + cy.getToken().then((tok) => { + _token = tok; + + // cy.task('backendApiPut', { + // token: token, + // path: '/api/settings/oauth-auth', + // data: { + // value: { + // client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU', + // client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq', + // authorization_url: 'http://authentik:9000/application/o/authorize/', + // resource_url: 'http://authentik:9000/application/o/userinfo/', + // token_url: 'http://authentik:9000/application/o/token/', + // logout_url: 'http://authentik:9000/application/o/npm/end-session/', + // identifier: 'preferred_username', + // scopes: [], + // auto_create_user: true + // } + // } + // }).then((data) => { + // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + // expect(data.result).to.have.property('id'); + // expect(data.result.id).to.be.greaterThan(0); + // }); + + // cy.task('backendApiPut', { + // token: token, + // path: '/api/settings/auth-methods', + // data: { + // value: [ + // 'local', + // 'oauth' + // ] + // } + // }).then((data) => { + // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + // expect(data.result).to.have.property('id'); + // expect(data.result.id).to.be.greaterThan(0); + // }); + }); + }); + + it.skip('Should log in with OAuth', () => { + // cy.task('backendApiGet', { + // path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')), + // }).then((data) => { + // expect(data).to.have.property('result'); + + // cy.origin('http://authentik:9000', {args: data.result}, (url) => { + // cy.visit(url); + // cy.get('ak-flow-executor') + // .shadow() + // .find('ak-stage-identification') + // .shadow() + // .find('input[name="uidField"]', { visible: true }) + // .type('cypress'); + + // cy.get('ak-flow-executor') + // .shadow() + // .find('ak-stage-identification') + // .shadow() + // .find('button[type="submit"]', { visible: true }) + // .click(); + + // cy.get('ak-flow-executor') + // .shadow() + // .find('ak-stage-password') + // .shadow() + // .find('input[name="password"]', { visible: true }) + // .type('fqXBfUYqHvYqiwBHWW7f'); + + // cy.get('ak-flow-executor') + // .shadow() + // .find('ak-stage-password') + // .shadow() + // .find('button[type="submit"]', { visible: true }) + // .click(); + // }) + + // // we should be logged in + // cy.get('#root p.chakra-text') + // .first() + // .should('have.text', 'Nginx Proxy Manager'); + + // // logout: + // cy.clearLocalStorage(); + // }); + }); + } +}); diff --git a/test/cypress/e2e/api/ProxyHosts.cy.js b/test/cypress/e2e/api/ProxyHosts.cy.js new file mode 100644 index 0000000..5f437cf --- /dev/null +++ b/test/cypress/e2e/api/ProxyHosts.cy.js @@ -0,0 +1,48 @@ +/// + +describe('Proxy Hosts endpoints', () => { + let token; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to create a http host', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/proxy-hosts', + data: { + domain_names: ['test.example.com'], + forward_scheme: 'http', + forward_host: '1.1.1.1', + forward_port: 80, + access_list_id: '0', + certificate_id: 0, + meta: { + dns_challenge: false + }, + advanced_config: '', + locations: [], + block_exploits: false, + caching_enabled: false, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + ssl_forced: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/proxy-hosts', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled'); + expect(data).to.have.property("enabled", true); + expect(data).to.have.property('meta'); + expect(typeof data.meta.nginx_online).to.be.equal('undefined'); + }); + }); + +}); diff --git a/test/cypress/e2e/api/Settings.cy.js b/test/cypress/e2e/api/Settings.cy.js new file mode 100644 index 0000000..a925b2d --- /dev/null +++ b/test/cypress/e2e/api/Settings.cy.js @@ -0,0 +1,125 @@ +/// + +describe('Settings endpoints', () => { + let token; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Get all settings', () => { + cy.task('backendApiGet', { + token: token, + path: '/api/settings', + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/settings', data); + expect(data.length).to.be.greaterThan(0); + }); + }); + + it('Get default-site setting', () => { + cy.task('backendApiGet', { + token: token, + path: '/api/settings/default-site', + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/settings/{settingID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.equal('default-site'); + }); + }); + + it('Default Site congratulations', () => { + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: 'congratulations', + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.equal('default-site'); + expect(data).to.have.property('value'); + expect(data.value).to.be.equal('congratulations'); + }); + }); + + it('Default Site 404', () => { + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: '404', + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.equal('default-site'); + expect(data).to.have.property('value'); + expect(data.value).to.be.equal('404'); + }); + }); + + it('Default Site 444', () => { + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: '444', + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.equal('default-site'); + expect(data).to.have.property('value'); + expect(data.value).to.be.equal('444'); + }); + }); + + it('Default Site redirect', () => { + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: 'redirect', + meta: { + redirect: 'https://www.google.com', + }, + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.equal('default-site'); + expect(data).to.have.property('value'); + expect(data.value).to.be.equal('redirect'); + expect(data).to.have.property('meta'); + expect(data.meta).to.have.property('redirect'); + expect(data.meta.redirect).to.be.equal('https://www.google.com'); + }); + }); + + it('Default Site html', () => { + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: 'html', + meta: { + html: '

    hello world

    ' + }, + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.equal('default-site'); + expect(data).to.have.property('value'); + expect(data.value).to.be.equal('html'); + expect(data).to.have.property('meta'); + expect(data.meta).to.have.property('html'); + expect(data.meta.html).to.be.equal('

    hello world

    '); + }); + }); +}); diff --git a/test/cypress/e2e/api/Streams.cy.js b/test/cypress/e2e/api/Streams.cy.js new file mode 100644 index 0000000..10809f8 --- /dev/null +++ b/test/cypress/e2e/api/Streams.cy.js @@ -0,0 +1,211 @@ +/// + +describe('Streams', () => { + let token; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + // Set default site content + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: 'html', + meta: { + html: '

    yay it works

    ' + }, + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + }); + }); + + // Create a custom cert pair + cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => { + expect(result.exitCode).to.eq(0); + // Install CA + cy.exec('mkcert -install').then((result) => { + expect(result.exitCode).to.eq(0); + }); + }); + + cy.exec('rm -f /test/results/testssl.json'); + }); + + it('Should be able to create TCP Stream', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1500, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: 0, + meta: {}, + tcp_forwarding: true, + udp_forwarding: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled', true); + expect(data).to.have.property('tcp_forwarding', true); + expect(data).to.have.property('udp_forwarding', false); + + cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => { + expect(result.exitCode).to.eq(0); + expect(result.stdout).to.contain('yay it works'); + }); + }); + }); + + it('Should be able to create UDP Stream', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1501, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: 0, + meta: {}, + tcp_forwarding: false, + udp_forwarding: true + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled', true); + expect(data).to.have.property('tcp_forwarding', false); + expect(data).to.have.property('udp_forwarding', true); + }); + }); + + it('Should be able to create TCP/UDP Stream', () => { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1502, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: 0, + meta: {}, + tcp_forwarding: true, + udp_forwarding: true + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled', true); + expect(data).to.have.property('tcp_forwarding', true); + expect(data).to.have.property('udp_forwarding', true); + + cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => { + expect(result.exitCode).to.eq(0); + expect(result.stdout).to.contain('yay it works'); + }); + }); + }); + + it('Should be able to create SSL TCP Stream', () => { + let certID = 0; + + // Create custom cert + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/certificates', + data: { + provider: "other", + nice_name: "Custom Certificate for SSL Stream", + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data); + expect(data).to.have.property('id'); + certID = data.id; + + // Upload files + cy.task('backendApiPostFiles', { + token: token, + path: `/api/nginx/certificates/${certID}/upload`, + files: { + certificate: 'website1.pem', + certificate_key: 'website1.key.pem', + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data); + expect(data).to.have.property('certificate'); + expect(data).to.have.property('certificate_key'); + + // Create the stream + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1503, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: certID, + meta: {}, + tcp_forwarding: true, + udp_forwarding: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property("enabled", true); + expect(data).to.have.property('tcp_forwarding', true); + expect(data).to.have.property('udp_forwarding', false); + expect(data).to.have.property('certificate_id', certID); + + // Check the ssl termination + cy.task('log', '[testssl.sh] Running ...'); + cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', { + timeout: 120000, // 2 minutes + }).then((result) => { + cy.task('log', `[testssl.sh] ${result.stdout}`); + + const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"]; + const ignoredIDs = [ + 'cert_chain_of_trust', + 'cert_extlifeSpan', + 'cert_revocation', + 'engine_problem', + 'overall_grade', + ]; + + cy.readFile('/test/results/testssl.json').then((data) => { + // Parse each array item + for (let i = 0; i < data.length; i++) { + const item = data[i]; + if (ignoredIDs.includes(item.id)) { + continue; + } + expect(item.severity).to.be.oneOf(allowedSeverities); + } + }); + }); + }); + }); + }); + }); + + it('Should be able to List Streams', () => { + cy.task('backendApiGet', { + token: token, + path: '/api/nginx/streams?expand=owner,certificate', + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/nginx/streams', data); + expect(data.length).to.be.greaterThan(0); + expect(data[0]).to.have.property('id'); + expect(data[0]).to.have.property('enabled'); + }); + }); + +}); diff --git a/test/cypress/e2e/api/SwaggerSchema.cy.js b/test/cypress/e2e/api/SwaggerSchema.cy.js new file mode 100644 index 0000000..ba6fc78 --- /dev/null +++ b/test/cypress/e2e/api/SwaggerSchema.cy.js @@ -0,0 +1,7 @@ +/// + +describe('Swagger Schema Linting', () => { + it('Should be a completely valid schema', () => { + cy.validateSwaggerFile('/api/schema', 'results/swagger-schema.json'); + }); +}); diff --git a/test/cypress/e2e/api/Users.cy.js b/test/cypress/e2e/api/Users.cy.js new file mode 100644 index 0000000..044dcde --- /dev/null +++ b/test/cypress/e2e/api/Users.cy.js @@ -0,0 +1,49 @@ +/// + +describe('Users endpoints', () => { + let token; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to get yourself', () => { + cy.task('backendApiGet', { + token: token, + path: '/api/users/me' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users/{userID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all users', () => { + cy.task('backendApiGet', { + token: token, + path: '/api/users' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users', data); + expect(data.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to update yourself', () => { + cy.task('backendApiPut', { + token: token, + path: '/api/users/me', + data: { + name: 'changed name' + } + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/users/{userID}', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data.name).to.be.equal('changed name'); + }); + }); + +}); diff --git a/test/cypress/fixtures/test.example.com-key.pem b/test/cypress/fixtures/test.example.com-key.pem new file mode 100644 index 0000000..307cdc3 --- /dev/null +++ b/test/cypress/fixtures/test.example.com-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd +qACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w +rbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge +Yz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ +oxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z +Eo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X +zGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU +ia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6 +YHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe +a0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu +W0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw +o72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW +H8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+ +N+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh +ELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU +MDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31 +qjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq +cMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9 +vMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO +utTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V +g0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1 +mJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq +YatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8 +EQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk +8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM +RrKmPK/msHKK/sVHiL+NFqo= +-----END PRIVATE KEY----- diff --git a/test/cypress/fixtures/test.example.com.pem b/test/cypress/fixtures/test.example.com.pem new file mode 100644 index 0000000..16340cd --- /dev/null +++ b/test/cypress/fixtures/test.example.com.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw +gZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1 +cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD +DDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu +b3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe +bWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93 +QEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu +GBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+ +2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU +QJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB +Ia5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE +OPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G +A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB +/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t +MA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy +l8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s +VXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn +ATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt +IImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u +m+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV +XxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp +1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw +hp8bJUp/QN7pnOVCDbjTQ+HVMXw= +-----END CERTIFICATE----- diff --git a/test/cypress/plugins/backendApi/client.mjs b/test/cypress/plugins/backendApi/client.mjs new file mode 100644 index 0000000..78618ab --- /dev/null +++ b/test/cypress/plugins/backendApi/client.mjs @@ -0,0 +1,148 @@ +import axios from "axios"; +import logger from "./logger.mjs"; + +const BackendApi = function (config, token) { + this.config = config; + this.token = token; + + this.axios = axios.create({ + baseURL: config.baseUrl, + timeout: 90000, + }); +}; + +/** + * @param {string} token + */ +BackendApi.prototype.setToken = function (token) { + this.token = token; +}; + +/** + * @param {bool} returnOnError + */ +BackendApi.prototype._prepareOptions = function (returnOnError) { + const options = { + headers: { + Accept: "application/json", + }, + }; + if (this.token) { + options.headers.Authorization = `Bearer ${this.token}`; + } + if (returnOnError) { + options.validateStatus = () => true; + } + return options; +}; + +/** + * @param {*} response + * @param {function} resolve + * @param {function} reject + * @param {bool} returnOnError + */ +BackendApi.prototype._handleResponse = ( + response, + resolve, + reject, + returnOnError, +) => { + logger("Response data:", response.data); + if ( + !returnOnError && + typeof response.data === "object" && + typeof response.data.error === "object" + ) { + if ( + typeof response.data === "object" && + typeof response.data.error === "object" && + typeof response.data.error.message !== "undefined" + ) { + reject( + new Error( + `${response.data.error.code}: ${response.data.error.message}`, + ), + ); + } else { + reject(new Error(`Error ${response.status}`)); + } + } else { + resolve(response.data); + } +}; + +/** + * @param {*} err + * @param {function} resolve + * @param {function} reject + * @param {bool} returnOnError + */ +BackendApi.prototype._handleError = (err, resolve, reject, returnOnError) => { + logger("Axios Error:", err); + if (returnOnError) { + resolve(typeof err.response.data !== "undefined" ? err.response.data : err); + } else { + reject(err); + } +}; + +/** + * @param {string} method + * @param {string} path + * @param {bool} [returnOnError] + * @param {*} [data] + * @returns {Promise} + */ +BackendApi.prototype.request = function (method, path, returnOnError, data) { + logger(method.toUpperCase(), path); + const options = this._prepareOptions(returnOnError); + + return new Promise((resolve, reject) => { + const opts = { + method: method, + url: path, + ...options, + }; + if (data !== undefined && data !== null) { + opts.data = data; + } + + this.axios(opts) + .then((response) => { + this._handleResponse(response, resolve, reject, returnOnError); + }) + .catch((err) => { + this._handleError(err, resolve, reject, returnOnError); + }); + }); +}; + +/** + * @param {string} path + * @param {form} form + * @param {bool} [returnOnError] + * @returns {Promise} + */ +BackendApi.prototype.postForm = function (path, form, returnOnError) { + logger("POST", this.config.baseUrl + path); + const options = this._prepareOptions(returnOnError); + + return new Promise((resolve, reject) => { + const opts = { + ...options, + ...form.getHeaders(), + }; + + this.axios + .post(path, form, opts) + .then((response) => { + this._handleResponse(response, resolve, reject, returnOnError); + }) + .catch((err) => { + this._handleError(err, resolve, reject, returnOnError); + }); + }); +}; + +export default BackendApi; diff --git a/test/cypress/plugins/backendApi/logger.mjs b/test/cypress/plugins/backendApi/logger.mjs new file mode 100644 index 0000000..613bc47 --- /dev/null +++ b/test/cypress/plugins/backendApi/logger.mjs @@ -0,0 +1,7 @@ +const log = (...args) => { + const arr = args; + arr.unshift("[Backend API]"); + console.log(...arr); +}; + +export default log; diff --git a/test/cypress/plugins/backendApi/task.mjs b/test/cypress/plugins/backendApi/task.mjs new file mode 100644 index 0000000..edc5015 --- /dev/null +++ b/test/cypress/plugins/backendApi/task.mjs @@ -0,0 +1,100 @@ +import fs from "node:fs"; +import FormData from "form-data"; +import Client from "./client.mjs"; +import logger from "./logger.mjs"; + +export default (config) => { + logger("Client Ready using", config.baseUrl); + + return { + /** + * @param {object} options + * @param {string} options.path API path + * @param {string} [options.token] JWT + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiGet: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request("get", options.path, options.returnOnError || false); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.data + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPost: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "post", + options.path, + options.returnOnError || false, + options.data, + ); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.files + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPostFiles: (options) => { + const api = new Client(config); + api.setToken(options.token); + + const form = new FormData(); + for (const [key, value] of Object.entries(options.files)) { + form.append( + key, + fs.createReadStream(`${config.fixturesFolder}/${value}`), + ); + } + return api.postForm(options.path, form, options.returnOnError || false); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.data + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPut: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "put", + options.path, + options.returnOnError || false, + options.data, + ); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiDelete: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "delete", + options.path, + options.returnOnError || false, + ); + }, + }; +}; diff --git a/test/cypress/plugins/index.mjs b/test/cypress/plugins/index.mjs new file mode 100644 index 0000000..1058d63 --- /dev/null +++ b/test/cypress/plugins/index.mjs @@ -0,0 +1,27 @@ +import { SwaggerValidation } from "@jc21/cypress-swagger-validation"; +import chalk from "chalk"; +import backendTask from "./backendApi/task.mjs"; + +export default (on, config) => { + // Replace swaggerBase config var wildcard + if (typeof config.env.swaggerBase !== "undefined") { + config.env.swaggerBase = config.env.swaggerBase.replace( + "{{baseUrl}}", + config.baseUrl, + ); + } + + // Plugin Events + on("task", SwaggerValidation(config)); + on("task", backendTask(config)); + on("task", { + log(message) { + console.log( + `${chalk.cyan.bold("[")}${chalk.blue.bold("LOG")}${chalk.cyan.bold("]")} ${chalk.red.bold(message)}`, + ); + return null; + }, + }); + + return config; +}; diff --git a/test/cypress/support/commands.mjs b/test/cypress/support/commands.mjs new file mode 100644 index 0000000..7d9224d --- /dev/null +++ b/test/cypress/support/commands.mjs @@ -0,0 +1,153 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// + +import 'cypress-wait-until'; + +Cypress.Commands.add('randomString', (length) => { + let result = ''; + const characters = 'ABCDEFGHIJK LMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}); + +/** + * Check the swagger schema file: + * + * @param {string} url + * @param {string} savePath + */ +Cypress.Commands.add("validateSwaggerFile", (url, savePath) => { + cy.task('log', `validateSwaggerFile: ${url} -- ${savePath}`) + .then(() => { + return cy + .request(url) + .then((response) => cy.writeFile(savePath, response.body, { log: false })) + .then(() => cy.exec(`yarn swagger-lint '${savePath}'`, { failOnNonZeroExit: false })) + .then((result) => cy.task('log', `Swagger Vacuum Results:\n${result.stdout || ''}`) + .then(() => expect(result.exitCode).to.eq(0))); + }); +}); + +/** + * Check the swagger schema for a specific endpoint: + * + * @param {string} method API Method in swagger doc, "get", "put", "post", "delete" + * @param {integer} code Swagger doc endpoint response code, exactly as defined in swagger doc + * @param {string} path Swagger doc endpoint path, exactly as defined in swagger doc + * @param {*} data The API response data to check against the swagger schema + */ +Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => { + cy.task('validateSwaggerSchema', { + file: Cypress.env('swaggerBase'), + endpoint: path, + method: method, + statusCode: code, + responseSchema: data, + verbose: true + }).should('equal', null); +}); + +Cypress.Commands.add('createInitialUser', (defaultUser) => { + let user = { + name: 'Cypress McGee', + nickname: 'Cypress', + email: 'cypress@example.com', + auth: { + type: 'password', + secret: 'changeme' + }, + }; + + if (typeof defaultUser === 'object' && defaultUser) { + user = Object.assign({}, user, defaultUser); + } + + return cy.task('backendApiPost', { + path: '/api/users', + data: user, + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('post', 201, '/users', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + cy.wrap(data); + }); +}); + +Cypress.Commands.add('getToken', (defaultUser, defaultAuth) => { + if (typeof defaultAuth === 'object' && defaultAuth) { + if (!defaultUser) { + defaultUser = {}; + } + defaultUser.auth = defaultAuth; + } + + cy.task('backendApiGet', { + path: '/api/', + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('get', 200, '/', data); + + if (!data.setup) { + cy.log('Setup = false'); + // create a new user + cy.createInitialUser(defaultUser).then(() => { + return cy.getToken(defaultUser); + }); + } else { + let auth = { + identity: 'cypress@example.com', + secret: 'changeme', + }; + + if (typeof defaultUser === 'object' && defaultUser && typeof defaultUser.auth === 'object' && defaultUser.auth) { + auth = Object.assign({}, auth, defaultUser.auth); + } + + cy.log('Setup = true'); + // login with existing user + cy.task('backendApiPost', { + path: '/api/tokens', + data: auth, + }).then((res) => { + cy.wrap(res.token); + }); + } + }); +}); + +Cypress.Commands.add('resetUsers', () => { + cy.task('backendApiDelete', { + path: '/api/users' + }).then((data) => { + expect(data).to.be.equal(true); + cy.wrap(data); + }); +}); + +// TODO: copied from v3, is this usable? +Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeout = 60) => { + cy.log(`Waiting for certificate (${certID}) status (${expected}) timeout (${timeout})`); + + cy.waitUntil(() => cy.task('backendApiGet', { + token: token, + path: `/api/certificates/${certID}` + }).then((data) => { + return data.result.status === expected; + }), { + errorMsg: 'Waiting for certificate status failed', + timeout: timeout * 1000, + interval: 5000 + }); +}); diff --git a/test/cypress/support/e2e.js b/test/cypress/support/e2e.js new file mode 100644 index 0000000..2d8a13a --- /dev/null +++ b/test/cypress/support/e2e.js @@ -0,0 +1,7 @@ +import './commands.mjs'; + +Cypress.on('uncaught:exception', (/*err, runnable*/) => { + // returning false here prevents Cypress from + // failing the test + return false; +}); diff --git a/test/jsconfig.json b/test/jsconfig.json new file mode 100644 index 0000000..9caeeb9 --- /dev/null +++ b/test/jsconfig.json @@ -0,0 +1,13 @@ +{ + "include": [ + "./node_modules/cypress", + "cypress/**/*.js", + "cypress/config/dev.mjs", + "cypress/config/ci.mjs", + "cypress/plugins/index.mjs", + "cypress/plugins/backendApi/task.mjs", + "cypress/plugins/backendApi/logger.mjs", + "cypress/plugins/backendApi/client.mjs", + "cypress/support/commands.mjs" + ] +} diff --git a/test/multi-reporter.json b/test/multi-reporter.json new file mode 100644 index 0000000..73a3526 --- /dev/null +++ b/test/multi-reporter.json @@ -0,0 +1,9 @@ +{ + "reporterEnabled": "spec, mocha-junit-reporter", + "mochaJunitReporterReporterOptions": { + "jenkinsMode": true, + "rootSuiteTitle": "Cypress.npm", + "jenkinsClassnamePrefix": "Cypress.npm.", + "mochaFile": "results/junit/cypress.npm.[hash].xml" + } +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..bb454ba --- /dev/null +++ b/test/package.json @@ -0,0 +1,31 @@ +{ + "name": "npm-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "@jc21/cypress-swagger-validation": "^0.3.2", + "@quobix/vacuum": "^0.24.0", + "axios": "^1.13.6", + "chalk": "^5.6.2", + "cypress": "^15.11.0", + "cypress-multi-reporters": "^2.0.5", + "cypress-wait-until": "^3.0.2", + "eslint": "^10.0.2", + "eslint-plugin-align-assignments": "^1.1.2", + "eslint-plugin-chai-friendly": "^1.1.0", + "eslint-plugin-cypress": "^6.1.0", + "form-data": "^4.0.5", + "lodash": "^4.17.23", + "mocha": "^11.7.5", + "mocha-junit-reporter": "^2.2.1" + }, + "scripts": { + "cypress": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress open --config-file=cypress/config/ci.mjs", + "cypress:headless": "HTTP_PROXY=127.0.0.1:8128 HTTPS_PROXY=127.0.0.1:8128 cypress run --config-file=cypress/config/ci.mjs", + "cypress:dev": "cypress run --config-file=cypress/config/dev.mjs", + "swagger-lint": "vacuum lint -b -q -d -a --no-clip -n=warn" + }, + "author": "", + "license": "ISC" +} diff --git a/test/vacuum-rules.yaml b/test/vacuum-rules.yaml new file mode 100644 index 0000000..fc129b7 --- /dev/null +++ b/test/vacuum-rules.yaml @@ -0,0 +1,971 @@ +description: Recommended rules for a high quality specification. +documentationUrl: https://quobix.com/vacuum/rulesets/recommended +rules: + component-description: + category: + description: Documentation is really important, in OpenAPI, just about everything can and should have a description. This set of rules checks for absent descriptions, poor quality descriptions (copy/paste), or short descriptions. + id: descriptions + name: Descriptions + description: Component description check + formats: + - oas3 + - oas3_1 + - oas3_2 + given: $ + howToFix: Components are the inputs and outputs of a specification. A user needs to be able to understand each component and what id does. Descriptions are critical to understanding components. Add a description! + id: component-description + recommended: true + resolved: true + severity: warn + then: + function: oasComponentDescriptions + type: validation + duplicate-paths: + category: + description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth. + id: operations + name: Operations + description: Paths cannot be duplicated; only the last definition will be kept. + formats: + - oas3 + - oas3_1 + - oas3_2 + given: $ + howToFix: Duplicate path definitions found in your OpenAPI specification. In YAML, duplicate keys are allowed, but only the last occurrence is used. This means earlier path definitions are silently ignored, which can lead to missing API endpoints in your specification. + id: duplicate-paths + recommended: true + severity: error + then: + function: duplicatePaths + type: validation + duplicated-entry-in-enum: + category: + description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures. + id: schemas + name: Schemas + description: Enum values must not have duplicate entry + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: Enums need to be unique, you can't duplicate them in the same definition. Please remove the duplicate value. + id: duplicated-entry-in-enum + recommended: true + severity: error + then: + function: duplicatedEnum + type: validation + info-description: + category: + description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed. + id: information + name: Contract Information + description: Info section is missing a description + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: The 'info' section is missing a description, surely you want people to know what this spec is all about, right? + id: info-description + recommended: true + resolved: true + severity: error + then: + function: infoDescription + type: validation + info-license-spdx: + category: + description: The info object contains licencing, contact, authorship details and more. Checks to confirm required details have been completed. + id: information + name: Contract Information + description: License section cannot contain both an identifier and a URL, they are mutually exclusive. + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: A license can contain either a URL or an SPDX identifier, but not both, They are mutually exclusive and cannot both be present. Choose one or the other + id: info-license-spdx + recommended: true + resolved: true + severity: error + then: + function: infoLicenseURLSPDX + type: validation + migrate-zally-ignore: + category: + description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications. + id: validation + name: Validation + description: x-zally-ignore keys should be migrated to x-lint-ignore for compatibility with vacuum + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: Migrate x-zally-ignore directives to vacuum's x-lint-ignore. Rename the key to x-lint-ignore and update the ignored rule id to the vacuum equivalent rule. + id: migrate-zally-ignore + recommended: true + resolved: true + severity: warn + then: + function: migrateZallyIgnore + type: validation + no-$ref-siblings: + category: + description: Schemas are how request bodies and response payloads are defined. They define the data going in and the data flowing out of an operation. These rules check for structural validity, checking types, checking required fields and validating correct use of structures. + id: schemas + name: Schemas + description: $ref values cannot be placed next to other properties (like a description) + formats: + - oas2 + - oas3 + given: $ + howToFix: $ref values must not be placed next to sibling nodes, There should only be a single node when using $ref. A common mistake is adding 'description' next to a $ref. This is wrong. remove all siblings! + id: no-$ref-siblings + recommended: true + severity: error + then: + function: refSiblings + type: validation + no-ambiguous-paths: + category: + description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth. + id: operations + name: Operations + description: Paths need to resolve unambiguously from one another + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: Paths must all resolve unambiguously, they can't be confused with one another (/{id}/ambiguous and /ambiguous/{id} are the same thing. Make sure every path and the variables used are unique and do conflict with one another. Check the ordering of variables and the naming of path segments. + id: no-ambiguous-paths + recommended: true + resolved: true + severity: error + then: + function: noAmbiguousPaths + type: validation + no-eval-in-markdown: + category: + description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications. + id: validation + name: Validation + description: Markdown descriptions must not have `eval()` statements' + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: Remove all references to 'eval()' in the description. These can be used by malicious actors to embed code in contracts that is then executed when read by a browser. + id: no-eval-in-markdown + recommended: true + resolved: true + severity: error + then: + function: noEvalDescription + functionOptions: + pattern: eval\( + type: validation + no-http-verbs-in-path: + category: + description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth. + id: operations + name: Operations + description: Path segments must not contain an HTTP verb + formats: + - oas3 + - oas3_1 + - oas3_2 + - oas2 + given: $ + howToFix: When HTTP verbs (get/post/put etc) are used in path segments, it muddies the semantics of REST and creates a confusing and inconsistent experience. It's highly recommended that verbs are not used in path segments. Replace those HTTP verbs with more meaningful nouns. + id: no-http-verbs-in-path + recommended: true + severity: warn + then: + function: noVerbsInPath + type: style + no-request-body: + category: + description: Operations are the core of the contract, they define paths and HTTP methods. These rules check operations have been well constructed, looks for operationId, parameter, schema and return types in depth. + id: operations + name: Operations + description: HTTP GET and DELETE should not accept request bodies + formats: + - oas3 + - oas3_1 + - oas3_2 + given: $ + howToFix: Remove 'requestBody' from HTTP GET and DELETE methods + id: no-request-body + recommended: true + severity: warn + then: + function: noRequestBody + type: style + no-script-tags-in-markdown: + category: + description: Validation rules make sure that certain characters or patterns have not been used that may cause issues when rendering in different types of applications. + id: validation + name: Validation + description: Markdown descriptions must not have `