diff --git a/Makefile b/Makefile index 2bf202b..3989d45 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ # Go parameters GOCMD=go +GOVERSION=1.25 MODULENAME=github.com/h44z/wg-portal -GOFILES:=$(shell go list ./... | grep -v /vendor/) +GOFILES=$(shell go list ./... | grep -v /vendor/) BUILDDIR=dist BINARIES=$(subst cmd/,,$(wildcard cmd/*)) IMAGE=h44z/wg-portal @@ -51,6 +52,11 @@ format: .PHONY: test test: test-vet test-race +#> test-in-docker: Run tests in Docker (for non-Linux environments e.g. MacOS) +.PHONY: test-in-docker +test-in-docker: + docker run --rm -u $(shell id -u):$(shell id -g) -e HOME=/tmp -v $(PWD):/app -w /app golang:$(GOVERSION) make test + #< test-vet: Static code analysis .PHONY: test-vet test-vet: build-dependencies diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index 327e327..de4a49f 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -135,7 +135,8 @@ func main() { apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers) apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard) apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth) - apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus) + apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus, apiV0BackendPeers) + apiV0EndpointWebsocket.StartBackgroundJobs(ctx) apiFrontend := handlersV0.NewRestApi(apiV0Session, apiV0EndpointAuth, diff --git a/config.yml.sample b/config.yml.sample index 5d4c594..7baed32 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -6,8 +6,9 @@ advanced: core: admin_user: test@test.de admin_password: secret - create_default_peer: true - create_default_peer_on_creation: false + create_default_peer_on_login: true + create_default_peer_on_user_creation: false + create_default_peer_on_interface_creation: false web: external_url: http://localhost:8888 @@ -46,7 +47,10 @@ auth: extra_scopes: - https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.profile + use_pkce: true + pkce_method: S256 registration_enabled: true + logout_idp_session: true - id: oidc2 provider_name: google2 display_name: Login with
Google2 @@ -57,6 +61,7 @@ auth: - https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.profile registration_enabled: true + logout_idp_session: true oauth: - id: google_plain_oauth provider_name: google3 @@ -76,6 +81,7 @@ auth: user_identifier: sub is_admin: this-attribute-must-be-true registration_enabled: true + use_pkce: false - id: google_plain_oauth_with_groups provider_name: google4 display_name: Login with
Google4 @@ -110,4 +116,4 @@ backend: api_verify_tls: true api_timeout: 30s concurrency: 5 - debug: false \ No newline at end of file + debug: false diff --git a/docs/documentation/configuration/examples.md b/docs/documentation/configuration/examples.md index 7dfdb33..ea4d1e0 100644 --- a/docs/documentation/configuration/examples.md +++ b/docs/documentation/configuration/examples.md @@ -8,7 +8,7 @@ core: admin_password: password admin_api_token: super-s3cr3t-api-token-or-a-UUID import_existing: false - create_default_peer: true + create_default_peer_on_login: true self_provisioning_allowed: true backend: @@ -144,6 +144,9 @@ auth: extra_scopes: - https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.profile + allowed_user_groups: + - the-admin-group + - vpn-users field_map: user_identifier: sub email: email @@ -201,6 +204,9 @@ auth: - email - profile - i-want-some-groups + allowed_user_groups: + - admin-group-name + - vpn-users field_map: email: email firstname: name diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md index 9fa1f84..717831a 100644 --- a/docs/documentation/configuration/overview.md +++ b/docs/documentation/configuration/overview.md @@ -155,17 +155,33 @@ More advanced options are found in the subsequent `Advanced` section. - **Environment Variable:** `WG_PORTAL_CORE_EDITABLE_KEYS` - **Description:** Allow editing of WireGuard key-pairs directly in the UI. -### `create_default_peer` +### `create_default_peer` (deprecated) +- **Default:** `false` +- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER` +- **Description:** **DEPRECATED** in favor of [create_default_peer_on_login](#create_default_peer_on_login). If set to `true`, this option is equivalent to enabling `create_default_peer_on_login`. It will be removed in a future release (2.4). + +### `create_default_peer_on_creation` (deprecated) +- **Default:** `false` +- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION` +- **Description:** **DEPRECATED** in favor of [create_default_peer_on_user_creation](#create_default_peer_on_user_creation) and [create_default_peer_on_interface_creation](#create_default_peer_on_interface_creation). If set to `true`, both of those options are enabled. It will be removed in a future release (2.4). + +### `create_default_peer_on_login` - **Default:** `false` - **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER` - **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set. - **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI). -### `create_default_peer_on_creation` +### `create_default_peer_on_user_creation` - **Default:** `false` -- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION` -- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set. -- **Important:** This option requires [create_default_peer](#create_default_peer) to be enabled. +- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_USER_CREATION` +- **Description:** If a new user is created (e.g., through LDAP sync or registration) and has no peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set. +- **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI). + +### `create_default_peer_on_interface_creation` +- **Default:** `false` +- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_INTERFACE_CREATION` +- **Description:** When a new server interface is created with the "Create default peer" flag set, automatically create a default WireGuard peer on that interface for every existing user who does not yet have a peer on it. +- **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI). ### `re_enable_peer_after_user_enable` - **Default:** `true` @@ -536,6 +552,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`: #### `provider_name` - **Default:** *(empty)* - **Description:** A **unique** name for this provider. Must not conflict with other providers. + This name is used to derive the callback URL for the OIDC provider: `/api/v0/auth/login//callback`. #### `display_name` - **Default:** *(empty)* @@ -561,6 +578,10 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`: - **Default:** *(empty)* - **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups. +#### `allowed_user_groups` +- **Default:** *(empty)* +- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values. + #### `field_map` - **Default:** *(empty)* - **Description:** Maps OIDC claims to WireGuard Portal user fields. @@ -582,6 +603,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`: - **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`. - `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`). - `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex. + - To identify which claim to match against, set log_level: debug and reload the config. Log in with the intended admin account and inspect the logs for the OIDC user info payload. If the required claim is missing it must be added by the OIDC provider. If it is present, use its value as the pattern for admin_group_regex. #### `registration_enabled` - **Default:** `false` @@ -596,6 +618,18 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`: - **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging). - **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues. +#### `use_pkce` +- **Default:** `true` +- **Description:** If `true`, Proof Key for Code Exchange (PKCE) is used for the OIDC authorization code flow. A fresh `code_verifier` is generated per login request, the matching `code_challenge` is sent with the authorization request, and the `code_verifier` is included in the token exchange. Set to `false` only for providers that do not support PKCE. + +#### `pkce_method` +- **Default:** `S256` +- **Description:** PKCE challenge method to use when `use_pkce` is enabled. Supported values are `S256` and `plain`. `S256` is recommended; use `plain` only for providers that explicitly require it. + +#### `logout_idp_session` +- **Default:** `true` +- **Description:** If `true` (default), WireGuard Portal will redirect the user to the OIDC provider's `end_session_endpoint` after local logout, terminating the session at the IdP as well. Set to `false` to only invalidate the local WireGuard Portal session without touching the IdP session. + --- ### OAuth @@ -606,6 +640,7 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`: #### `provider_name` - **Default:** *(empty)* - **Description:** A **unique** name for this provider. Must not conflict with other providers. + This name is used to derive the callback URL for the OAuth provider: `/api/v0/auth/login//callback`. #### `display_name` - **Default:** *(empty)* @@ -639,6 +674,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`: - **Default:** *(empty)* - **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups. +#### `allowed_user_groups` +- **Default:** *(empty)* +- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values. + #### `field_map` - **Default:** *(empty)* - **Description:** Maps OAuth attributes to WireGuard Portal fields. @@ -674,6 +713,14 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`: - **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging). - **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues. +#### `use_pkce` +- **Default:** `true` +- **Description:** If `true`, Proof Key for Code Exchange (PKCE) is used for the OIDC authorization code flow. A fresh `code_verifier` is generated per login request, the matching `code_challenge` is sent with the authorization request, and the `code_verifier` is included in the token exchange. Set to `false` only for providers that do not support PKCE. + +#### `pkce_method` +- **Default:** `S256` +- **Description:** PKCE challenge method to use when `use_pkce` is enabled. Supported values are `S256` and `plain`. `S256` is recommended; use `plain` only for providers that explicitly require it. + --- ### LDAP diff --git a/docs/documentation/getting-started/binaries.md b/docs/documentation/getting-started/binaries.md index 54eda62..843cefc 100644 --- a/docs/documentation/getting-started/binaries.md +++ b/docs/documentation/getting-started/binaries.md @@ -51,13 +51,31 @@ sudo install wg-portal /opt/wg-portal/ To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/). Refer to [Systemd Service Setup](#systemd-service-setup) for instructions. -## Systemd Service Setup +## Systemd Integration > **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand. > > The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`. > The configuration file is expected to be located at `/opt/wg-portal/config.yml`. +### Limit Systemd-Networkd Management Scope + +If you are using `systemd-networkd` to manage the rest of your network +configuration, you will need to ensure it doesn't remove routing policy +created by `wg-portal` when it restarts: + +```shell +sudo mkdir --parents /etc/systemd/networkd.conf.d/ +sudo tee --append /etc/systemd/networkd.conf.d/foreign-routing.conf </api/v0/auth/login//callback +``` + +Replace `` with the value configured in [`external_url`](../configuration/overview.md#external_url) and +`` with the exact `provider_name` from the matching OAuth2 or OIDC provider configuration. + #### Limiting Login to Specific Domains You can limit the login to specific domains by setting the `allowed_domains` property for OAuth2 or OIDC providers. @@ -66,6 +75,40 @@ auth: - "outlook.com" ``` +#### Limiting Login to Specific User Groups + +You can limit the login to specific user groups by setting the `allowed_user_groups` property for OAuth2 or OIDC providers. +If this property is not empty, the user's `user_groups` claim must contain at least one matching group. + +To use this feature, ensure your group claim is mapped via `field_map.user_groups`. + +```yaml +auth: + oidc: + - provider_name: "oidc1" + # ... other settings + allowed_user_groups: + - "wg-users" + - "wg-admins" + field_map: + user_groups: "groups" +``` + +If `allowed_user_groups` is configured and the authenticated user has no matching group in `user_groups`, login is denied. + +Minimal deny-by-group example: + +```yaml +auth: + oauth: + - provider_name: "oauth1" + # ... other settings + allowed_user_groups: + - "vpn-users" + field_map: + user_groups: "groups" +``` + #### Limit Login to Existing Users You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth2 or OIDC providers. diff --git a/docs/documentation/usage/backends.md b/docs/documentation/usage/backends.md index aeac9d6..cadc8d4 100644 --- a/docs/documentation/usage/backends.md +++ b/docs/documentation/usage/backends.md @@ -61,7 +61,7 @@ backend: > :warning: The pfSense backend is currently **alpha**. Only basic interface and peer CRUD are supported. Traffic statistics (rx/tx, last handshake) are not exposed by the pfSense REST API and will show as empty. -The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically. +The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically. wg-portal is developed for and tested against REST API v2.8.0. ### Prerequisites on pfSense: - pfSense with the REST API package enabled (`System -> API`) and WireGuard configured. diff --git a/docs/documentation/usage/security.md b/docs/documentation/usage/security.md index 08024b2..224a8b4 100644 --- a/docs/documentation/usage/security.md +++ b/docs/documentation/usage/security.md @@ -8,6 +8,23 @@ To enable encryption, set the [`encryption_passphrase`](../configuration/overvie > :warning: Important: Once encryption is enabled, it cannot be disabled, and the passphrase cannot be changed! > Only new or updated records will be encrypted; existing data remains in plaintext until it’s next modified. +## External Identity Provider Data Sanitization + +When users authenticate via LDAP, OIDC, or OAuth, WireGuard Portal sanitizes the field values received from the provider before storing them. This protects against several classes of attack that a compromised or misconfigured identity provider could introduce: + +- **Unsafe control characters** — Unicode control and format characters, null bytes, and invalid UTF-8 bytes are stripped from external profile fields before they reach the Vue.js UI or email templates. +- **Email header injection** — carriage return and line feed characters in email fields are rejected entirely, and email fields must parse as plain email addresses. +- **Log injection** — unsafe control and format characters are stripped from all external profile fields and from sanitization log context. +- **Denial of service via oversized fields** — field lengths are capped (e.g., 256 runes for identifiers, 254 characters for email addresses). +- **Reserved identifier collision** — reserved user identifiers such as `"all"`, `"new"`, `"id"`, and internal system user identifiers are rejected. +- **Unsafe authorization groups** — OIDC/OAuth group claims are sanitized before group-based checks; groups changed by control/format stripping or truncation are dropped rather than repaired into allowed/admin matches. + +Sanitization is always enabled and cannot be disabled. + +When sanitization modifies or clears a field value, a `WARN` log entry is emitted with the provider name, provider type, and field name — but never the raw or sanitized value, to avoid leaking sensitive data into logs. This makes it straightforward to detect and investigate potentially malicious or misconfigured providers. + +--- + ## UI and API Access WireGuard Portal provides a web UI and a REST API for user interaction. It is important to secure these interfaces to prevent unauthorized access and data breaches. @@ -21,4 +38,4 @@ A detailed explanation is available in the [Reverse Proxy](../getting-started/re ### Secure Authentication To prevent unauthorized access, WireGuard Portal supports integrating with secure authentication providers such as LDAP, OAuth2, or Passkeys, see [Authentication](./authentication.md) for more details. When possible, use centralized authentication and enforce multi-factor authentication (MFA) at the provider level for enhanced account security. -For local accounts, administrators should enforce strong password requirements. \ No newline at end of file +For local accounts, administrators should enforce strong password requirements. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b557f2c..dab5357 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,15 +23,15 @@ "is-ip": "^5.0.1", "pinia": "^3.0.4", "prismjs": "^1.30.0", - "vue": "^3.5.31", - "vue-i18n": "^11.3.0", + "vue": "^3.5.32", + "vue-i18n": "^11.3.2", "vue-prism-component": "github:h44z/vue-prism-component", "vue-router": "^5.0.4" }, "devDependencies": { - "@vitejs/plugin-vue": "^6.0.5", - "sass-embedded": "^1.98.0", - "vite": "^8.0.3" + "@vitejs/plugin-vue": "^6.0.6", + "sass-embedded": "^1.99.0", + "vite": "^8.0.8" } }, "node_modules/@babel/generator": { @@ -104,38 +104,35 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -159,14 +156,14 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz", - "integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.2.tgz", + "integrity": "sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==", "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.3.0", - "@intlify/message-compiler": "11.3.0", - "@intlify/shared": "11.3.0" + "@intlify/devtools-types": "11.3.2", + "@intlify/message-compiler": "11.3.2", + "@intlify/shared": "11.3.2" }, "engines": { "node": ">= 16" @@ -176,13 +173,13 @@ } }, "node_modules/@intlify/devtools-types": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz", - "integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.2.tgz", + "integrity": "sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.3.0", - "@intlify/shared": "11.3.0" + "@intlify/core-base": "11.3.2", + "@intlify/shared": "11.3.2" }, "engines": { "node": ">= 16" @@ -192,12 +189,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz", - "integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.2.tgz", + "integrity": "sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.3.0", + "@intlify/shared": "11.3.2", "source-map-js": "^1.0.2" }, "engines": { @@ -208,9 +205,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz", - "integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.2.tgz", + "integrity": "sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==", "license": "MIT", "engines": { "node": ">= 16" @@ -274,9 +271,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, @@ -293,9 +290,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -641,9 +638,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -658,9 +655,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -675,9 +672,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -692,9 +689,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -709,9 +706,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -726,9 +723,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -746,9 +743,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -766,9 +763,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -786,9 +783,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -806,9 +803,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -826,9 +823,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -846,9 +843,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -863,9 +860,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -873,16 +870,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -897,9 +896,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -914,9 +913,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", "dev": true, "license": "MIT" }, @@ -938,13 +937,13 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", - "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2" + "@rolldown/pluginutils": "1.0.0-rc.13" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -996,39 +995,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", - "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.31", + "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", - "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", - "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", - "@vue/compiler-core": "3.5.31", - "@vue/compiler-dom": "3.5.31", - "@vue/compiler-ssr": "3.5.31", - "@vue/shared": "3.5.31", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", @@ -1036,13 +1035,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", - "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/devtools-api": { @@ -1079,53 +1078,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", - "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.31" + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", - "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", - "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.31", - "@vue/runtime-core": "3.5.31", - "@vue/shared": "3.5.31", + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", - "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { - "vue": "3.5.31" + "vue": "3.5.32" } }, "node_modules/@vue/shared": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", - "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, "node_modules/acorn": { @@ -2067,14 +2066,14 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2083,27 +2082,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, @@ -2118,9 +2117,9 @@ } }, "node_modules/sass": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", - "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", "optional": true, @@ -2140,9 +2139,9 @@ } }, "node_modules/sass-embedded": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz", - "integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.99.0.tgz", + "integrity": "sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg==", "dev": true, "license": "MIT", "dependencies": { @@ -2161,30 +2160,30 @@ "node": ">=16.0.0" }, "optionalDependencies": { - "sass-embedded-all-unknown": "1.98.0", - "sass-embedded-android-arm": "1.98.0", - "sass-embedded-android-arm64": "1.98.0", - "sass-embedded-android-riscv64": "1.98.0", - "sass-embedded-android-x64": "1.98.0", - "sass-embedded-darwin-arm64": "1.98.0", - "sass-embedded-darwin-x64": "1.98.0", - "sass-embedded-linux-arm": "1.98.0", - "sass-embedded-linux-arm64": "1.98.0", - "sass-embedded-linux-musl-arm": "1.98.0", - "sass-embedded-linux-musl-arm64": "1.98.0", - "sass-embedded-linux-musl-riscv64": "1.98.0", - "sass-embedded-linux-musl-x64": "1.98.0", - "sass-embedded-linux-riscv64": "1.98.0", - "sass-embedded-linux-x64": "1.98.0", - "sass-embedded-unknown-all": "1.98.0", - "sass-embedded-win32-arm64": "1.98.0", - "sass-embedded-win32-x64": "1.98.0" + "sass-embedded-all-unknown": "1.99.0", + "sass-embedded-android-arm": "1.99.0", + "sass-embedded-android-arm64": "1.99.0", + "sass-embedded-android-riscv64": "1.99.0", + "sass-embedded-android-x64": "1.99.0", + "sass-embedded-darwin-arm64": "1.99.0", + "sass-embedded-darwin-x64": "1.99.0", + "sass-embedded-linux-arm": "1.99.0", + "sass-embedded-linux-arm64": "1.99.0", + "sass-embedded-linux-musl-arm": "1.99.0", + "sass-embedded-linux-musl-arm64": "1.99.0", + "sass-embedded-linux-musl-riscv64": "1.99.0", + "sass-embedded-linux-musl-x64": "1.99.0", + "sass-embedded-linux-riscv64": "1.99.0", + "sass-embedded-linux-x64": "1.99.0", + "sass-embedded-unknown-all": "1.99.0", + "sass-embedded-win32-arm64": "1.99.0", + "sass-embedded-win32-x64": "1.99.0" } }, "node_modules/sass-embedded-all-unknown": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.98.0.tgz", - "integrity": "sha512-6n4RyK7/1mhdfYvpP3CClS3fGoYqDvRmLClCESS6I7+SAzqjxvGG6u5Fo+cb1nrPNbbilgbM4QKdgcgWHO9NCA==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.99.0.tgz", + "integrity": "sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw==", "cpu": [ "!arm", "!arm64", @@ -2195,13 +2194,13 @@ "license": "MIT", "optional": true, "dependencies": { - "sass": "1.98.0" + "sass": "1.99.0" } }, "node_modules/sass-embedded-android-arm": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.98.0.tgz", - "integrity": "sha512-LjGiMhHgu7VL1n7EJxTCre1x14bUsWd9d3dnkS2rku003IWOI/fxc7OXgaKagoVzok1kv09rzO3vFXJR5ZeONQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.99.0.tgz", + "integrity": "sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ==", "cpu": [ "arm" ], @@ -2216,9 +2215,9 @@ } }, "node_modules/sass-embedded-android-arm64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.98.0.tgz", - "integrity": "sha512-M9Ra98A6vYJHpwhoC/5EuH1eOshQ9ZyNwC8XifUDSbRl/cGeQceT1NReR9wFj3L7s1pIbmes1vMmaY2np0uAKQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.99.0.tgz", + "integrity": "sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg==", "cpu": [ "arm64" ], @@ -2233,9 +2232,9 @@ } }, "node_modules/sass-embedded-android-riscv64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.98.0.tgz", - "integrity": "sha512-WPe+0NbaJIZE1fq/RfCZANMeIgmy83x4f+SvFOG7LhUthHpZWcOcrPTsCKKmN3xMT3iw+4DXvqTYOCYGRL3hcQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.99.0.tgz", + "integrity": "sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw==", "cpu": [ "riscv64" ], @@ -2250,9 +2249,9 @@ } }, "node_modules/sass-embedded-android-x64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.98.0.tgz", - "integrity": "sha512-zrD25dT7OHPEgLWuPEByybnIfx4rnCtfge4clBgjZdZ3lF6E7qNLRBtSBmoFflh6Vg0RlEjJo5VlpnTMBM5MQQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.99.0.tgz", + "integrity": "sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ==", "cpu": [ "x64" ], @@ -2267,9 +2266,9 @@ } }, "node_modules/sass-embedded-darwin-arm64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.98.0.tgz", - "integrity": "sha512-cgr1z9rBnCdMf8K+JabIaYd9Rag2OJi5mjq08XJfbJGMZV/TA6hFJCLGkr5/+ZOn4/geTM5/3aSfQ8z5EIJAOg==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.99.0.tgz", + "integrity": "sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg==", "cpu": [ "arm64" ], @@ -2284,9 +2283,9 @@ } }, "node_modules/sass-embedded-darwin-x64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.98.0.tgz", - "integrity": "sha512-OLBOCs/NPeiMqTdOrMFbVHBQFj19GS3bSVSxIhcCq16ZyhouUkYJEZjxQgzv9SWA2q6Ki8GCqp4k6jMeUY9dcA==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.99.0.tgz", + "integrity": "sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A==", "cpu": [ "x64" ], @@ -2301,9 +2300,9 @@ } }, "node_modules/sass-embedded-linux-arm": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.98.0.tgz", - "integrity": "sha512-03baQZCxVyEp8v1NWBRlzGYrmVT/LK7ZrHlF1piscGiGxwfdxoLXVuxsylx3qn/dD/4i/rh7Bzk7reK1br9jvQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.99.0.tgz", + "integrity": "sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw==", "cpu": [ "arm" ], @@ -2319,9 +2318,9 @@ } }, "node_modules/sass-embedded-linux-arm64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.98.0.tgz", - "integrity": "sha512-axOE3t2MTBwCtkUCbrdM++Gj0gC0fdHJPrgzQ+q1WUmY9NoNMGqflBtk5mBZaWUeha2qYO3FawxCB8lctFwCtw==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.99.0.tgz", + "integrity": "sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew==", "cpu": [ "arm64" ], @@ -2337,9 +2336,9 @@ } }, "node_modules/sass-embedded-linux-musl-arm": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.98.0.tgz", - "integrity": "sha512-OBkjTDPYR4hSaueOGIM6FDpl9nt/VZwbSRpbNu9/eEJcxE8G/vynRugW8KRZmCFjPy8j/jkGBvvS+k9iOqKV3g==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.99.0.tgz", + "integrity": "sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ==", "cpu": [ "arm" ], @@ -2355,9 +2354,9 @@ } }, "node_modules/sass-embedded-linux-musl-arm64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.98.0.tgz", - "integrity": "sha512-LeqNxQA8y4opjhe68CcFvMzCSrBuJqYVFbwElEj9bagHXQHTp9xVPJRn6VcrC+0VLEDq13HVXMv7RslIuU0zmA==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.99.0.tgz", + "integrity": "sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw==", "cpu": [ "arm64" ], @@ -2373,9 +2372,9 @@ } }, "node_modules/sass-embedded-linux-musl-riscv64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.98.0.tgz", - "integrity": "sha512-7w6hSuOHKt8FZsmjRb3iGSxEzM87fO9+M8nt5JIQYMhHTj5C+JY/vcske0v715HCVj5e1xyTnbGXf8FcASeAIw==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.99.0.tgz", + "integrity": "sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg==", "cpu": [ "riscv64" ], @@ -2391,9 +2390,9 @@ } }, "node_modules/sass-embedded-linux-musl-x64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.98.0.tgz", - "integrity": "sha512-QikNyDEJOVqPmxyCFkci8ZdCwEssdItfjQFJB+D+Uy5HFqcS5Lv3d3GxWNX/h1dSb23RPyQdQc267ok5SbEyJw==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.99.0.tgz", + "integrity": "sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew==", "cpu": [ "x64" ], @@ -2409,9 +2408,9 @@ } }, "node_modules/sass-embedded-linux-riscv64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.98.0.tgz", - "integrity": "sha512-E7fNytc/v4xFBQKzgzBddV/jretA4ULAPO6XmtBiQu4zZBdBozuSxsQLe2+XXeb0X4S2GIl72V7IPABdqke/vA==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.99.0.tgz", + "integrity": "sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA==", "cpu": [ "riscv64" ], @@ -2427,9 +2426,9 @@ } }, "node_modules/sass-embedded-linux-x64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.98.0.tgz", - "integrity": "sha512-VsvP0t/uw00mMNPv3vwyYKUrFbqzxQHnRMO+bHdAMjvLw4NFf6mscpym9Bzf+NXwi1ZNKnB6DtXjmcpcvqFqYg==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.99.0.tgz", + "integrity": "sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA==", "cpu": [ "x64" ], @@ -2445,9 +2444,9 @@ } }, "node_modules/sass-embedded-unknown-all": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.98.0.tgz", - "integrity": "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.99.0.tgz", + "integrity": "sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg==", "dev": true, "license": "MIT", "optional": true, @@ -2458,13 +2457,13 @@ "!win32" ], "dependencies": { - "sass": "1.98.0" + "sass": "1.99.0" } }, "node_modules/sass-embedded-win32-arm64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.98.0.tgz", - "integrity": "sha512-nP/10xbAiPbhQkMr3zQfXE4TuOxPzWRQe1Hgbi90jv2R4TbzbqQTuZVOaJf7KOAN4L2Bo6XCTRjK5XkVnwZuwQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.99.0.tgz", + "integrity": "sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw==", "cpu": [ "arm64" ], @@ -2479,9 +2478,9 @@ } }, "node_modules/sass-embedded-win32-x64": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.98.0.tgz", - "integrity": "sha512-/lbrVsfbcbdZQ5SJCWcV0NVPd6YRs+FtAnfedp4WbCkO/ZO7Zt/58MvI4X2BVpRY/Nt5ZBo1/7v2gYcQ+J4svQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.99.0.tgz", + "integrity": "sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg==", "cpu": [ "x64" ], @@ -2675,16 +2674,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -2702,7 +2701,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -2753,16 +2752,16 @@ } }, "node_modules/vue": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", - "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.31", - "@vue/compiler-sfc": "3.5.31", - "@vue/runtime-dom": "3.5.31", - "@vue/server-renderer": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" @@ -2774,14 +2773,14 @@ } }, "node_modules/vue-i18n": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz", - "integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.2.tgz", + "integrity": "sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.3.0", - "@intlify/devtools-types": "11.3.0", - "@intlify/shared": "11.3.0", + "@intlify/core-base": "11.3.2", + "@intlify/devtools-types": "11.3.2", + "@intlify/shared": "11.3.2", "@vue/devtools-api": "^6.5.0" }, "engines": { diff --git a/frontend/package.json b/frontend/package.json index b57459e..e0396d3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,14 +23,14 @@ "is-ip": "^5.0.1", "pinia": "^3.0.4", "prismjs": "^1.30.0", - "vue": "^3.5.31", - "vue-i18n": "^11.3.0", + "vue": "^3.5.32", + "vue-i18n": "^11.3.2", "vue-prism-component": "github:h44z/vue-prism-component", "vue-router": "^5.0.4" }, "devDependencies": { - "@vitejs/plugin-vue": "^6.0.5", - "sass-embedded": "^1.98.0", - "vite": "^8.0.3" + "@vitejs/plugin-vue": "^6.0.6", + "sass-embedded": "^1.99.0", + "vite": "^8.0.8" } } diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue index 65f5a94..a078e84 100644 --- a/frontend/src/components/InterfaceEditModal.vue +++ b/frontend/src/components/InterfaceEditModal.vue @@ -53,6 +53,7 @@ const formData = ref(freshInterface()) const isSaving = ref(false) const isDeleting = ref(false) const isApplyingDefaults = ref(false) +const isCreatingDefaultPeers = ref(false) const isBackendValid = computed(() => { if (!props.visible || !selectedInterface.value) { @@ -313,6 +314,39 @@ async function applyPeerDefaults() { } } +async function createDefaultPeers() { + if (props.interfaceId==='#NEW#') { + return; // do nothing for new interfaces + } + + if (!formData.value.CreateDefaultPeer) { + return; // only allowed if the interface flag is set + } + + if (isCreatingDefaultPeers.value) return + isCreatingDefaultPeers.value = true + try { + await interfaces.CreateDefaultPeers(selectedInterface.value.Identifier) + + notify({ + title: "Default Peers Created", + text: "Created default peers for all users on this interface.", + type: 'success', + }) + + await peers.LoadPeers(selectedInterface.value.Identifier) // reload peers list + } catch (e) { + console.log(e) + notify({ + title: "Failed to create default peers!", + text: e.toString(), + type: 'error', + }) + } finally { + isCreatingDefaultPeers.value = false + } +} + async function del() { if (isDeleting.value) return if (!confirm(t('modals.interface-edit.confirm-delete', {id: selectedInterface.value.Identifier}))) return @@ -490,9 +524,15 @@ async function del() { -
- - +
+
+ + +
+
diff --git a/frontend/src/components/Pagination.vue b/frontend/src/components/Pagination.vue new file mode 100644 index 0000000..46d9213 --- /dev/null +++ b/frontend/src/components/Pagination.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/frontend/src/lang/index.js b/frontend/src/lang/index.js index 34d5f7a..38e5564 100644 --- a/frontend/src/lang/index.js +++ b/frontend/src/lang/index.js @@ -9,6 +9,7 @@ import uk from './translations/uk.json'; import vi from './translations/vi.json'; import zh from './translations/zh.json'; import es from './translations/es.json'; +import ja from './translations/ja.json'; import {createI18n} from "vue-i18n"; @@ -33,7 +34,8 @@ const i18n = createI18n({ "uk": uk, "vi": vi, "zh": zh, - "es": es, + "es": es, + "ja": ja, } }); diff --git a/frontend/src/lang/translations/de.json b/frontend/src/lang/translations/de.json index 20c42f5..efbbf84 100644 --- a/frontend/src/lang/translations/de.json +++ b/frontend/src/lang/translations/de.json @@ -505,6 +505,7 @@ } }, "button-apply-defaults": "Peer-Standardeinstellungen anwenden", + "button-create-default-peers": "Standard-Peers erstellen", "confirm-delete": "Interface '{id}' wirklich löschen?" }, "peer-view": { diff --git a/frontend/src/lang/translations/en.json b/frontend/src/lang/translations/en.json index 2e93614..ba3a891 100644 --- a/frontend/src/lang/translations/en.json +++ b/frontend/src/lang/translations/en.json @@ -505,6 +505,7 @@ } }, "button-apply-defaults": "Apply Peer Defaults", + "button-create-default-peers": "Create Default Peers", "confirm-delete": "Are you sure you want to delete interface '{id}'?" }, "peer-view": { diff --git a/frontend/src/lang/translations/es.json b/frontend/src/lang/translations/es.json index e5974d2..eca0290 100644 --- a/frontend/src/lang/translations/es.json +++ b/frontend/src/lang/translations/es.json @@ -495,6 +495,7 @@ } }, "button-apply-defaults": "Aplicar Valores Predeterminados de peers", + "button-create-default-peers": "Crear Peers Predeterminados", "confirm-delete": "Seguro que desea eliminar la interfaz '{id}'?" }, "peer-view": { diff --git a/frontend/src/lang/translations/fr.json b/frontend/src/lang/translations/fr.json index 23f6a5b..4c4df6a 100644 --- a/frontend/src/lang/translations/fr.json +++ b/frontend/src/lang/translations/fr.json @@ -377,6 +377,7 @@ } }, "button-apply-defaults": "Appliquer les valeurs par défaut des pairs", + "button-create-default-peers": "Créer les pairs par défaut", "confirm-delete": "Voulez-vous vraiment supprimer l'interface \"{id}\" ?" }, "peer-view": { diff --git a/frontend/src/lang/translations/ja.json b/frontend/src/lang/translations/ja.json new file mode 100644 index 0000000..4fb3db7 --- /dev/null +++ b/frontend/src/lang/translations/ja.json @@ -0,0 +1,651 @@ +{ + "languages": { + "en": "日本語" + }, + "general": { + "pagination": { + "size": "件数", + "all": "全件 (低速)" + }, + "search": { + "placeholder": "検索...", + "button": "検索" + }, + "select-all": "すべて選択", + "yes": "はい", + "no": "いいえ", + "cancel": "キャンセル", + "close": "閉じる", + "save": "保存", + "delete": "削除" + }, + "login": { + "headline": "ログイン", + "username": { + "label": "ユーザー名", + "placeholder": "ユーザー名を入力してください" + }, + "password": { + "label": "パスワード", + "placeholder": "パスワードを入力してください" + }, + "button": "ログイン", + "button-webauthn": "パスキーでログイン" + }, + "menu": { + "home": "ホーム", + "interfaces": "インターフェース", + "users": "ユーザー", + "lang": "言語切替", + "profile": "マイプロフィール", + "settings": "設定", + "audit": "監査ログ", + "login": "ログイン", + "logout": "ログアウト", + "keygen": "鍵生成", + "calculator": "IP計算機" + }, + "home": { + "headline": "WireGuard® VPN ポータル", + "info-headline": "詳細情報", + "abstract": "WireGuard® は最新の暗号技術を活用した、シンプルかつ高速なモダンVPNです。IPsec より高速・シンプル・軽量・実用的な設計を目指し、OpenVPN を大きく上回る性能を発揮します。", + "installation": { + "box-header": "WireGuard インストール", + "headline": "インストール", + "content": "クライアントソフトウェアのインストール手順は WireGuard 公式サイトでご確認いただけます。", + "button": "インストール手順を開く" + }, + "about-wg": { + "box-header": "WireGuard について", + "headline": "概要", + "content": "WireGuard® は最新の暗号技術を活用したシンプルかつ高速なモダンVPNです。", + "button": "詳細" + }, + "about-portal": { + "box-header": "WireGuard Portal について", + "headline": "WireGuard Portal", + "content": "WireGuard Portal は WireGuard を Web から簡単に設定できる管理ポータルです。", + "button": "詳細" + }, + "profiles": { + "headline": "VPN プロファイル", + "abstract": "個人 VPN 設定の確認・ダウンロードはユーザープロファイルから行えます。", + "content": "設定済みプロファイルの一覧は下のボタンから開けます。", + "button": "マイプロフィールを開く" + }, + "admin": { + "headline": "管理エリア", + "abstract": "管理エリアでは、WireGuard ピア、サーバーインターフェース、および WireGuard Portal にログイン可能なユーザーを管理できます。", + "content": "", + "button-admin": "サーバー管理を開く", + "button-user": "ユーザー管理を開く" + } + }, + "interfaces": { + "headline": "インターフェース管理", + "headline-peers": "現在の VPN ピア", + "headline-endpoints": "現在のエンドポイント", + "no-interface": { + "default-selection": "利用可能なインターフェースなし", + "headline": "インターフェースが見つかりません...", + "abstract": "上の「+」ボタンから新しい WireGuard インターフェースを作成してください。" + }, + "no-peer": { + "headline": "ピアがありません", + "abstract": "選択した WireGuard インターフェースには現在ピアが登録されていません。" + }, + "table-heading": { + "name": "名前", + "user": "ユーザー", + "ip": "IP", + "endpoint": "エンドポイント", + "status": "ステータス" + }, + "interface": { + "headline": "インターフェース状態:", + "backend": "バックエンド", + "unknown-backend": "不明", + "wrong-backend": "バックエンドが無効です。代わりにローカル WireGuard バックエンドを使用します。", + "key": "公開鍵", + "endpoint": "公開エンドポイント", + "port": "待受ポート", + "peers": "有効ピア数", + "total-peers": "全ピア数", + "endpoints": "有効エンドポイント数", + "total-endpoints": "全エンドポイント数", + "ip": "IPアドレス", + "default-allowed-ip": "デフォルト許可IP", + "dns": "DNSサーバー", + "mtu": "MTU", + "default-keep-alive": "デフォルトキープアライブ間隔", + "default-dns": "デフォルトDNSサーバー", + "button-show-config": "設定を表示", + "button-download-config": "設定をダウンロード", + "button-store-config": "wg-quick 用に設定を保存", + "button-edit": "インターフェースを編集" + }, + "button-add-interface": "インターフェース追加", + "button-add-peer": "ピア追加", + "button-add-peers": "複数ピアを追加", + "button-show-peer": "ピア詳細", + "button-edit-peer": "ピア編集", + "button-bulk-delete": "選択したピアを削除", + "button-bulk-enable": "選択したピアを有効化", + "button-bulk-disable": "選択したピアを無効化", + "confirm-bulk-delete": "{count} 件のピアを削除してもよろしいですか?", + "confirm-bulk-disable": "{count} 件のピアを無効化してもよろしいですか?", + "peer-disabled": "ピアは無効化されています。理由:", + "peer-expiring": "ピアの有効期限:", + "peer-connected": "接続中", + "peer-not-connected": "未接続", + "peer-handshake": "最終ハンドシェイク:" + }, + "users": { + "headline": "ユーザー管理", + "table-heading": { + "id": "ID", + "email": "メール", + "firstname": "名", + "lastname": "姓", + "sources": "ソース", + "peers": "ピア", + "admin": "管理者" + }, + "no-user": { + "headline": "ユーザーがいません", + "abstract": "現在 WireGuard Portal に登録されているユーザーはいません。" + }, + "button-add-user": "ユーザー追加", + "button-show-user": "ユーザー詳細", + "button-edit-user": "ユーザー編集", + "button-bulk-delete": "選択したユーザーを削除", + "button-bulk-enable": "選択したユーザーを有効化", + "button-bulk-disable": "選択したユーザーを無効化", + "button-bulk-lock": "選択したユーザーをロック", + "button-bulk-unlock": "選択したユーザーのロック解除", + "confirm-bulk-delete": "{count} 件のユーザーを削除してもよろしいですか?", + "confirm-bulk-disable": "{count} 件のユーザーを無効化してもよろしいですか?", + "confirm-bulk-lock": "{count} 件のユーザーをロックしてもよろしいですか?", + "user-disabled": "ユーザーは無効化されています。理由:", + "user-locked": "アカウントはロックされています。理由:", + "admin": "管理者権限あり", + "no-admin": "管理者権限なし" + }, + "profile": { + "headline": "マイ VPN ピア", + "table-heading": { + "name": "名前", + "ip": "IP", + "stats": "ステータス", + "interface": "サーバーインターフェース" + }, + "no-peer": { + "headline": "ピアがありません", + "abstract": "現在、あなたのユーザープロフィールにはピアが関連付けられていません。" + }, + "peer-connected": "接続中", + "button-add-peer": "ピア追加", + "button-show-peer": "ピア詳細", + "button-edit-peer": "ピア編集" + }, + "settings": { + "headline": "設定", + "abstract": "ここで個人設定を変更できます。", + "api": { + "headline": "API 設定", + "abstract": "RESTful API の設定はこちらで行います。", + "active-description": "あなたのアカウントで API は現在有効です。すべての API リクエストは Basic 認証で行います。以下の認証情報を使用してください。", + "inactive-description": "API は現在無効です。下のボタンを押して有効化してください。", + "user-label": "API ユーザー名:", + "user-placeholder": "API ユーザー", + "token-label": "API パスワード:", + "token-placeholder": "API トークン", + "token-created-label": "API アクセス許可日時: ", + "button-disable-title": "API を無効化します。現在のトークンは無効になります。", + "button-disable-text": "API を無効化", + "button-enable-title": "API を有効化します。新しいトークンが生成されます。", + "button-enable-text": "API を有効化", + "api-link": "API ドキュメント" + }, + "webauthn": { + "headline": "パスキー設定", + "abstract": "パスキーはパスワード不要でユーザー認証を行う最新の方法です。ブラウザに安全に保存され、WireGuard Portal へのログインに使用できます。", + "active-description": "現在、あなたのアカウントには少なくとも 1 つのパスキーが登録されています。", + "inactive-description": "あなたのアカウントにはパスキーが登録されていません。下のボタンから新しいパスキーを登録してください。", + "table": { + "name": "名前", + "created": "作成日時", + "actions": "" + }, + "credentials-list": "登録済みパスキー", + "modal-delete": { + "headline": "パスキー削除", + "abstract": "このパスキーを削除してもよろしいですか? 削除後はこのパスキーでログインできなくなります。", + "created": "作成日時:", + "button-delete": "削除", + "button-cancel": "キャンセル" + }, + "button-rename-title": "リネーム", + "button-rename-text": "パスキーの名前を変更します。", + "button-save-title": "保存", + "button-save-text": "新しいパスキー名を保存します。", + "button-cancel-title": "キャンセル", + "button-cancel-text": "リネームをキャンセルします。", + "button-delete-title": "削除", + "button-delete-text": "パスキーを削除します。削除後はこのパスキーでログインできなくなります。", + "button-register-title": "パスキー登録", + "button-register-text": "新しいパスキーを登録してアカウントを保護します。" + }, + "password": { + "headline": "パスワード設定", + "abstract": "ここでパスワードを変更できます。", + "current-label": "現在のパスワード", + "new-label": "新しいパスワード", + "new-confirm-label": "新しいパスワード(確認)", + "change-button-text": "パスワード変更", + "invalid-confirm-label": "パスワードが一致しません", + "weak-label": "パスワードが脆弱です" + } + }, + "audit": { + "headline": "監査ログ", + "abstract": "WireGuard Portal で実行されたすべての操作の監査ログを確認できます。", + "no-entries": { + "headline": "ログがありません", + "abstract": "現在、監査ログは記録されていません。" + }, + "entries-headline": "ログエントリ", + "table-heading": { + "id": "#", + "time": "日時", + "user": "ユーザー", + "severity": "重要度", + "origin": "発生元", + "message": "メッセージ" + } + }, + "keygen": { + "headline": "WireGuard 鍵生成", + "abstract": "新しい WireGuard 鍵を生成します。鍵はローカルブラウザで生成され、サーバーには送信されません。", + "headline-keypair": "新しい鍵ペア", + "headline-preshared-key": "新しい事前共有鍵", + "button-generate": "生成", + "private-key": { + "label": "秘密鍵", + "placeholder": "秘密鍵" + }, + "public-key": { + "label": "公開鍵", + "placeholder": "公開鍵" + }, + "preshared-key": { + "label": "事前共有鍵", + "placeholder": "事前共有鍵" + } + }, + "calculator": { + "headline": "WireGuard IP 計算機", + "abstract": "WireGuard の Allowed IPs を生成します。IP サブネットはローカルブラウザで生成され、サーバーには送信されません。", + "headline-allowed-ip": "新しい Allowed IPs", + "button-exclude-private": "プライベートIP範囲を除外", + "allowed-ip": { + "label": "Allowed IPs", + "placeholder": "0.0.0.0/0, ::/0", + "empty": "値は必須です" + }, + "dissallowed-ip": { + "label": "除外IP", + "placeholder": "10.0.0.0/8, 192.168.0.0/16", + "invalid": "無効なアドレス: {addr}" + }, + "new-allowed-ip": { + "label": "Allowed IPs", + "placeholder": "" + } + }, + "modals": { + "user-view": { + "headline": "ユーザーアカウント:", + "tab-user": "情報", + "tab-peers": "ピア", + "headline-info": "ユーザー情報:", + "headline-notes": "備考:", + "email": "メール", + "firstname": "名", + "lastname": "姓", + "phone": "電話番号", + "department": "部署", + "api-enabled": "API アクセス", + "disabled": "アカウント無効", + "locked": "アカウントロック中", + "no-peers": "このユーザーには関連するピアがありません。", + "peers": { + "name": "名前", + "interface": "インターフェース", + "ip": "IP" + } + }, + "user-edit": { + "headline-edit": "ユーザー編集:", + "headline-new": "新規ユーザー", + "header-general": "全般", + "header-personal": "ユーザー情報", + "header-notes": "備考", + "header-state": "状態", + "identifier": { + "label": "識別子", + "placeholder": "一意のユーザー識別子" + }, + "source": { + "label": "ソース", + "placeholder": "ユーザーのソース" + }, + "password": { + "label": "パスワード", + "placeholder": "強固なパスワード", + "description": "現在のパスワードを保持する場合は空のままにします。", + "too-weak": "パスワードが脆弱です。より強固なパスワードを使用してください。" + }, + "email": { + "label": "メール", + "placeholder": "メールアドレス" + }, + "phone": { + "label": "電話", + "placeholder": "電話番号" + }, + "department": { + "label": "部署", + "placeholder": "部署名" + }, + "firstname": { + "label": "名", + "placeholder": "名" + }, + "lastname": { + "label": "姓", + "placeholder": "姓" + }, + "notes": { + "label": "備考", + "placeholder": "" + }, + "disabled": { + "label": "無効化 (WireGuard 接続およびログインを禁止)" + }, + "locked": { + "label": "ロック (ログイン禁止、WireGuard 接続は引き続き有効)" + }, + "admin": { + "label": "管理者" + }, + "persist-local-changes": { + "label": "ローカル変更を保持" + }, + "sync-warning": "同期されたユーザーを変更するには、ローカル変更の保持を有効化してください。そうしないと次回の同期時に変更が上書きされます。", + "confirm-delete": "ユーザー '{id}' を削除してもよろしいですか?" + }, + "interface-view": { + "headline": "インターフェース設定:" + }, + "interface-edit": { + "headline-edit": "インターフェース編集:", + "headline-new": "新規インターフェース", + "tab-interface": "インターフェース", + "tab-peerdef": "ピアのデフォルト", + "header-general": "全般", + "header-network": "ネットワーク", + "header-crypto": "暗号化", + "header-hooks": "インターフェースフック", + "header-peer-hooks": "フック", + "header-state": "状態", + "identifier": { + "label": "識別子", + "placeholder": "一意のインターフェース識別子" + }, + "mode": { + "label": "インターフェースモード", + "server": "サーバーモード", + "client": "クライアントモード", + "any": "不明モード" + }, + "backend": { + "label": "インターフェースバックエンド", + "invalid-label": "元のバックエンドが利用できなくなったため、ローカル WireGuard バックエンドを使用します。", + "local": "ローカル WireGuard バックエンド" + }, + "display-name": { + "label": "表示名", + "placeholder": "インターフェースの説明的な名前" + }, + "private-key": { + "label": "秘密鍵", + "placeholder": "秘密鍵" + }, + "public-key": { + "label": "公開鍵", + "placeholder": "公開鍵" + }, + "ip": { + "label": "IPアドレス", + "placeholder": "IPアドレス (CIDR 形式)" + }, + "listen-port": { + "label": "待受ポート", + "placeholder": "待ち受けポート" + }, + "dns": { + "label": "DNSサーバー", + "placeholder": "使用するDNSサーバー" + }, + "dns-search": { + "label": "DNS 検索ドメイン", + "placeholder": "DNS 検索プレフィックス" + }, + "mtu": { + "label": "MTU", + "placeholder": "インターフェース MTU (0 = デフォルト)" + }, + "firewall-mark": { + "label": "ファイアウォールマーク", + "placeholder": "送信トラフィックに付与するファイアウォールマーク (0 = 自動)" + }, + "routing-table": { + "label": "ルーティングテーブル", + "placeholder": "ルーティングテーブルID", + "description": "特殊値: off = 経路を管理しない、0 = 自動" + }, + "pre-up": { + "label": "Pre-Up", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "post-up": { + "label": "Post-Up", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "pre-down": { + "label": "Pre-Down", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "post-down": { + "label": "Post-Down", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "disabled": { + "label": "インターフェース無効化" + }, + "create-default-peer": { + "label": "新規ユーザー用にデフォルトピアを作成" + }, + "save-config": { + "label": "wg-quick 設定を自動保存" + }, + "defaults": { + "endpoint": { + "label": "エンドポイントアドレス", + "placeholder": "エンドポイントアドレス", + "description": "ピアが接続するエンドポイントアドレス。(例: wg.example.com または wg.example.com:51820)" + }, + "networks": { + "label": "IP ネットワーク", + "placeholder": "ネットワークアドレス", + "description": "ピアにはこれらのサブネットから IP が割り当てられます。" + }, + "allowed-ip": { + "label": "Allowed IPs", + "placeholder": "デフォルトの Allowed IPs" + }, + "mtu": { + "label": "MTU", + "placeholder": "クライアント MTU (0 = デフォルト)" + }, + "keep-alive": { + "label": "キープアライブ間隔", + "placeholder": "Persistent Keepalive (0 = デフォルト)" + } + }, + "button-apply-defaults": "ピアのデフォルトを適用", + "button-create-default-peers": "デフォルトピアを作成", + "confirm-delete": "インターフェース '{id}' を削除してもよろしいですか?" + }, + "peer-view": { + "headline-peer": "ピア:", + "headline-endpoint": "エンドポイント:", + "section-info": "ピア情報", + "section-status": "現在の状態", + "section-config": "設定", + "identifier": "識別子", + "ip": "IPアドレス", + "allowed-ip": "Allowed IPs", + "extra-allowed-ip": "サーバー側 Allowed IPs", + "user": "関連ユーザー", + "notes": "備考", + "expiry-status": "有効期限", + "disabled-status": "無効化日時", + "traffic": "通信量", + "connection-status": "接続統計", + "upload": "アップロード(サーバー → ピア、バイト)", + "download": "ダウンロード(ピア → サーバー、バイト)", + "pingable": "ping 応答", + "handshake": "最終ハンドシェイク", + "connected-since": "接続開始日時", + "endpoint": "エンドポイント", + "endpoint-key": "エンドポイント公開鍵", + "keepalive": "Persistent Keepalive", + "button-download": "設定をダウンロード", + "button-email": "設定をメール送信", + "style-label": "設定スタイル" + }, + "peer-edit": { + "headline-edit-peer": "ピア編集:", + "headline-edit-endpoint": "エンドポイント編集:", + "headline-new-peer": "ピア作成", + "headline-new-endpoint": "エンドポイント作成", + "header-general": "全般", + "header-network": "ネットワーク", + "header-crypto": "暗号化", + "header-hooks": "フック (ピア側で実行)", + "header-state": "状態", + "display-name": { + "label": "表示名", + "placeholder": "ピアの説明的な名前" + }, + "linked-user": { + "label": "関連ユーザー", + "placeholder": "このピアを所有するユーザーアカウント" + }, + "private-key": { + "label": "秘密鍵", + "placeholder": "秘密鍵", + "help": "秘密鍵はサーバー上に安全に保存されます。すでにユーザーがコピーを持っている場合は空欄でも構いません。サーバーはピアの公開鍵のみで動作します。" + }, + "public-key": { + "label": "公開鍵", + "placeholder": "公開鍵" + }, + "preshared-key": { + "label": "事前共有鍵", + "placeholder": "オプションの事前共有鍵" + }, + "endpoint-public-key": { + "label": "エンドポイント公開鍵", + "placeholder": "リモートエンドポイントの公開鍵" + }, + "endpoint": { + "label": "エンドポイントアドレス", + "placeholder": "リモートエンドポイントのアドレス" + }, + "ip": { + "label": "IPアドレス", + "placeholder": "IPアドレス (CIDR 形式)" + }, + "allowed-ip": { + "label": "Allowed IPs", + "placeholder": "Allowed IPs (CIDR 形式)" + }, + "extra-allowed-ip": { + "label": "追加の Allowed IPs", + "placeholder": "追加 Allowed IPs (サーバー側)", + "description": "これらの IP はリモート WireGuard インターフェースの Allowed IPs に追加されます。" + }, + "dns": { + "label": "DNSサーバー", + "placeholder": "使用するDNSサーバー" + }, + "dns-search": { + "label": "DNS 検索ドメイン", + "placeholder": "DNS 検索プレフィックス" + }, + "keep-alive": { + "label": "キープアライブ間隔", + "placeholder": "Persistent Keepalive (0 = デフォルト)" + }, + "mtu": { + "label": "MTU", + "placeholder": "クライアント MTU (0 = デフォルト)" + }, + "pre-up": { + "label": "Pre-Up", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "post-up": { + "label": "Post-Up", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "pre-down": { + "label": "Pre-Down", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "post-down": { + "label": "Post-Down", + "placeholder": "; で区切られた1つ以上の bash コマンド" + }, + "disabled": { + "label": "ピア無効化" + }, + "ignore-global": { + "label": "グローバル設定を無視" + }, + "expires-at": { + "label": "有効期限" + }, + "confirm-delete": "ピア '{id}' を削除してもよろしいですか?" + }, + "peer-multi-create": { + "headline-peer": "複数ピア作成", + "headline-endpoint": "複数エンドポイント作成", + "identifiers": { + "label": "ユーザー識別子", + "placeholder": "ユーザー識別子", + "description": "ピアを作成するユーザー識別子(ユーザー名)。" + }, + "prefix": { + "headline-peer": "ピア:", + "headline-endpoint": "エンドポイント:", + "label": "表示名プレフィックス", + "placeholder": "プレフィックス", + "description": "ピア表示名に追加されるプレフィックス。" + } + } + } +} diff --git a/frontend/src/lang/translations/ko.json b/frontend/src/lang/translations/ko.json index 8f9bad1..979f5d0 100644 --- a/frontend/src/lang/translations/ko.json +++ b/frontend/src/lang/translations/ko.json @@ -395,6 +395,7 @@ } }, "button-apply-defaults": "피어 기본값 적용", + "button-create-default-peers": "기본 피어 생성", "confirm-delete": "인터페이스 '{id}'를 삭제하시겠습니까?" }, "peer-view": { diff --git a/frontend/src/lang/translations/pt.json b/frontend/src/lang/translations/pt.json index c5a86b5..3e1ea96 100644 --- a/frontend/src/lang/translations/pt.json +++ b/frontend/src/lang/translations/pt.json @@ -415,6 +415,7 @@ } }, "button-apply-defaults": "Aplicar Padrões de Peer", + "button-create-default-peers": "Criar Peers Padrão", "confirm-delete": "Tem certeza que deseja excluir a interface '{id}'?" }, "peer-view": { diff --git a/frontend/src/lang/translations/ru.json b/frontend/src/lang/translations/ru.json index eeaa254..068ed48 100644 --- a/frontend/src/lang/translations/ru.json +++ b/frontend/src/lang/translations/ru.json @@ -486,6 +486,7 @@ } }, "button-apply-defaults": "Применить настройки пира по умолчанию", + "button-create-default-peers": "Создать пиров по умолчанию", "confirm-delete": "Вы уверены, что хотите удалить интерфейс «{id}»?" }, "peer-view": { diff --git a/frontend/src/lang/translations/uk.json b/frontend/src/lang/translations/uk.json index 1f0982a..e87d302 100644 --- a/frontend/src/lang/translations/uk.json +++ b/frontend/src/lang/translations/uk.json @@ -377,6 +377,7 @@ } }, "button-apply-defaults": "Застосувати значення за замовчуванням для пірів", + "button-create-default-peers": "Створити пірів за замовчуванням", "confirm-delete": "Ви впевнені, що хочете видалити інтерфейс «{id}»?" }, "peer-view": { diff --git a/frontend/src/lang/translations/vi.json b/frontend/src/lang/translations/vi.json index 27a015f..a283766 100644 --- a/frontend/src/lang/translations/vi.json +++ b/frontend/src/lang/translations/vi.json @@ -355,6 +355,7 @@ } }, "button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer", + "button-create-default-peers": "Tạo Peer Mặc định", "confirm-delete": "Ban co chac muon xoa giao dien '{id}' khong?" }, "peer-view": { diff --git a/frontend/src/lang/translations/zh.json b/frontend/src/lang/translations/zh.json index f1993fb..a76c2ba 100644 --- a/frontend/src/lang/translations/zh.json +++ b/frontend/src/lang/translations/zh.json @@ -355,6 +355,7 @@ } }, "button-apply-defaults": "应用节点默认值", + "button-create-default-peers": "创建默认节点", "confirm-delete": "确定要删除接口“{id}”吗?" }, "peer-view": { diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 07cb5c5..6b5728b 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -6,8 +6,10 @@ import {authStore} from '@/stores/auth' import {securityStore} from '@/stores/security' import {notify} from "@kyvg/vue3-notification"; +const routerBase = `${WGPORTAL_BASE_PATH || ''}${import.meta.env.BASE_URL || '/'}` + const router = createRouter({ - history: createWebHashHistory(), + history: createWebHashHistory(routerBase), routes: [ { path: '/', diff --git a/frontend/src/stores/audit.js b/frontend/src/stores/audit.js index 771df90..7f80762 100644 --- a/frontend/src/stores/audit.js +++ b/frontend/src/stores/audit.js @@ -11,7 +11,6 @@ export const auditStore = defineStore('audit', { filter: "", pageSize: 10, pageOffset: 0, - pages: [], fetching: false, }), getters: { @@ -41,33 +40,22 @@ export const auditStore = defineStore('audit', { afterPageSizeChange() { // reset pageOffset to avoid problems with new page sizes this.pageOffset = 0 - this.calculatePages() - }, - calculatePages() { - let pageCounter = 1; - this.pages = [] - for (let i = 0; i < this.FilteredCount; i+=this.pageSize) { - this.pages.push(pageCounter++) - } }, gotoPage(page) { this.pageOffset = (page-1) * this.pageSize - - this.calculatePages() }, nextPage() { - this.pageOffset += this.pageSize - - this.calculatePages() + if (this.hasNextPage) { + this.pageOffset += this.pageSize + } }, previousPage() { - this.pageOffset -= this.pageSize - - this.calculatePages() + if (this.hasPrevPage) { + this.pageOffset -= this.pageSize + } }, setEntries(entries) { this.entries = entries - this.calculatePages() this.fetching = false }, async LoadEntries() { diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 83d97d2..b72538f 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -108,12 +108,19 @@ export const authStore = defineStore('auth',{ this.setUserInfo(null) this.ResetReturnUrl() // just to be sure^^ + let logoutResponse = null try { - await apiWrapper.post(`/auth/logout`) + logoutResponse = await apiWrapper.post(`/auth/logout`) } catch (e) { console.log("Logout request failed:", e) } + const redirectUrl = logoutResponse?.RedirectUrl + if (redirectUrl) { + window.location.href = redirectUrl + return + } + notify({ title: "Logged Out", text: "Logout successful!", diff --git a/frontend/src/stores/interfaces.js b/frontend/src/stores/interfaces.js index efe75c2..c2e5a93 100644 --- a/frontend/src/stores/interfaces.js +++ b/frontend/src/stores/interfaces.js @@ -148,6 +148,18 @@ export const interfaceStore = defineStore('interfaces', { throw new Error(error) }) }, + async CreateDefaultPeers(id) { + this.fetching = true + return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/create-default-peers`) + .then(() => { + this.fetching = false + }) + .catch(error => { + this.fetching = false + console.log(error) + throw new Error(error) + }) + }, async SaveConfiguration(id) { this.fetching = true return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`) diff --git a/frontend/src/stores/peers.js b/frontend/src/stores/peers.js index 8e80b2c..f464cdd 100644 --- a/frontend/src/stores/peers.js +++ b/frontend/src/stores/peers.js @@ -19,7 +19,6 @@ export const peerStore = defineStore('peers', { filter: "", pageSize: 10, pageOffset: 0, - pages: [], fetching: false, sortKey: 'IsConnected', // Default sort key sortOrder: -1, // 1 for ascending, -1 for descending @@ -87,33 +86,22 @@ export const peerStore = defineStore('peers', { afterPageSizeChange() { // reset pageOffset to avoid problems with new page sizes this.pageOffset = 0 - this.calculatePages() - }, - calculatePages() { - let pageCounter = 1; - this.pages = [] - for (let i = 0; i < this.FilteredCount; i+=this.pageSize) { - this.pages.push(pageCounter++) - } }, gotoPage(page) { this.pageOffset = (page-1) * this.pageSize - - this.calculatePages() }, nextPage() { - this.pageOffset += this.pageSize - - this.calculatePages() + if (this.hasNextPage) { + this.pageOffset += this.pageSize + } }, previousPage() { - this.pageOffset -= this.pageSize - - this.calculatePages() + if (this.hasPrevPage) { + this.pageOffset -= this.pageSize + } }, setPeers(peers) { this.peers = peers - this.calculatePages() this.fetching = false this.trafficStats = {} }, diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js index 632e931..268e4db 100644 --- a/frontend/src/stores/profile.js +++ b/frontend/src/stores/profile.js @@ -20,7 +20,6 @@ export const profileStore = defineStore('profile', { filter: "", pageSize: 10, pageOffset: 0, - pages: [], fetching: false, sortKey: 'IsConnected', // Default sort key sortOrder: -1, // 1 for ascending, -1 for descending @@ -80,29 +79,19 @@ export const profileStore = defineStore('profile', { afterPageSizeChange() { // reset pageOffset to avoid problems with new page sizes this.pageOffset = 0 - this.calculatePages() - }, - calculatePages() { - let pageCounter = 1; - this.pages = [] - for (let i = 0; i < this.FilteredPeerCount; i+=this.pageSize) { - this.pages.push(pageCounter++) - } }, gotoPage(page) { this.pageOffset = (page-1) * this.pageSize - - this.calculatePages() }, nextPage() { - this.pageOffset += this.pageSize - - this.calculatePages() + if (this.hasNextPage) { + this.pageOffset += this.pageSize + } }, previousPage() { - this.pageOffset -= this.pageSize - - this.calculatePages() + if (this.hasPrevPage) { + this.pageOffset -= this.pageSize + } }, setPeers(peers) { this.peers = peers diff --git a/frontend/src/stores/users.js b/frontend/src/stores/users.js index 8eb194a..19816ca 100644 --- a/frontend/src/stores/users.js +++ b/frontend/src/stores/users.js @@ -12,7 +12,6 @@ export const userStore = defineStore('users', { filter: "", pageSize: 10, pageOffset: 0, - pages: [], fetching: false, }), getters: { @@ -43,33 +42,22 @@ export const userStore = defineStore('users', { afterPageSizeChange() { // reset pageOffset to avoid problems with new page sizes this.pageOffset = 0 - this.calculatePages() - }, - calculatePages() { - let pageCounter = 1; - this.pages = [] - for (let i = 0; i < this.FilteredCount; i+=this.pageSize) { - this.pages.push(pageCounter++) - } }, gotoPage(page) { this.pageOffset = (page-1) * this.pageSize - - this.calculatePages() }, nextPage() { - this.pageOffset += this.pageSize - - this.calculatePages() + if (this.hasNextPage) { + this.pageOffset += this.pageSize + } }, previousPage() { - this.pageOffset -= this.pageSize - - this.calculatePages() + if (this.hasPrevPage) { + this.pageOffset -= this.pageSize + } }, setUsers(users) { this.users = users - this.calculatePages() this.fetching = false }, setUserPeers(peers) { diff --git a/frontend/src/views/AuditView.vue b/frontend/src/views/AuditView.vue index ccd78f7..7a47d26 100644 --- a/frontend/src/views/AuditView.vue +++ b/frontend/src/views/AuditView.vue @@ -1,6 +1,7 @@ @@ -185,36 +185,33 @@ onMounted(async () => {
-
- +
+
-
+
-
-
diff --git a/frontend/src/views/UserView.vue b/frontend/src/views/UserView.vue index 42e452b..777a999 100644 --- a/frontend/src/views/UserView.vue +++ b/frontend/src/views/UserView.vue @@ -1,8 +1,9 @@ " + want := "" + got := SanitizeString(input, 256) + if got != want { + t.Errorf("SanitizeString(%q, 256) = %q; want %q", input, got, want) + } +} + +// --------------------------------------------------------------------------- +// Property 1: SanitizeString output invariants +// --------------------------------------------------------------------------- + +// Feature: external-identity-sanitization, Property 1: SanitizeString output is free of control characters and bounded in length +func TestPropertySanitizeStringOutputInvariants(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + s := rapid.String().Draw(t, "s") + maxLen := rapid.IntRange(0, 512).Draw(t, "maxLen") + result := SanitizeString(s, maxLen) + + // No control or format runes in result + for _, r := range result { + if unicode.IsControl(r) || unicode.Is(unicode.Cf, r) { + t.Fatalf("result %q contains unsafe character %U", result, r) + } + } + + if !utf8.ValidString(result) { + t.Fatalf("result %q is not valid UTF-8", result) + } + + // No leading or trailing whitespace + if result != strings.TrimSpace(result) { + t.Fatalf("result %q has leading or trailing whitespace", result) + } + + // Rune count <= maxLen + runeCount := utf8.RuneCountInString(result) + if runeCount > maxLen { + t.Fatalf("result %q has %d runes, exceeds maxLen %d", result, runeCount, maxLen) + } + }) +} + +// --------------------------------------------------------------------------- +// Property 2: SanitizeString is idempotent +// --------------------------------------------------------------------------- + +// Feature: external-identity-sanitization, Property 2: SanitizeString is idempotent +func TestPropertySanitizeStringIdempotent(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + s := rapid.String().Draw(t, "s") + maxLen := rapid.IntRange(1, 512).Draw(t, "maxLen") + + once := SanitizeString(s, maxLen) + twice := SanitizeString(once, maxLen) + + if once != twice { + t.Fatalf("SanitizeString is not idempotent: once=%q, twice=%q (input=%q, maxLen=%d)", + once, twice, s, maxLen) + } + }) +} + +// --------------------------------------------------------------------------- +// Property 3: SanitizeEmail rejection rules +// --------------------------------------------------------------------------- + +// Feature: external-identity-sanitization, Property 3: SanitizeEmail rejects strings without "@" or containing CR/LF +func TestPropertySanitizeEmailRejectionRules(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + s := rapid.String().Draw(t, "s") + maxLen := rapid.IntRange(1, 512).Draw(t, "maxLen") + result := SanitizeEmail(s, maxLen) + + sanitized := SanitizeString(s, maxLen) + addr, parseErr := mail.ParseAddress(sanitized) + reject := strings.ContainsAny(s, "\r\n") || + sanitized == "" || + strings.Count(sanitized, "@") != 1 || + parseErr != nil || + addr.Name != "" || + addr.Address != sanitized + if reject { + if result != "" { + t.Fatalf("SanitizeEmail(%q, %d) = %q; expected empty string (contains CR/LF or no @)", + s, maxLen, result) + } + } + }) +} + +// --------------------------------------------------------------------------- +// Property 4: SanitizePhone allowed character set +// --------------------------------------------------------------------------- + +// isAllowedPhoneCharTest mirrors the internal isAllowedPhoneRune logic for test assertions. +func isAllowedPhoneCharTest(r rune) bool { + switch { + case r >= '0' && r <= '9': + return true + case r == '+', r == '-', r == '(', r == ')', r == ' ', r == '.': + return true + default: + return false + } +} + +// Feature: external-identity-sanitization, Property 4: SanitizePhone output contains only allowed characters +func TestPropertySanitizePhoneAllowedChars(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + s := rapid.String().Draw(t, "s") + maxLen := rapid.IntRange(1, 512).Draw(t, "maxLen") + result := SanitizePhone(s, maxLen) + + for _, r := range result { + if !isAllowedPhoneCharTest(r) { + t.Fatalf("SanitizePhone(%q, %d) = %q; contains disallowed rune %U (%c)", + s, maxLen, result, r, r) + } + } + }) +} + +// --------------------------------------------------------------------------- +// Property 5: SanitizeIdentifier rejects reserved identifiers +// --------------------------------------------------------------------------- + +// Feature: external-identity-sanitization, Property 5: SanitizeIdentifier rejects reserved values +func TestPropertySanitizeIdentifierRejectsReservedValues(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + s := rapid.String().Draw(t, "s") + maxLen := rapid.IntRange(1, 512).Draw(t, "maxLen") + result := SanitizeIdentifier(s, maxLen) + sanitized := SanitizeString(s, maxLen) + + _, reserved := reservedUserIdentifiers[sanitized] + if reserved { + if result != "" { + t.Fatalf("SanitizeIdentifier(%q, %d) = %q; expected empty string when sanitized is reserved", + s, maxLen, result) + } + } else { + if result != sanitized { + t.Fatalf("SanitizeIdentifier(%q, %d) = %q; expected %q (== SanitizeString result)", + s, maxLen, result, sanitized) + } + } + }) +} diff --git a/internal/domain/user.go b/internal/domain/user.go index 872e52b..4eed5f2 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -270,6 +270,44 @@ func (u *User) DisplayName() string { return displayName } +// CreateDefaultPeers determines whether default peers should be created for this user. +func (u *User) CreateDefaultPeers() bool { + if u.IsDisabled() { + return false + } + if u.IsLocked() { + return false + } + + return true +} + +// SanitizeExternalData sanitizes user profile fields received from an external identity provider. +// Returns ErrInvalidData if the identifier becomes empty after sanitization. +func (u *User) SanitizeExternalData(providerType, providerName string) error { + identifier := string(u.Identifier) + LogSanitizeChange(providerType, providerName, "identifier", identifier, + func() string { return SanitizeIdentifier(identifier, 256) }, &identifier) + u.Identifier = UserIdentifier(identifier) + + LogSanitizeChange(providerType, providerName, "email", u.Email, + func() string { return SanitizeEmail(u.Email, 254) }, &u.Email) + LogSanitizeChange(providerType, providerName, "firstname", u.Firstname, + func() string { return SanitizeString(u.Firstname, 128) }, &u.Firstname) + LogSanitizeChange(providerType, providerName, "lastname", u.Lastname, + func() string { return SanitizeString(u.Lastname, 128) }, &u.Lastname) + LogSanitizeChange(providerType, providerName, "phone", u.Phone, + func() string { return SanitizePhone(u.Phone, 50) }, &u.Phone) + LogSanitizeChange(providerType, providerName, "department", u.Department, + func() string { return SanitizeString(u.Department, 128) }, &u.Department) + + if u.Identifier == "" { + return fmt.Errorf("empty user identifier: %w", ErrInvalidData) + } + + return nil +} + // region webauthn func (u *User) WebAuthnID() []byte { diff --git a/internal/domain/user_sanitize_test.go b/internal/domain/user_sanitize_test.go new file mode 100644 index 0000000..0d5f8a5 --- /dev/null +++ b/internal/domain/user_sanitize_test.go @@ -0,0 +1,64 @@ +package domain + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/h44z/wg-portal/internal/testutil" +) + +func TestUser_SanitizeExternalData_NullByteInFirstname(t *testing.T) { + u := &User{ + Identifier: "alice", + Email: "alice@example.com", + Firstname: "Ali\x00ce", + Lastname: "Smith", + } + + restore := testutil.CaptureWarnLogs(t) + err := u.SanitizeExternalData("ldap", "test-provider") + records := restore() + + require.NoError(t, err) + assert.Equal(t, "Alice", u.Firstname) + assert.Equal(t, 1, testutil.CountWarnEntries(records)) + + _, found := testutil.FindWarnWithField(records, "firstname") + assert.True(t, found) +} + +func TestUser_SanitizeExternalData_IdentifierAll(t *testing.T) { + u := &User{ + Identifier: "all", + Email: "all@example.com", + Firstname: "Alice", + Lastname: "Smith", + } + + err := u.SanitizeExternalData("ldap", "test-provider") + + require.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidData)) +} + +func TestUser_SanitizeExternalData_AllFieldsClean(t *testing.T) { + u := &User{ + Identifier: "alice", + Email: "alice@example.com", + Firstname: "Alice", + Lastname: "Smith", + Phone: "+1 555-1234", + Department: "Engineering", + } + + restore := testutil.CaptureWarnLogs(t) + err := u.SanitizeExternalData("ldap", "test-provider") + records := restore() + + require.NoError(t, err) + assert.Equal(t, UserIdentifier("alice"), u.Identifier) + assert.Equal(t, 0, testutil.CountWarnEntries(records)) +} diff --git a/internal/domain/user_test.go b/internal/domain/user_test.go index 515a41c..7100c2d 100644 --- a/internal/domain/user_test.go +++ b/internal/domain/user_test.go @@ -145,3 +145,17 @@ func TestUser_HashPassword(t *testing.T) { user.Password = "" assert.NoError(t, user.HashPassword()) } + +func TestUser_CreateDefaultPeers(t *testing.T) { + user := &User{} + assert.True(t, user.CreateDefaultPeers()) + + user2 := &User{Disabled: &time.Time{}} + assert.False(t, user2.CreateDefaultPeers()) + + user3 := &User{Locked: &time.Time{}} + assert.False(t, user3.CreateDefaultPeers()) + + user4 := &User{Disabled: &time.Time{}, Locked: &time.Time{}} + assert.False(t, user4.CreateDefaultPeers()) +} diff --git a/internal/lowlevel/mikrotik.go b/internal/lowlevel/mikrotik.go index 0c86a13..327dec4 100644 --- a/internal/lowlevel/mikrotik.go +++ b/internal/lowlevel/mikrotik.go @@ -57,6 +57,9 @@ type EmptyResponse struct{} func (JsonObject GenericJsonObject) GetString(key string) string { if value, ok := JsonObject[key]; ok { + if value == nil { + return "" + } if strValue, ok := value.(string); ok { return strValue } else { diff --git a/internal/lowlevel/pfsense.go b/internal/lowlevel/pfsense.go index e58471a..4c65ff5 100644 --- a/internal/lowlevel/pfsense.go +++ b/internal/lowlevel/pfsense.go @@ -23,7 +23,7 @@ import ( // region models const ( - PfsenseApiStatusOk = "ok" // pfSense REST API uses "ok" in response + PfsenseApiStatusOk = "ok" // pfSense REST API uses "ok" in response PfsenseApiStatusError = "error" ) @@ -37,8 +37,8 @@ const ( type PfsenseApiResponse[T any] struct { Status string Code int - Data T `json:"data,omitempty"` - Error *PfsenseApiError `json:"error,omitempty"` + Data T `json:"data,omitempty"` + Error *PfsenseApiError `json:"error,omitempty"` } type PfsenseApiError struct { @@ -193,6 +193,7 @@ func (p *PfsenseApiClient) preparePayloadRequest( if err != nil { return nil, fmt.Errorf("failed to marshal payload: %w", err) } + p.debugLog("Prepared payload", "payload", string(payloadBytes)) req, err := http.NewRequestWithContext(ctx, method, fullUrl, bytes.NewReader(payloadBytes)) if err != nil { @@ -243,7 +244,7 @@ func parsePfsenseHttpResponse[T any](resp *http.Response, err error) PfsenseApiR if err != nil { return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, "failed to read response body", err) } - + // Close the body after reading defer func() { if err := resp.Body.Close(); err != nil { @@ -273,7 +274,7 @@ func parsePfsenseHttpResponse[T any](resp *http.Response, err error) PfsenseApiR "method", resp.Request.Method, "body_preview", bodyPreview, "error", err) - return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, + return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, fmt.Sprintf("failed to decode response (status %d, content-type: %s): %v", resp.StatusCode, contentType, err), err) } @@ -405,11 +406,12 @@ func (p *PfsenseApiClient) Update( func (p *PfsenseApiClient) Delete( ctx context.Context, command string, + opts *PfsenseRequestOptions, ) PfsenseApiResponse[EmptyResponse] { apiCtx, cancel := context.WithTimeout(ctx, p.cfg.GetApiTimeout()) defer cancel() - fullUrl := p.getFullPath(command) + fullUrl := opts.GetPath(p.getFullPath(command)) req, err := p.prepareDeleteRequest(apiCtx, fullUrl) if err != nil { @@ -425,4 +427,3 @@ func (p *PfsenseApiClient) Delete( } // endregion API-client - diff --git a/internal/sanitize/log.go b/internal/sanitize/log.go new file mode 100644 index 0000000..51e4d9b --- /dev/null +++ b/internal/sanitize/log.go @@ -0,0 +1,32 @@ +package sanitize + +import ( + "log/slog" + + "github.com/h44z/wg-portal/internal/domain" +) + +// LogChange applies sanitizeFn to raw, logs when the value changes, and writes +// the sanitized value to dest. Raw and sanitized values are intentionally omitted. +func LogChange( + providerType string, + providerName string, + field string, + raw string, + sanitizeFn func() string, + dest *string, +) { + sanitized := sanitizeFn() + if sanitized != raw { + message := "sanitization modified field value from external provider" + if sanitized == "" { + message = "sanitization cleared field value from external provider" + } + slog.Warn(message, + "provider_type", domain.SanitizeString(providerType, 64), + "provider", domain.SanitizeString(providerName, 128), + "field", domain.SanitizeString(field, 64), + ) + } + *dest = sanitized +} diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go new file mode 100644 index 0000000..c34c37a --- /dev/null +++ b/internal/testutil/testutil.go @@ -0,0 +1,50 @@ +package testutil + +import ( + "bytes" + "encoding/json" + "log/slog" + "testing" +) + +func CaptureWarnLogs(t *testing.T) (restore func() []map[string]any) { + t.Helper() + original := slog.Default() + var buf bytes.Buffer + handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}) + slog.SetDefault(slog.New(handler)) + + return func() []map[string]any { + slog.SetDefault(original) + var records []map[string]any + decoder := json.NewDecoder(&buf) + for decoder.More() { + var rec map[string]any + if err := decoder.Decode(&rec); err == nil { + records = append(records, rec) + } + } + return records + } +} + +func CountWarnEntries(records []map[string]any) int { + count := 0 + for _, r := range records { + if lvl, ok := r["level"].(string); ok && lvl == "WARN" { + count++ + } + } + return count +} + +func FindWarnWithField(records []map[string]any, fieldName string) (map[string]any, bool) { + for _, r := range records { + if lvl, ok := r["level"].(string); ok && lvl == "WARN" { + if f, ok := r["field"].(string); ok && f == fieldName { + return r, true + } + } + } + return nil, false +}