Compare commits
44
Commits
9581e6eacb
...
e7c91c34f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7c91c34f2 | ||
|
|
94a9967dd1 | ||
|
|
f731135564 | ||
|
|
3dfea018c3 | ||
|
|
2adadfb582 | ||
|
|
d4a088b951 | ||
|
|
1bc0529e63 | ||
|
|
5fed90e0f8 | ||
|
|
de008e5542 | ||
|
|
c50e99cd90 | ||
|
|
0b97cf6d23 | ||
|
|
52fb584bb0 | ||
|
|
8ae33866fc | ||
|
|
78b2838498 | ||
|
|
915e559514 | ||
|
|
ee232569f0 | ||
|
|
11071c2ad5 | ||
|
|
ec301d17d7 | ||
|
|
977a18801b | ||
|
|
391bed2256 | ||
|
|
042dce50b5 | ||
|
|
e158d073c2 | ||
|
|
c7b665b61c | ||
|
|
2af5cd04b4 | ||
|
|
e7ea65a898 | ||
|
|
032347648d | ||
|
|
5f54fa3e58 | ||
|
|
30498520d2 | ||
|
|
47c6f476ac | ||
|
|
09cf208933 | ||
|
|
e9252be45c | ||
|
|
66b292b11b | ||
|
|
bb5aff1919 | ||
|
|
8add67a4cc | ||
|
|
ac547bbf7c | ||
|
|
8c70c24205 | ||
|
|
c70ad1d08b | ||
|
|
d0566a1df9 | ||
|
|
bc95a2851f | ||
|
|
e03d743307 | ||
|
|
99357848e5 | ||
|
|
c41ae0d4c5 | ||
|
|
66f8bde206 | ||
|
|
b3afb9ac1b |
@@ -1,28 +1,51 @@
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
<!-- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
<!-- Describe your changes in detail -->
|
||||
<!-- Focus on the implementation and any important technical decisions. -->
|
||||
|
||||
## Motivation and Context
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
<!-- Why is this change needed? What problem does it solve? -->
|
||||
<!-- Use "Fixes #123" or "Closes #123" when this pull request resolves an issue. -->
|
||||
|
||||
## How has this been tested?
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, tests ran to see how -->
|
||||
<!--- your change affects other areas of the code, etc. -->
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
<!-- Please describe in detail how you tested your changes. -->
|
||||
<!-- Include details of your testing environment, tests ran to see how -->
|
||||
<!-- your change affects other areas of the code, etc. -->
|
||||
|
||||
## Screenshots:
|
||||
|
||||
<!-- Add screenshots or recordings for visual changes. -->
|
||||
<!-- Remove this section if it is not applicable. -->
|
||||
|
||||
## Types of changes
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
|
||||
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Refactoring (change which does not alter existing functionality)
|
||||
- [ ] Documentation (changes to documentation only)
|
||||
|
||||
## Checklist:
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
|
||||
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
|
||||
- [ ] I have reviewed my own changes.
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] I have added or updated tests where appropriate.
|
||||
- [ ] My changes do not include unrelated modifications.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
|
||||
## AI Disclosure:
|
||||
|
||||
<!-- See the AI contribution policy: https://github.com/wg-easy/wg-easy/blob/master/contributing.md#ai-contribution-policy -->
|
||||
<!-- Disclose whether AI tools were used -->
|
||||
<!-- Which tools were used -->
|
||||
<!-- The extent to which they were used -->
|
||||
|
||||
@@ -5,13 +5,3 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -123,8 +123,14 @@ jobs:
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
n=3
|
||||
for i in $(seq 1 $n); do
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/wg-easy/wg-easy@sha256:%s ' *)
|
||||
$(printf 'ghcr.io/wg-easy/wg-easy@sha256:%s ' *) && break
|
||||
|
||||
[ "$i" = "$n" ] && exit 1
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
@@ -138,7 +144,7 @@ jobs:
|
||||
contents: write
|
||||
needs: docker-merge
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
@@ -132,8 +132,14 @@ jobs:
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
n=3
|
||||
for i in $(seq 1 $n); do
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/wg-easy/wg-easy@sha256:%s ' *)
|
||||
$(printf 'ghcr.io/wg-easy/wg-easy@sha256:%s ' *) && break
|
||||
|
||||
[ "$i" = "$n" ] && exit 1
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
@@ -147,7 +153,7 @@ jobs:
|
||||
contents: write
|
||||
needs: docker-merge
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
# - platform: linux/arm/v7
|
||||
# os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -135,8 +135,14 @@ jobs:
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
n=3
|
||||
for i in $(seq 1 $n); do
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/wg-easy/wg-easy@sha256:%s ' *)
|
||||
$(printf 'ghcr.io/wg-easy/wg-easy@sha256:%s ' *) && break
|
||||
|
||||
[ "$i" = "$n" ] && exit 1
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
@@ -152,7 +158,7 @@ jobs:
|
||||
contents: write
|
||||
needs: docker-merge
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "lts/krypton"
|
||||
check-latest: true
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "lts/krypton"
|
||||
check-latest: true
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
.DS_Store
|
||||
*.swp
|
||||
node_modules
|
||||
|
||||
docs/site
|
||||
docs/.cache
|
||||
Vendored
+4
-1
@@ -26,5 +26,8 @@
|
||||
"i18n-ally.sortKeys": false,
|
||||
"i18n-ally.keepFulfilled": false,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"editor.gotoLocation.multipleDefinitions": "goto"
|
||||
"editor.gotoLocation.multipleDefinitions": "goto",
|
||||
"[toml]": {
|
||||
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Support disabling version check (https://github.com/wg-easy/wg-easy/pull/2648)
|
||||
- OAuth integration (https://github.com/wg-easy/wg-easy/pull/2659)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Button styling (https://github.com/wg-easy/wg-easy/pull/2666)
|
||||
- Escape Prometheus label values (https://github.com/wg-easy/wg-easy/pull/2702)
|
||||
- Skip ip6tables for firewall when IPv6 is disabled (https://github.com/wg-easy/wg-easy/pull/2701)
|
||||
|
||||
### Changed
|
||||
|
||||
- Only set sysctl if needed (https://github.com/wg-easy/wg-easy/pull/2630)
|
||||
- Improve security (https://github.com/wg-easy/wg-easy/pull/2661, https://github.com/wg-easy/wg-easy/pull/2668, https://github.com/wg-easy/wg-easy/pull/2669)
|
||||
- Replace mkdocs with zensical (https://github.com/wg-easy/wg-easy/pull/2667)
|
||||
- Migrate to Nuxt 4 (https://github.com/wg-easy/wg-easy/pull/2660)
|
||||
|
||||
## [15.3.0] - 2026-05-18
|
||||
|
||||
### Added
|
||||
|
||||
- AWG: support for H1-H4 ranges (https://github.com/wg-easy/wg-easy/pull/2480)
|
||||
- Client Firewall (https://github.com/wg-easy/wg-easy/pull/2418)
|
||||
- CLI: Show QR code (https://github.com/wg-easy/wg-easy/pull/2518)
|
||||
|
||||
+19
-10
@@ -2,17 +2,15 @@
|
||||
|
||||
First and foremost, thank you! We appreciate that you want to contribute to wg-easy, your time is valuable, and your contributions mean a lot to us.
|
||||
|
||||
|
||||
## Important!
|
||||
|
||||
By contributing to this project, you:
|
||||
|
||||
* Agree that you have authored 100% of the content
|
||||
* Agree that you have the necessary rights to the content
|
||||
* Agree that you have received the necessary permissions from your employer to make the contributions (if applicable)
|
||||
* Agree that the content you contribute may be provided under the Project license(s)
|
||||
* Agree that, if you did not author 100% of the content, the appropriate licenses and copyrights have been added along with any other necessary attribution.
|
||||
|
||||
- Agree that you have authored 100% of the content
|
||||
- Agree that you have the necessary rights to the content
|
||||
- Agree that you have received the necessary permissions from your employer to make the contributions (if applicable)
|
||||
- Agree that the content you contribute may be provided under the Project license(s)
|
||||
- Agree that, if you did not author 100% of the content, the appropriate licenses and copyrights have been added along with any other necessary attribution.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -24,7 +22,6 @@ Creating an issue is the simplest form of contributing to a project. But there a
|
||||
- Feature requests
|
||||
- Bug reports
|
||||
|
||||
|
||||
## Showing support for wg-easy
|
||||
|
||||
Please keep in mind that open source software is built by people like you, who spend their free time creating things the rest the community can use.
|
||||
@@ -34,6 +31,20 @@ Don't have time to contribute? No worries, here are some other ways to show your
|
||||
- star the [project](https://github.com/wg-easy/wg-easy)
|
||||
- tweet your support for wg-easy
|
||||
|
||||
## AI Contribution Policy
|
||||
|
||||
Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly and disclose that use clearly.
|
||||
|
||||
1. Review AI-generated code closely before marking a pull request ready for review.
|
||||
2. Manually test the changes and add appropriate automated tests where feasible.
|
||||
3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review.
|
||||
4. Disclose AI-assisted content clearly.
|
||||
5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model.
|
||||
6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit.
|
||||
|
||||
Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves.
|
||||
|
||||
We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process.
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -71,7 +82,6 @@ Please be as descriptive as possible when creating an issue. Give us the informa
|
||||
- **error messages**: (required) please paste any error messages into the issue, or a [gist](https://gist.github.com/)
|
||||
- **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using
|
||||
|
||||
|
||||
### Closing issues
|
||||
|
||||
The original poster or the maintainers of wg-easy may close an issue at any time. Typically, but not exclusively, issues are closed when:
|
||||
@@ -81,7 +91,6 @@ The original poster or the maintainers of wg-easy may close an issue at any time
|
||||
- An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked.
|
||||
- A discussion has clearly run its course
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
**Tips for creating idiomatic issues**
|
||||
|
||||
@@ -5,6 +5,7 @@ title: API
|
||||
/// warning | Breaking Changes
|
||||
|
||||
This API is not yet stable and may change in the future. The API is currently in development and is subject to change without notice. The API is not yet documented, but we will add documentation as the API stabilizes.
|
||||
|
||||
///
|
||||
|
||||
You can use the API to interact with the application programmatically. The API is available at `/api` and supports both GET and POST requests. The API is designed to be simple and easy to use, with a focus on providing a consistent interface for all endpoints.
|
||||
|
||||
@@ -40,13 +40,13 @@ If a parameter is not set, it will not be added to the configuration. If all Amn
|
||||
### Parameter Compatibility Table
|
||||
|
||||
| Parameter | Can differ between server and client | Configurable on server | Configurable on client |
|
||||
| --------- | ------------------------------------ | ---------------------- | ----------------------- |
|
||||
| Jc | ✅ Yes | ✅ | ✅ |
|
||||
| Jmin | ✅ Yes | ✅ | ✅ |
|
||||
| Jmax | ✅ Yes | ✅ | ✅ |
|
||||
| S1-S4 | ❌ No, must match | ✅ | ❌ (copied from server) |
|
||||
| H1-H4 | ❌ No, must match | ✅ | ❌ (copied from server) |
|
||||
| I1-I5 | ✅ Yes | ✅ | ✅ |
|
||||
| --------- | ------------------------------------ | ---------------------- | ------------------------ |
|
||||
| Jc | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Jmin | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Jmax | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| S1-S4 | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| H1-H4 | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| I1-I5 | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
## Client Applications
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: External Authentication
|
||||
---
|
||||
|
||||
## OAuth
|
||||
|
||||
### Setup
|
||||
|
||||
To enable OAuth set the env var `OAUTH_PROVIDERS` to any of the following providers:
|
||||
|
||||
| Provider | Value |
|
||||
| ----------------------------- | -------- |
|
||||
| [Google](#google) | `google` |
|
||||
| [GitHub](#github) | `github` |
|
||||
| [Generic OIDC](#generic-oidc) | `oidc` |
|
||||
|
||||
You can enable multiple providers by separating them with a comma:
|
||||
|
||||
e.g. `google,github`
|
||||
|
||||
### Auto Register
|
||||
|
||||
To automatically register users that log in with an OAuth provider, set the following environment variable to `true`:
|
||||
|
||||
| Env | Required | Default | Description |
|
||||
| --------------------- | -------- | ------- | ------------------------ |
|
||||
| `OAUTH_AUTO_REGISTER` | :x: | `false` | Enable auto-registration |
|
||||
|
||||
When enabled:
|
||||
|
||||
- If a user logs in with an email address that is not yet registered, a new account will be created for them.
|
||||
|
||||
- If a user logs in with an email address that is already registered, their account will be linked to the OAuth provider (if not already linked), regardless of the value of `OAUTH_AUTO_REGISTER`.
|
||||
|
||||
/// warning | Security
|
||||
|
||||
Users will be created with Admin Permissions, as the permissions system is not yet implemented. Only enable this if you trust all users that can log in with the OAuth provider.
|
||||
|
||||
Use [Allowed Domains](#allowed-domains) to restrict which users can log in.
|
||||
|
||||
///
|
||||
|
||||
### Allowed Domains
|
||||
|
||||
To only allow users with an email address from a specific domain to log in, set the following environment variable to the allowed domain.
|
||||
|
||||
| Env | Required | Default | Description |
|
||||
| ----------------------- | -------- | ------- | --------------------- |
|
||||
| `OAUTH_ALLOWED_DOMAINS` | :x: | - | Allowed email domains |
|
||||
|
||||
You can allow multiple domains by separating them with a comma:
|
||||
|
||||
e.g. `example.com,example.org`
|
||||
|
||||
### Auto Launch
|
||||
|
||||
To automatically launch the OAuth login flow when visiting the login page, set the following environment variable to the provider you want to launch:
|
||||
|
||||
| Env | Required | Default | Description |
|
||||
| ------------------- | -------- | ------- | ----------------------------- |
|
||||
| `OAUTH_AUTO_LAUNCH` | :x: | - | Auto launch an OAuth provider |
|
||||
|
||||
When enabled:
|
||||
|
||||
- Visiting the login page will automatically redirect to the selected provider's login page
|
||||
- The user can still access the normal login page by visiting `/login?auto_launch=false`
|
||||
- You can auto launch any provider by visiting `/login?auto_launch=<provider>`
|
||||
|
||||
### Redirect URIs
|
||||
|
||||
You have to configure the following redirect URIs in your OAuth provider:
|
||||
|
||||
- `https://<your-domain>/api/auth/<provider>/callback`
|
||||
Used to log in to with the provider
|
||||
- `https://<your-domain>/api/auth/<provider>/link`
|
||||
Used to link an existing account to the provider
|
||||
|
||||
If your provider does not support multiple redirect URIs (e.g. GitHub) but allows multiple URIs under the same base, then configure:
|
||||
|
||||
- `https://<your-domain>/api/auth/<provider>/`
|
||||
|
||||
### Provider Configuration
|
||||
|
||||
#### Google
|
||||
|
||||
| Env | Required | Description |
|
||||
| ---------------------------- | ------------------ | -------------------- |
|
||||
| `OAUTH_GOOGLE_CLIENT_ID` | :white_check_mark: | Google Client ID |
|
||||
| `OAUTH_GOOGLE_CLIENT_SECRET` | :white_check_mark: | Google Client Secret |
|
||||
|
||||
<h5>Setup</h5>
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
||||
2. Create an OAuth 2.0 Client ID (Web application)
|
||||
3. Add Authorized redirect URI: See [Redirect URIs](#redirect-uris)
|
||||
4. Copy the Client ID and Client Secret to the environment variables
|
||||
|
||||
#### GitHub
|
||||
|
||||
| Env | Required | Description |
|
||||
| ---------------------------- | ------------------ | -------------------- |
|
||||
| `OAUTH_GITHUB_CLIENT_ID` | :white_check_mark: | GitHub Client ID |
|
||||
| `OAUTH_GITHUB_CLIENT_SECRET` | :white_check_mark: | GitHub Client Secret |
|
||||
|
||||
<h5>Setup</h5>
|
||||
|
||||
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
2. Create a new OAuth App
|
||||
3. Add Authorization callback URL: See [Redirect URIs](#redirect-uris)
|
||||
4. Create a new client secret
|
||||
5. Copy the Client ID and Client Secret to the environment variables
|
||||
|
||||
#### Generic OIDC
|
||||
|
||||
This supports generic OIDC providers like Authelia, Authentik, etc.
|
||||
|
||||
The provider needs to support:
|
||||
|
||||
- PKCE
|
||||
- default scopes: `openid email profile`
|
||||
- Client Secret Authentication `client_secret_post`
|
||||
|
||||
The provider needs to be available with HTTPS and have a valid certificate.
|
||||
|
||||
| Env | Required | Default | Example | Description |
|
||||
| -------------------------- | ------------------ | ------- | -------------------------- | ------------------ |
|
||||
| `OAUTH_OIDC_SERVER` | :white_check_mark: | - | `https://auth.example.com` | OIDC Server |
|
||||
| `OAUTH_OIDC_CLIENT_ID` | :white_check_mark: | - | - | OIDC Client ID |
|
||||
| `OAUTH_OIDC_CLIENT_SECRET` | :white_check_mark: | - | - | OIDC Client Secret |
|
||||
| `OAUTH_OIDC_NAME` | :x: | OIDC | `Authelia` | Provider Name |
|
||||
|
||||
##### Authelia Setup
|
||||
|
||||
Generate Client ID and Secret:
|
||||
|
||||
```shell
|
||||
# Client ID
|
||||
docker run --rm authelia/authelia:latest authelia crypto rand --length 72 --charset rfc3986
|
||||
# Client Secret
|
||||
docker run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
|
||||
```
|
||||
|
||||
```yaml
|
||||
- client_id: '...'
|
||||
client_name: wg-easy
|
||||
client_secret: '$pbkdf2-...'
|
||||
redirect_uris:
|
||||
- https://<your-domain>/api/auth/oidc/callback
|
||||
- https://<your-domain>/api/auth/oidc/link
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
authorization_policy: one_factor
|
||||
pre_configured_consent_duration: 1 week
|
||||
require_pkce: true
|
||||
token_endpoint_auth_method: client_secret_post
|
||||
```
|
||||
|
||||
#### Generic OAuth
|
||||
|
||||
Not currently supported
|
||||
|
||||
### Disable Password Authentication
|
||||
|
||||
To disable password-based authentication and only allow login via OAuth providers, set the following environment variable to `true`:
|
||||
|
||||
| Env | Required | Default | Description |
|
||||
| ----------------------- | -------- | ------- | ------------------------------- |
|
||||
| `DISABLE_PASSWORD_AUTH` | :x: | `false` | Disable password authentication |
|
||||
|
||||
When enabled:
|
||||
|
||||
- Users will not be able to log in with a password
|
||||
|
||||
/// warning | Access Recovery
|
||||
|
||||
Before disabling password authentication, ensure that at least one OAuth provider is configured and that you have successfully linked an administrator account.
|
||||
|
||||
If no login method is available, you will not be able to log in to the application and will need to reset the configuration to regain access.
|
||||
|
||||
///
|
||||
@@ -5,11 +5,12 @@ title: Optional Configuration
|
||||
You can set these environment variables to configure the container. They are not required, but can be useful in some cases.
|
||||
|
||||
| Env | Default | Example | Description |
|
||||
| -------------- | --------- | ----------- | ---------------------------------- |
|
||||
| ----------------------- | --------- | ----------- | --------------------------------------- |
|
||||
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
|
||||
| `HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
|
||||
| `INSECURE` | `false` | `true` | If access over http is allowed |
|
||||
| `DISABLE_IPV6` | `false` | `true` | If IPv6 support should be disabled |
|
||||
| `DISABLE_VERSION_CHECK` | `false` | `true` | If wg-easy should check for new updates |
|
||||
|
||||
/// note | IPv6 Caveats
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ These will only be used during the first start of the container. After that, the
|
||||
If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`.
|
||||
|
||||
If you want to skip the setup process, you have to configure group `1`
|
||||
|
||||
///
|
||||
|
||||
/// note | Security
|
||||
@@ -30,4 +31,5 @@ If you want to skip the setup process, you have to configure group `1`
|
||||
The initial username and password is not checked for complexity. Make sure to set a long enough username and password. Otherwise, the user won't be able to log in.
|
||||
|
||||
It's recommended to remove the variables after the setup is done to prevent the password from being exposed.
|
||||
|
||||
///
|
||||
|
||||
@@ -39,4 +39,5 @@ You can use the following Grafana dashboard to visualize the metrics:
|
||||
|
||||
The Grafana dashboard is not official and is not maintained by the `wg-easy` team. If you have any issues with the dashboard, please contact the author of the dashboard.
|
||||
See [#1299](https://github.com/wg-easy/wg-easy/pull/1299) for more information.
|
||||
|
||||
///
|
||||
|
||||
@@ -9,6 +9,7 @@ This project is Open Source. That means that you can contribute on enhancements,
|
||||
/// note | Attention
|
||||
|
||||
**Before opening an issue**, read the [`README`][github-file-readme] carefully, study the docs for your version (maybe [latest][docs-latest]) and your search engine you trust. The issue tracker is not meant to be used for unrelated questions!
|
||||
|
||||
///
|
||||
|
||||
When opening an issue, please provide details use case to let the community reproduce your problem.
|
||||
@@ -16,6 +17,7 @@ When opening an issue, please provide details use case to let the community repr
|
||||
/// note | Attention
|
||||
|
||||
**Use the issue templates** to provide the necessary information. Issues which do not use these templates are not worked on and closed.
|
||||
|
||||
///
|
||||
|
||||
By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution.
|
||||
@@ -39,6 +41,7 @@ When an option is marked with "not officially supported" / "unsupported", then s
|
||||
/// question | Motivation
|
||||
|
||||
You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results.
|
||||
|
||||
///
|
||||
|
||||
### Submit a Pull-Request
|
||||
|
||||
@@ -9,9 +9,11 @@ This tutorial is a follow-up to the official [Traefik tutorial](./traefik.md). I
|
||||
- A working [wg-easy](./basic-installation.md) and [Traefik](./traefik.md) setup from the previous guides.
|
||||
|
||||
/// warning | Important: Following this guide will reset your WireGuard configuration.
|
||||
|
||||
The process involves re-creating the `wg-easy` container and its data, which means **all existing WireGuard clients and settings will be deleted.**
|
||||
|
||||
You will need to create your clients again after completing this guide.
|
||||
|
||||
///
|
||||
|
||||
## Add `adguard` configuration
|
||||
@@ -152,13 +154,15 @@ networks:
|
||||
2. Navigate to `https://adguard.$example.com$` to begin the AdGuard Home setup.
|
||||
|
||||
/// warning | Important: Configure AdGuard Home Admin Web Interface Port
|
||||
|
||||
During the initial AdGuard Home setup on the `Step 2/5` page, you **must** set the **Admin Web Interface Port** to **3000**. Do not use the default port 80, as it will not work with the Traefik configuration.
|
||||
|
||||
After completing the setup, the AdGuard UI might appear unresponsive. This is expected. **Simply reload the page**, and the panel will display correctly.
|
||||
///
|
||||
|
||||
> If you accidentally left it default (80), you will need to manually edit the `docker-compose.yml` file for AdGuard Home (`/etc/docker/containers/adguard/docker-compose.yml`) and change the line `traefik.http.services.adguard.loadbalancer.server.port=3000` to `traefik.http.services.adguard.loadbalancer.server.port=80`. After making this change, restart AdGuard Home by navigating to `/etc/docker/containers/adguard` and running `sudo docker compose up -d`.
|
||||
|
||||
///
|
||||
|
||||
## Final System Checks
|
||||
|
||||
### Firewall
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Basic Installation
|
||||
---
|
||||
|
||||
<!-- TOOD: add docs for pihole, nginx, caddy, traefik -->
|
||||
<!-- TODO: add docs for pihole, nginx, caddy, traefik -->
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Caddy
|
||||
/// note | Opinionated
|
||||
|
||||
This guide is opinionated. If you use other conventions or folder layouts, feel free to change the commands and paths.
|
||||
|
||||
///
|
||||
|
||||
We're using [Caddy](https://caddyserver.com/) here as reverse proxy to serve `wg-easy` on [https://wg-easy.example.com](https://wg-easy.example.com) via TLS.
|
||||
|
||||
@@ -7,6 +7,7 @@ title: No Reverse Proxy
|
||||
This is insecure. You should use a reverse proxy to secure the connection.
|
||||
|
||||
Only use this method if you know what you are doing.
|
||||
|
||||
///
|
||||
|
||||
If you only allow access to the web UI from your local network, you can skip the reverse proxy setup. This is not recommended, but it is possible.
|
||||
|
||||
@@ -61,6 +61,7 @@ Pick an IPv4 and IPv6 subnet for your clients and add static routes on your rout
|
||||
/// note | 2001:db8::/32
|
||||
|
||||
The _documentation prefix_ `2001:db8::/32` (RFC 3849) used in this example is not meant for production use, replace it with your own ISP-assigned IPv6 prefix (GUA) or local prefix (ULA)
|
||||
|
||||
///
|
||||
|
||||
I want my WireGuard clients in `192.168.0.0/24` and `2001:db8:abc:0::/64`.
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Traefik
|
||||
/// note | Opinionated
|
||||
|
||||
This guide is opinionated. If you use other conventions or folder layouts, feel free to change the commands and paths.
|
||||
|
||||
///
|
||||
|
||||
## Create docker compose project
|
||||
|
||||
@@ -24,6 +24,7 @@ There are a few requirements for a suitable host system:
|
||||
|
||||
On the host, you need to have a suitable container runtime (like _Docker_ or _Podman_) installed. We assume [_Docker Compose_][docker-compose] is [installed][docker-compose-installation]. We have aligned file names and configuration conventions with the latest [Docker Compose specification][docker-compose-specification].
|
||||
If you're using podman, make sure to read the related [documentation][docs-podman].
|
||||
|
||||
///
|
||||
|
||||
[docker-compose]: https://docs.docker.com/compose/
|
||||
@@ -69,6 +70,7 @@ When publishing a tag we follow the [Semantic Versioning][semver] specification.
|
||||
/// danger | Use the Correct Commands For Stopping and Starting `wg-easy`
|
||||
|
||||
**Use `sudo docker compose up / down`, not `sudo docker compose start / stop`**. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.
|
||||
|
||||
///
|
||||
|
||||
**That's it! It really is that easy**.
|
||||
|
||||
@@ -23,7 +23,9 @@ This feature is currently experimental. While functional, it should be thoroughl
|
||||
- The feature cannot be enabled if these tools are not available
|
||||
|
||||
/// note
|
||||
|
||||
Most Linux distributions include iptables by default. If you're running in a minimal container environment, you may need to install the `iptables` package on the host system.
|
||||
|
||||
///
|
||||
|
||||
**Enable this feature if you want to:**
|
||||
|
||||
@@ -62,7 +62,9 @@ Protocol specifiers (`/tcp` or `/udp`) require a port number. The following form
|
||||
- **Disable for specific client**: To disable firewall filtering for a single client while keeping it enabled for others, add `0.0.0.0/0, ::/0` to allow all traffic
|
||||
|
||||
/// note
|
||||
|
||||
To allow clients to reach the VPN server itself (e.g. for DNS), include the server's VPN address in the firewall allowed IPs.
|
||||
|
||||
///
|
||||
|
||||
**Use Case Examples**:
|
||||
|
||||
@@ -9,6 +9,7 @@ hide:
|
||||
/// info | This Documentation is Versioned
|
||||
|
||||
**Make sure** to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to [the most recent stable release][docs-tagging].
|
||||
|
||||
///
|
||||
|
||||
This documentation provides you not only with the basic setup and configuration of `wg-easy` but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
site_name: 'wg-easy'
|
||||
site_description: 'The easiest way to run WireGuard VPN + Web-based Admin UI.'
|
||||
site_author: 'WireGuard Easy'
|
||||
copyright: >
|
||||
<p>
|
||||
© <a href="https://github.com/wg-easy"><em>Wireguard Easy</em></a><br/>
|
||||
<span>This project is licensed under AGPL-3.0-only.</span><br/>
|
||||
<span>This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security</span><br/>
|
||||
<span>"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld</span>
|
||||
</p>
|
||||
|
||||
repo_url: https://github.com/wg-easy/wg-easy
|
||||
repo_name: wg-easy
|
||||
|
||||
edit_uri: 'edit/master/docs/content'
|
||||
|
||||
docs_dir: 'content/'
|
||||
|
||||
site_url: https://wg-easy.github.io/wg-easy
|
||||
|
||||
theme:
|
||||
name: material
|
||||
favicon: assets/logo/favicon.png
|
||||
logo: assets/logo/logo.png
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.top
|
||||
- navigation.expand
|
||||
- navigation.instant
|
||||
- content.action.edit
|
||||
- content.action.view
|
||||
- content.code.annotate
|
||||
palette:
|
||||
# Light mode
|
||||
- media: '(prefers-color-scheme: light)'
|
||||
scheme: default
|
||||
primary: grey
|
||||
accent: red
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
# Dark mode
|
||||
- media: '(prefers-color-scheme: dark)'
|
||||
scheme: slate
|
||||
primary: grey
|
||||
accent: red
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
|
||||
extra:
|
||||
version:
|
||||
provider: mike
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
anchorlink: true
|
||||
- abbr
|
||||
- attr_list
|
||||
- pymdownx.blocks.admonition:
|
||||
types:
|
||||
- danger
|
||||
- note
|
||||
- info
|
||||
- question
|
||||
- warning
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
slugify: !!python/object/apply:pymdownx.slugs.slugify
|
||||
kwds:
|
||||
case: lower
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.magiclink
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.tilde
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
@@ -1,4 +1,2 @@
|
||||
mkdocs-material
|
||||
pillow
|
||||
cairosvg
|
||||
mike
|
||||
zensical==0.0.45
|
||||
git+https://github.com/squidfunk/mike.git
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
[project]
|
||||
site_name = "wg-easy"
|
||||
site_description = "The easiest way to run WireGuard VPN + Web-based Admin UI."
|
||||
site_author = "WireGuard Easy"
|
||||
copyright = """
|
||||
<p>
|
||||
© <a href="https://github.com/wg-easy"><em>Wireguard Easy</em></a><br/>
|
||||
<span>This project is licensed under AGPL-3.0-only.</span><br/>
|
||||
<span>This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Jason A. Donenfeld, ZX2C4 or Edge Security</span><br/>
|
||||
<span>"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld</span>
|
||||
</p>
|
||||
"""
|
||||
|
||||
repo_url = "https://github.com/wg-easy/wg-easy"
|
||||
repo_name = "wg-easy"
|
||||
|
||||
edit_uri = "edit/master/docs/content"
|
||||
|
||||
docs_dir = "content/"
|
||||
|
||||
site_url = "https://wg-easy.github.io/wg-easy"
|
||||
|
||||
[project.theme]
|
||||
variant = "modern"
|
||||
favicon = "assets/logo/favicon.png"
|
||||
logo = "assets/logo/logo.png"
|
||||
features = [
|
||||
"navigation.instant",
|
||||
"navigation.tabs",
|
||||
"navigation.expand",
|
||||
"navigation.path",
|
||||
"navigation.top",
|
||||
"content.action.edit",
|
||||
"content.action.view",
|
||||
"content.code.copy",
|
||||
"content.code.annotate",
|
||||
]
|
||||
|
||||
[project.theme.icon]
|
||||
repo = "fontawesome/brands/github"
|
||||
|
||||
# Palette toggle for automatic mode
|
||||
[[project.theme.palette]]
|
||||
media = "(prefers-color-scheme)"
|
||||
toggle.icon = "lucide/sun-moon"
|
||||
toggle.name = "Switch to light mode"
|
||||
|
||||
# Palette toggle for light mode
|
||||
[[project.theme.palette]]
|
||||
media = "(prefers-color-scheme: light)"
|
||||
scheme = "default"
|
||||
primary = "grey"
|
||||
accent = "red"
|
||||
toggle.icon = "lucide/sun"
|
||||
toggle.name = "Switch to dark mode"
|
||||
|
||||
# Palette toggle for dark mode
|
||||
[[project.theme.palette]]
|
||||
media = "(prefers-color-scheme: dark)"
|
||||
scheme = "slate"
|
||||
primary = "grey"
|
||||
accent = "red"
|
||||
toggle.icon = "lucide/moon"
|
||||
toggle.name = "Switch to system preference"
|
||||
|
||||
# [project.plugins.privacy]
|
||||
|
||||
[project.extra.version]
|
||||
provider = "mike"
|
||||
|
||||
[project.markdown_extensions.pymdownx.blocks.admonition]
|
||||
types = ["danger", "note", "info", "question", "warning"]
|
||||
|
||||
[project.markdown_extensions.md_in_html]
|
||||
|
||||
[project.markdown_extensions.toc]
|
||||
permalink = true
|
||||
|
||||
[project.markdown_extensions.pymdownx.details]
|
||||
|
||||
[project.markdown_extensions.pymdownx.emoji]
|
||||
emoji_generator = "zensical.extensions.emoji.to_svg"
|
||||
emoji_index = "zensical.extensions.emoji.twemoji"
|
||||
|
||||
[project.markdown_extensions.pymdownx.highlight]
|
||||
anchor_linenums = true
|
||||
line_spans = "__span"
|
||||
pygments_lang_class = true
|
||||
|
||||
[project.markdown_extensions.pymdownx.inlinehilite]
|
||||
|
||||
[project.markdown_extensions.pymdownx.smartsymbols]
|
||||
|
||||
[project.markdown_extensions.pymdownx.superfences]
|
||||
custom_fences = [
|
||||
{ name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" },
|
||||
]
|
||||
+3
-3
@@ -5,13 +5,13 @@
|
||||
"dev": "docker compose -f docker-compose.dev.yml up wg-easy --build",
|
||||
"cli:dev": "docker compose -f docker-compose.dev.yml run --build --rm -it wg-easy cli:dev",
|
||||
"build": "docker build -t wg-easy .",
|
||||
"docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs squidfunk/mkdocs-material serve -a 0.0.0.0:8080",
|
||||
"docs:preview": "docker run --rm -it -p 8080:8080 -v ./docs:/docs zensical/zensical serve -a 0.0.0.0:8080",
|
||||
"scripts:version": "bash scripts/version.sh",
|
||||
"scripts:i18n": "bash scripts/i18n.sh",
|
||||
"format:check:docs": "prettier --check docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.8.3"
|
||||
"prettier": "^3.9.5"
|
||||
},
|
||||
"packageManager": "pnpm@11.1.2"
|
||||
"packageManager": "pnpm@11.15.1"
|
||||
}
|
||||
|
||||
Generated
+5
-5
@@ -9,16 +9,16 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.8.3
|
||||
version: 3.8.3
|
||||
specifier: ^3.9.5
|
||||
version: 3.9.5
|
||||
|
||||
packages:
|
||||
|
||||
prettier@3.8.3:
|
||||
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
|
||||
prettier@3.9.5:
|
||||
resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
prettier@3.8.3: {}
|
||||
prettier@3.9.5: {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<component
|
||||
:is="elementType"
|
||||
role="button"
|
||||
class="rounded-lg border-2 border-gray-100 text-gray-500 hover:border-red-800 hover:bg-red-800 hover:text-white focus:border-red-800 focus:outline-0 focus:ring-0 disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-100 disabled:text-gray-400 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400 dark:disabled:border-neutral-800 dark:disabled:bg-neutral-700 dark:disabled:text-neutral-400"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
as: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
});
|
||||
|
||||
const elementType = computed(() => props.as);
|
||||
|
||||
const attrs = computed(() => {
|
||||
const { as, ...rest } = props;
|
||||
return rest;
|
||||
});
|
||||
</script>
|
||||
@@ -14,10 +14,11 @@ const props = defineProps<{ client: LocalClient }>();
|
||||
const clientsStore = useClientsStore();
|
||||
|
||||
const _showOneTimeLink = useSubmit(
|
||||
`/api/client/${props.client.id}/generateOneTimeLink`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/client/${props.client.id}/generateOneTimeLink`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
await clientsStore.refresh();
|
||||
|
||||
@@ -18,10 +18,11 @@ const enabled = ref(props.client.enabled);
|
||||
const clientsStore = useClientsStore();
|
||||
|
||||
const _disableClient = useSubmit(
|
||||
`/api/client/${props.client.id}/disable`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/client/${props.client.id}/disable`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
await clientsStore.refresh();
|
||||
@@ -31,10 +32,11 @@ const _disableClient = useSubmit(
|
||||
);
|
||||
|
||||
const _enableClient = useSubmit(
|
||||
`/api/client/${props.client.id}/enable`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/client/${props.client.id}/enable`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
await clientsStore.refresh();
|
||||
|
||||
@@ -43,10 +43,11 @@ function createClient() {
|
||||
}
|
||||
|
||||
const _createClient = useSubmit(
|
||||
'/api/client',
|
||||
{
|
||||
(data) =>
|
||||
$fetch('/api/client', {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: () => clientsStore.refresh(),
|
||||
successMsg: t('client.created'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BasePrimaryButton @click="toggleSort">
|
||||
<IconsArrowDown v-if="globalStore.sortClient === true" class="mr-2 w-4" />
|
||||
<IconsArrowDown v-if="globalStore.sortClient === 'asc'" class="mr-2 w-4" />
|
||||
<IconsArrowUp v-else class="mr-2 w-4" />
|
||||
<span class="text-sm">{{ $t('client.sort') }}</span>
|
||||
</BasePrimaryButton>
|
||||
@@ -11,7 +11,7 @@ const globalStore = useGlobalStore();
|
||||
const clientsStore = useClientsStore();
|
||||
|
||||
function toggleSort() {
|
||||
globalStore.sortClient = !globalStore.sortClient;
|
||||
globalStore.sortClient = globalStore.sortClient === 'asc' ? 'desc' : 'asc';
|
||||
clientsStore.refresh().catch(console.error);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<input
|
||||
:value="label"
|
||||
:type="type ?? 'button'"
|
||||
class="col-span-2 rounded-lg border-2 border-red-800 bg-red-800 py-2 text-white hover:border-red-600 hover:bg-red-600 focus:border-red-800 focus:outline-0 focus:ring-0"
|
||||
class="col-span-2 rounded-lg border-2 border-red-800 bg-red-800 px-4 py-2 text-white hover:border-red-600 hover:bg-red-600 focus:border-red-800 focus:outline-0 focus:ring-0"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
<template>
|
||||
<input
|
||||
:value="label"
|
||||
:type="type ?? 'button'"
|
||||
class="col-span-2 rounded-lg border-2 border-gray-100 py-2 text-gray-500 hover:border-red-800 hover:bg-red-800 hover:text-white focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<BaseFormSecondaryButton
|
||||
:as="as"
|
||||
:value="isInput ? label : undefined"
|
||||
:type="isInput ? type : undefined"
|
||||
class="col-span-2 px-4 py-2"
|
||||
>
|
||||
<template v-if="!isInput">
|
||||
{{ label }}
|
||||
</template>
|
||||
</BaseFormSecondaryButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { InputTypeHTMLAttribute } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label: string;
|
||||
type?: InputTypeHTMLAttribute;
|
||||
}>();
|
||||
as?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'button',
|
||||
as: 'input',
|
||||
}
|
||||
);
|
||||
|
||||
const isInput = computed(() => {
|
||||
return props.as === 'input';
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<SelectRoot v-model="langProxy" :default-value="locale">
|
||||
<SelectTrigger
|
||||
class="inline-flex h-8 items-center justify-around gap-2 rounded bg-gray-200 px-3 text-sm leading-none dark:bg-neutral-700 dark:text-neutral-400"
|
||||
class="group inline-flex h-8 items-center justify-around gap-2 rounded bg-gray-200 px-3 text-sm leading-none dark:bg-neutral-700 dark:text-neutral-400"
|
||||
aria-label="Select language"
|
||||
>
|
||||
<IconsLanguage class="size-3" />
|
||||
<SelectValue />
|
||||
<IconsArrowDown class="size-3" />
|
||||
<IconsArrowDown
|
||||
class="size-3 transition-transform group-data-[state=open]:rotate-180"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectPortal>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<!-- https://brand.github.com/foundations/logo -->
|
||||
<svg
|
||||
width="98"
|
||||
height="96"
|
||||
viewBox="0 0 98 96"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_730_27136)">
|
||||
<path
|
||||
d="M41.4395 69.3848C28.8066 67.8535 19.9062 58.7617 19.9062 46.9902C19.9062 42.2051 21.6289 37.0371 24.5 33.5918C23.2559 30.4336 23.4473 23.7344 24.8828 20.959C28.7109 20.4805 33.8789 22.4902 36.9414 25.2656C40.5781 24.1172 44.4062 23.543 49.0957 23.543C53.7852 23.543 57.6133 24.1172 61.0586 25.1699C64.0254 22.4902 69.2891 20.4805 73.1172 20.959C74.457 23.543 74.6484 30.2422 73.4043 33.4961C76.4668 37.1328 78.0937 42.0137 78.0937 46.9902C78.0937 58.7617 69.1934 67.6621 56.3691 69.2891C59.623 71.3945 61.8242 75.9883 61.8242 81.252L61.8242 91.2051C61.8242 94.0762 64.2168 95.7031 67.0879 94.5547C84.4102 87.9512 98 70.6289 98 49.1914C98 22.1074 75.9883 6.69539e-07 48.9043 4.309e-07C21.8203 1.92261e-07 -1.9479e-07 22.1074 -4.3343e-07 49.1914C-6.20631e-07 70.4375 13.4941 88.0469 31.6777 94.6504C34.2617 95.6074 36.75 93.8848 36.75 91.3008L36.75 83.6445C35.4102 84.2188 33.6875 84.6016 32.1562 84.6016C25.8398 84.6016 22.1074 81.1563 19.4277 74.7441C18.375 72.1602 17.2266 70.6289 15.0254 70.3418C13.877 70.2461 13.4941 69.7676 13.4941 69.1934C13.4941 68.0449 15.4082 67.1836 17.3223 67.1836C20.0977 67.1836 22.4902 68.9063 24.9785 72.4473C26.8926 75.2227 28.9023 76.4668 31.2949 76.4668C33.6875 76.4668 35.2187 75.6055 37.4199 73.4043C39.0469 71.7773 40.291 70.3418 41.4395 69.3848Z"
|
||||
fill="white"
|
||||
class="fill-black dark:fill-white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_730_27136">
|
||||
<rect width="98" height="96" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<!-- By Original: Google Vector: Designism, Liquinoid, and YeBoy371 - Own work based on: Google updating its ‘G’ icon for the first time in 10 years, Public Domain, https://commons.wikimedia.org/w/index.php?curid=165142698 -->
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 268.1522 273.8827"
|
||||
overflow="hidden"
|
||||
xml:space="preserve"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="a">
|
||||
<stop offset="0" stop-color="#0fbc5c" />
|
||||
<stop offset="1" stop-color="#0cba65" />
|
||||
</linearGradient>
|
||||
<linearGradient id="g">
|
||||
<stop offset=".2312727" stop-color="#0fbc5f" />
|
||||
<stop offset=".3115468" stop-color="#0fbc5f" />
|
||||
<stop offset=".3660131" stop-color="#0fbc5e" />
|
||||
<stop offset=".4575163" stop-color="#0fbc5d" />
|
||||
<stop offset=".540305" stop-color="#12bc58" />
|
||||
<stop offset=".6993464" stop-color="#28bf3c" />
|
||||
<stop offset=".7712418" stop-color="#38c02b" />
|
||||
<stop offset=".8605665" stop-color="#52c218" />
|
||||
<stop offset=".9150327" stop-color="#67c30f" />
|
||||
<stop offset="1" stop-color="#86c504" />
|
||||
</linearGradient>
|
||||
<linearGradient id="h">
|
||||
<stop offset=".1416122" stop-color="#1abd4d" />
|
||||
<stop offset=".2475151" stop-color="#6ec30d" />
|
||||
<stop offset=".3115468" stop-color="#8ac502" />
|
||||
<stop offset=".3660131" stop-color="#a2c600" />
|
||||
<stop offset=".4456735" stop-color="#c8c903" />
|
||||
<stop offset=".540305" stop-color="#ebcb03" />
|
||||
<stop offset=".6156363" stop-color="#f7cd07" />
|
||||
<stop offset=".6993454" stop-color="#fdcd04" />
|
||||
<stop offset=".7712418" stop-color="#fdce05" />
|
||||
<stop offset=".8605661" stop-color="#ffce0a" />
|
||||
</linearGradient>
|
||||
<linearGradient id="f">
|
||||
<stop offset=".3159041" stop-color="#ff4c3c" />
|
||||
<stop offset=".6038179" stop-color="#ff692c" />
|
||||
<stop offset=".7268366" stop-color="#ff7825" />
|
||||
<stop offset=".884534" stop-color="#ff8d1b" />
|
||||
<stop offset="1" stop-color="#ff9f13" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b">
|
||||
<stop offset=".2312727" stop-color="#ff4541" />
|
||||
<stop offset=".3115468" stop-color="#ff4540" />
|
||||
<stop offset=".4575163" stop-color="#ff4640" />
|
||||
<stop offset=".540305" stop-color="#ff473f" />
|
||||
<stop offset=".6993464" stop-color="#ff5138" />
|
||||
<stop offset=".7712418" stop-color="#ff5b33" />
|
||||
<stop offset=".8605665" stop-color="#ff6c29" />
|
||||
<stop offset="1" stop-color="#ff8c18" />
|
||||
</linearGradient>
|
||||
<linearGradient id="d">
|
||||
<stop offset=".4084578" stop-color="#fb4e5a" />
|
||||
<stop offset="1" stop-color="#ff4540" />
|
||||
</linearGradient>
|
||||
<linearGradient id="c">
|
||||
<stop offset=".1315461" stop-color="#0cba65" />
|
||||
<stop offset=".2097843" stop-color="#0bb86d" />
|
||||
<stop offset=".2972969" stop-color="#09b479" />
|
||||
<stop offset=".3962575" stop-color="#08ad93" />
|
||||
<stop offset=".4771242" stop-color="#0aa6a9" />
|
||||
<stop offset=".5684245" stop-color="#0d9cc6" />
|
||||
<stop offset=".667385" stop-color="#1893dd" />
|
||||
<stop offset=".7687273" stop-color="#258bf1" />
|
||||
<stop offset=".8585063" stop-color="#3086ff" />
|
||||
</linearGradient>
|
||||
<linearGradient id="e">
|
||||
<stop offset=".3660131" stop-color="#ff4e3a" />
|
||||
<stop offset=".4575163" stop-color="#ff8a1b" />
|
||||
<stop offset=".540305" stop-color="#ffa312" />
|
||||
<stop offset=".6156363" stop-color="#ffb60c" />
|
||||
<stop offset=".7712418" stop-color="#ffcd0a" />
|
||||
<stop offset=".8605665" stop-color="#fecf0a" />
|
||||
<stop offset=".9150327" stop-color="#fecf08" />
|
||||
<stop offset="1" stop-color="#fdcd01" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="s"
|
||||
xlink:href="#a"
|
||||
x1="219.6997"
|
||||
y1="329.5351"
|
||||
x2="254.4673"
|
||||
y2="329.5351"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
/>
|
||||
<radialGradient
|
||||
id="m"
|
||||
xlink:href="#b"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-1.936885,1.043001,1.455731,2.555422,290.5254,-400.6338)"
|
||||
cx="109.6267"
|
||||
cy="135.8619"
|
||||
fx="109.6267"
|
||||
fy="135.8619"
|
||||
r="71.46001"
|
||||
/>
|
||||
<radialGradient
|
||||
id="n"
|
||||
xlink:href="#c"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-3.512595,-4.45809,-1.692547,1.260616,870.8006,191.554)"
|
||||
cx="45.25866"
|
||||
cy="279.2738"
|
||||
fx="45.25866"
|
||||
fy="279.2738"
|
||||
r="71.46001"
|
||||
/>
|
||||
<radialGradient
|
||||
id="l"
|
||||
xlink:href="#d"
|
||||
cx="304.0166"
|
||||
cy="118.0089"
|
||||
fx="304.0166"
|
||||
fy="118.0089"
|
||||
r="47.85445"
|
||||
gradientTransform="matrix(2.064353,-4.926832e-6,-2.901531e-6,2.592041,-297.6788,-151.7469)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
/>
|
||||
<radialGradient
|
||||
id="o"
|
||||
xlink:href="#e"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.2485783,2.083138,2.962486,0.3341668,-255.1463,-331.1636)"
|
||||
cx="181.001"
|
||||
cy="177.2013"
|
||||
fx="181.001"
|
||||
fy="177.2013"
|
||||
r="71.46001"
|
||||
/>
|
||||
<radialGradient
|
||||
id="p"
|
||||
xlink:href="#f"
|
||||
cx="207.6733"
|
||||
cy="108.0972"
|
||||
fx="207.6733"
|
||||
fy="108.0972"
|
||||
r="41.1025"
|
||||
gradientTransform="matrix(-1.249206,1.343263,-3.896837,-3.425693,880.5011,194.9051)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
/>
|
||||
<radialGradient
|
||||
id="r"
|
||||
xlink:href="#g"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-1.936885,-1.043001,1.455731,-2.555422,290.5254,838.6834)"
|
||||
cx="109.6267"
|
||||
cy="135.8619"
|
||||
fx="109.6267"
|
||||
fy="135.8619"
|
||||
r="71.46001"
|
||||
/>
|
||||
<radialGradient
|
||||
id="j"
|
||||
xlink:href="#h"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.081402,-1.93722,2.926737,-0.1162508,-215.1345,632.8606)"
|
||||
cx="154.8697"
|
||||
cy="145.9691"
|
||||
fx="154.8697"
|
||||
fy="145.9691"
|
||||
r="71.46001"
|
||||
/>
|
||||
<filter
|
||||
id="q"
|
||||
x="-.04842873"
|
||||
y="-.0582241"
|
||||
width="1.096857"
|
||||
height="1.116448"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feGaussianBlur stdDeviation="1.700914" />
|
||||
</filter>
|
||||
<filter
|
||||
id="k"
|
||||
x="-.01670084"
|
||||
y="-.01009856"
|
||||
width="1.033402"
|
||||
height="1.020197"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feGaussianBlur stdDeviation=".2419367" />
|
||||
</filter>
|
||||
<clipPath id="i" clipPathUnits="userSpaceOnUse">
|
||||
<path
|
||||
d="M371.3784 193.2406H237.0825v53.4375h77.167c-1.2405 7.5627-4.0259 15.0024-8.1049 21.7862-4.6734 7.7723-10.4511 13.6895-16.373 18.1957-17.7389 13.4983-38.42 16.2584-52.7828 16.2584-36.2824 0-67.2833-23.2865-79.2844-54.9287-.4843-1.1482-.8059-2.3344-1.1975-3.5068-2.652-8.0533-4.101-16.5825-4.101-25.4474 0-9.226 1.5691-18.0575 4.4301-26.3985 11.2851-32.8967 42.9849-57.4674 80.1789-57.4674 7.4811 0 14.6854.8843 21.5173 2.6481 15.6135 4.0309 26.6578 11.9698 33.4252 18.2494l40.834-39.7111c-24.839-22.616-57.2194-36.3201-95.8444-36.3201-30.8782-.00066-59.3863 9.55308-82.7477 25.6992-18.9454 13.0941-34.4833 30.6254-44.9695 50.9861-9.75366 18.8785-15.09441 39.7994-15.09441 62.2934 0 22.495 5.34891 43.6334 15.10261 62.3374v.126c10.3023 19.8567 25.3678 36.9537 43.6783 49.9878 15.9962 11.3866 44.6789 26.5516 84.0307 26.5516 22.6301 0 42.6867-4.0517 60.3748-11.6447 12.76-5.4775 24.0655-12.6217 34.3012-21.8036 13.5247-12.1323 24.1168-27.1388 31.3465-44.4041 7.2297-17.2654 11.097-36.7895 11.097-57.957 0-9.858-.9971-19.8694-2.6881-28.9684Z"
|
||||
fill="#000"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g transform="matrix(0.957922,0,0,0.985255,-90.17436,-78.85577)">
|
||||
<g clip-path="url(#i)">
|
||||
<path
|
||||
d="M92.07563 219.9585c.14844 22.14 6.5014 44.983 16.11767 63.4234v.1269c6.9482 13.3919 16.4444 23.9704 27.2604 34.4518l65.326-23.67c-12.3593-6.2344-14.2452-10.0546-23.1048-17.0253-9.0537-9.0658-15.8015-19.4735-20.0038-31.677h-.1693l.1693-.1269c-2.7646-8.0587-3.0373-16.6129-3.1393-25.5029Z"
|
||||
fill="url(#j)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="M237.0835 79.02491c-6.4568 22.52569-3.988 44.42139 0 57.16129 7.4561.0055 14.6388.8881 21.4494 2.6464 15.6135 4.0309 26.6566 11.97 33.424 18.2496l41.8794-40.7256c-24.8094-22.58904-54.6663-37.2961-96.7528-37.33169Z"
|
||||
fill="url(#l)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="M236.9434 78.84678c-31.6709-.00068-60.9107 9.79833-84.8718 26.35902-8.8968 6.149-17.0612 13.2521-24.3311 21.1509-1.9045 17.7429 14.2569 39.5507 46.2615 39.3702 15.5284-17.9373 38.4946-29.5427 64.0561-29.5427.0233 0 .046.0019.0693.002l-1.0439-57.33536c-.0472-.00003-.0929-.00406-.1401-.00406Z"
|
||||
fill="url(#m)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="m341.4751 226.3788-28.2685 19.2848c-1.2405 7.5627-4.0278 15.0023-8.1068 21.7861-4.6734 7.7723-10.4506 13.6898-16.3725 18.196-17.7022 13.4704-38.3286 16.2439-52.6877 16.2553-14.8415 25.1018-17.4435 37.6749 1.0439 57.9342 22.8762-.0167 43.157-4.1174 61.0458-11.7965 12.9312-5.551 24.3879-12.7913 34.7609-22.0964 13.7061-12.295 24.4421-27.5034 31.7688-45.0003 7.3267-17.497 11.2446-37.2822 11.2446-58.7336Z"
|
||||
fill="url(#n)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="M234.9956 191.2104v57.4981h136.0062c1.1962-7.8745 5.1523-18.0644 5.1523-26.5001 0-9.858-.9963-21.899-2.6873-30.998Z"
|
||||
fill="#3086ff"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="M128.3894 124.3268c-8.393 9.1191-15.5632 19.326-21.2483 30.3646-9.75351 18.8785-15.09402 41.8295-15.09402 64.3235 0 .317.02642.6271.02855.9436 4.31953 8.2244 59.66647 6.6495 62.45617 0-.0035-.3103-.0387-.6128-.0387-.9238 0-9.226 1.5696-16.0262 4.4306-24.3672 3.5294-10.2885 9.0557-19.7628 16.1223-27.9257 1.6019-2.0309 5.8748-6.3969 7.1214-9.0157.4749-.9975-.8621-1.5574-.9369-1.9085-.0836-.3927-1.8762-.0769-2.2778-.3694-1.2751-.9288-3.8001-1.4138-5.3334-1.8449-3.2772-.9215-8.7085-2.9536-11.7252-5.0601-9.5357-6.6586-24.417-14.6122-33.5047-24.2164Z"
|
||||
fill="url(#o)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="M162.0989 155.8569c22.1123 13.3013 28.4714-6.7139 43.173-12.9771L179.698 90.21568c-9.4075 3.92642-18.2957 8.80465-26.5426 14.50442-12.316 8.5122-23.192 18.8995-32.1763 30.7204Z"
|
||||
fill="url(#p)"
|
||||
filter="url(#q)"
|
||||
/>
|
||||
<path
|
||||
d="M171.0987 290.222c-29.6829 10.6413-34.3299 11.023-37.0622 29.2903 5.2213 5.0597 10.8312 9.74 16.7926 13.9835 15.9962 11.3867 46.766 26.5517 86.1178 26.5517.0462 0 .0904-.004.1366-.004v-59.1574c-.0298.0001-.064.002-.0938.002-14.7359 0-26.5113-3.8435-38.5848-10.5273-2.9768-1.6479-8.3775 2.7772-11.1229.799-3.7865-2.7284-12.8991 2.3508-16.1833-.9378Z"
|
||||
fill="url(#r)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
<path
|
||||
d="M219.6997 299.0227v59.9959c5.506.6402 11.2361 1.0289 17.2472 1.0289 6.0259 0 11.8556-.3073 17.5204-.8723v-59.7481c-6.3482 1.0777-12.3272 1.461-17.4776 1.461-5.9318 0-11.7005-.6858-17.29-1.8654Z"
|
||||
opacity=".5"
|
||||
fill="url(#s)"
|
||||
filter="url(#k)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<IconsBrandsGitHub v-if="provider === 'github'" />
|
||||
<IconsBrandsGoogle v-else-if="provider === 'google'" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
provider: OAUTH_PROVIDER;
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<a
|
||||
:href="`/api/auth/${provider}`"
|
||||
class="flex items-center justify-center gap-2 rounded border border-gray-300 bg-white py-2 text-sm text-gray-700 shadow-sm transition hover:bg-gray-50 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<IconsBrandsProvider :provider="provider" class="h-4 w-4" />
|
||||
<span>
|
||||
{{ $t('login.signInWith', [info.friendlyName]) }}
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
info: {
|
||||
enabled: boolean;
|
||||
friendlyName: string;
|
||||
};
|
||||
provider: OAUTH_PROVIDER;
|
||||
}>();
|
||||
</script>
|
||||
@@ -70,10 +70,11 @@ const authStore = useAuthStore();
|
||||
const toggleState = ref(false);
|
||||
|
||||
const _submit = useSubmit(
|
||||
'/api/session',
|
||||
{
|
||||
(data) =>
|
||||
$fetch('/api/session', {
|
||||
method: 'delete',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
await navigateTo('/login');
|
||||
|
||||
@@ -1,49 +1,24 @@
|
||||
import type {
|
||||
NitroFetchRequest,
|
||||
NitroFetchOptions,
|
||||
TypedInternalResponse,
|
||||
ExtractedRouteMethod,
|
||||
} from 'nitropack/types';
|
||||
import { FetchError } from 'ofetch';
|
||||
|
||||
type RevertFn<
|
||||
R extends NitroFetchRequest,
|
||||
T = unknown,
|
||||
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
|
||||
> = (
|
||||
success: boolean,
|
||||
data:
|
||||
| TypedInternalResponse<
|
||||
R,
|
||||
T,
|
||||
NitroFetchOptions<R> extends O ? 'get' : ExtractedRouteMethod<R, O>
|
||||
>
|
||||
| undefined
|
||||
) => Promise<void>;
|
||||
type RevertFn<T> = (success: boolean, data: T | undefined) => Promise<void>;
|
||||
|
||||
type SubmitOpts<
|
||||
R extends NitroFetchRequest,
|
||||
T = unknown,
|
||||
O extends NitroFetchOptions<R> = NitroFetchOptions<R>,
|
||||
> = {
|
||||
revert: RevertFn<R, T, O>;
|
||||
type SubmitOpts<T> = {
|
||||
revert: RevertFn<T>;
|
||||
successMsg?: string;
|
||||
noSuccessToast?: boolean;
|
||||
};
|
||||
|
||||
export function useSubmit<
|
||||
R extends NitroFetchRequest,
|
||||
O extends NitroFetchOptions<R> & { body?: never },
|
||||
T = unknown,
|
||||
>(url: R, options: O, opts: SubmitOpts<R, T, O>) {
|
||||
type Body = Record<string, unknown> | null | undefined;
|
||||
|
||||
export function useSubmit<T>(
|
||||
fetcher: (data: Body) => Promise<T>,
|
||||
opts: SubmitOpts<T>
|
||||
) {
|
||||
const toast = useToast();
|
||||
|
||||
return async (data: unknown) => {
|
||||
return async (data: Body) => {
|
||||
try {
|
||||
const res = await $fetch(url, {
|
||||
...options,
|
||||
body: data,
|
||||
});
|
||||
const res = await fetcher(data);
|
||||
|
||||
if (!opts.noSuccessToast) {
|
||||
toast.showToast({
|
||||
|
||||
@@ -29,5 +29,7 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const loggedIn = computed(() => route.path !== '/login');
|
||||
const loggedIn = computed(
|
||||
() => route.path !== '/login' && route.path !== '/login/2fa'
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -9,8 +9,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const authStore = useAuthStore();
|
||||
authStore.userData = await authStore.getSession(event);
|
||||
|
||||
// skip login if already logged in
|
||||
if (to.path === '/login') {
|
||||
const isLoginRoute = to.path === '/login' || to.path === '/login/2fa';
|
||||
|
||||
// skip login pages if already logged in
|
||||
if (isLoginRoute) {
|
||||
if (authStore.userData?.username) {
|
||||
return navigateTo('/', { redirectCode: 302 });
|
||||
}
|
||||
|
||||
@@ -121,10 +121,11 @@ const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, {
|
||||
const data = toRef(_data.value);
|
||||
|
||||
const _submit = useSubmit(
|
||||
`/api/admin/userconfig`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/admin/userconfig`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{ revert }
|
||||
);
|
||||
|
||||
|
||||
@@ -46,10 +46,11 @@ const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
|
||||
const data = toRef(_data.value);
|
||||
|
||||
const _submit = useSubmit(
|
||||
`/api/admin/general`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/admin/general`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{ revert }
|
||||
);
|
||||
|
||||
|
||||
@@ -40,10 +40,11 @@ const { data: _data, refresh } = await useFetch(`/api/admin/hooks`, {
|
||||
const data = toRef(_data.value);
|
||||
|
||||
const _submit = useSubmit(
|
||||
`/api/admin/hooks`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/admin/hooks`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{ revert }
|
||||
);
|
||||
|
||||
|
||||
@@ -145,8 +145,8 @@
|
||||
>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('admin.interface.changeCidr')"
|
||||
class="w-full"
|
||||
tabindex="-1"
|
||||
class="inline-block w-full"
|
||||
as="span"
|
||||
/>
|
||||
</AdminCidrDialog>
|
||||
<AdminRestartInterfaceDialog
|
||||
@@ -155,8 +155,8 @@
|
||||
>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('admin.interface.restart')"
|
||||
class="w-full"
|
||||
tabindex="-1"
|
||||
class="inline-block w-full"
|
||||
as="span"
|
||||
/>
|
||||
</AdminRestartInterfaceDialog>
|
||||
</FormGroup>
|
||||
@@ -176,10 +176,11 @@ const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
|
||||
const data = toRef(_data.value);
|
||||
|
||||
const _submit = useSubmit(
|
||||
`/api/admin/interface`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/admin/interface`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success) => {
|
||||
await revert();
|
||||
@@ -201,10 +202,11 @@ async function revert() {
|
||||
}
|
||||
|
||||
const _changeCidr = useSubmit(
|
||||
`/api/admin/interface/cidr`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/admin/interface/cidr`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert,
|
||||
successMsg: t('admin.interface.cidrSuccess'),
|
||||
@@ -216,10 +218,11 @@ async function changeCidr(ipv4Cidr: string, ipv6Cidr: string) {
|
||||
}
|
||||
|
||||
const _restartInterface = useSubmit(
|
||||
`/api/admin/interface/restart`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/admin/interface/restart`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert,
|
||||
successMsg: t('admin.interface.restartSuccess'),
|
||||
|
||||
@@ -188,9 +188,7 @@
|
||||
>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('client.delete')"
|
||||
class="w-full"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="inline-block w-full"
|
||||
as="span"
|
||||
/>
|
||||
</ClientsDeleteDialog>
|
||||
@@ -200,9 +198,7 @@
|
||||
>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('client.viewConfig')"
|
||||
class="w-full"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="inline-block w-full"
|
||||
as="span"
|
||||
/>
|
||||
</ClientsConfigDialog>
|
||||
@@ -225,10 +221,11 @@ const { data: _data, refresh } = await useFetch(`/api/client/${id}`, {
|
||||
const data = toRef(_data.value);
|
||||
|
||||
const _submit = useSubmit(
|
||||
`/api/client/${id}`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/client/${id}`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success) => {
|
||||
if (success) {
|
||||
@@ -250,10 +247,11 @@ async function revert() {
|
||||
}
|
||||
|
||||
const _deleteClient = useSubmit(
|
||||
`/api/client/${id}`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/client/${id}`, {
|
||||
method: 'delete',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
await navigateTo('/');
|
||||
|
||||
+3
-114
@@ -2,9 +2,8 @@
|
||||
<main>
|
||||
<UiBanner />
|
||||
<HeaderInsecure />
|
||||
<form
|
||||
<div
|
||||
class="mx-auto mt-10 flex w-64 flex-col gap-5 overflow-hidden rounded-md bg-white p-5 text-gray-700 shadow dark:bg-neutral-700 dark:text-neutral-200"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
@@ -13,117 +12,7 @@
|
||||
<IconsAvatar class="m-5 h-10 w-10 text-white dark:text-white" />
|
||||
</div>
|
||||
|
||||
<BaseInput
|
||||
v-model="username"
|
||||
type="text"
|
||||
:placeholder="$t('general.username')"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
name="username"
|
||||
/>
|
||||
|
||||
<BaseInput
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
:placeholder="$t('general.password')"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<BaseInput
|
||||
v-if="totpRequired"
|
||||
v-model="totp"
|
||||
type="text"
|
||||
name="totp"
|
||||
:placeholder="$t('general.2faCode')"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
pattern="\d{6}"
|
||||
/>
|
||||
|
||||
<label
|
||||
class="flex gap-2 whitespace-nowrap"
|
||||
:title="$t('login.rememberMeDesc')"
|
||||
>
|
||||
<BaseSwitch v-model="remember" />
|
||||
<span class="text-sm">{{ $t('login.rememberMe') }}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="rounded py-2 text-sm text-white shadow transition dark:text-white"
|
||||
:class="{
|
||||
'cursor-pointer bg-red-800 hover:bg-red-700 dark:bg-red-800 dark:hover:bg-red-700':
|
||||
password && username,
|
||||
'cursor-not-allowed bg-gray-200 dark:bg-neutral-800':
|
||||
!password || !username,
|
||||
}"
|
||||
>
|
||||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" />
|
||||
<span v-else>{{ $t('login.signIn') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const toast = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const authenticating = ref(false);
|
||||
const remember = ref(false);
|
||||
const username = ref<string>('');
|
||||
const password = ref<string>('');
|
||||
const totpRequired = ref(false);
|
||||
const totp = ref<string>('');
|
||||
|
||||
const _submit = useSubmit(
|
||||
'/api/session',
|
||||
{
|
||||
method: 'post',
|
||||
},
|
||||
{
|
||||
revert: async (success, data) => {
|
||||
if (success) {
|
||||
if (data?.status === 'success') {
|
||||
await navigateTo('/');
|
||||
} else if (data?.status === 'TOTP_REQUIRED') {
|
||||
authenticating.value = false;
|
||||
totpRequired.value = true;
|
||||
toast.showToast({
|
||||
title: t('general.2fa'),
|
||||
message: t('login.2faRequired'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
} else if (data?.status === 'INVALID_TOTP_CODE') {
|
||||
authenticating.value = false;
|
||||
totp.value = '';
|
||||
toast.showToast({
|
||||
title: t('general.2fa'),
|
||||
message: t('login.2faWrong'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
authenticating.value = false;
|
||||
password.value = '';
|
||||
},
|
||||
noSuccessToast: true,
|
||||
}
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
if (!username.value || !password.value || authenticating.value) return;
|
||||
|
||||
authenticating.value = true;
|
||||
|
||||
return _submit({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
remember: remember.value,
|
||||
totpCode: totpRequired.value ? totp.value : undefined,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<form class="flex flex-col gap-5" @submit.prevent="submit">
|
||||
<BaseInput
|
||||
v-model="totp"
|
||||
type="text"
|
||||
name="totp"
|
||||
:placeholder="$t('general.2faCode')"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
pattern="\d{6}"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<button
|
||||
class="rounded bg-red-800 py-2 text-sm text-white shadow transition hover:bg-red-700 disabled:cursor-not-allowed disabled:bg-gray-200 dark:bg-red-800 dark:text-white dark:hover:bg-red-700 disabled:dark:bg-neutral-800"
|
||||
:disabled="!totp || authenticating"
|
||||
>
|
||||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" />
|
||||
<span v-else>{{ $t('general.continue') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border-2 border-gray-100 py-2 text-sm text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white dark:border-neutral-600 dark:text-neutral-200"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ $t('dialog.cancel') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const toast = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const authenticating = ref(false);
|
||||
const totp = ref<string>('');
|
||||
|
||||
const { error } = await useFetch('/api/auth/pending');
|
||||
if (error.value) {
|
||||
await navigateTo('/login');
|
||||
}
|
||||
|
||||
const _submit = useSubmit(
|
||||
(data) =>
|
||||
$fetch('/api/auth/verify-2fa', {
|
||||
method: 'post',
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success, data) => {
|
||||
if (success) {
|
||||
if (data?.status === 'success') {
|
||||
await navigateTo('/');
|
||||
return;
|
||||
} else if (data?.status === 'INVALID_TOTP_CODE') {
|
||||
authenticating.value = false;
|
||||
totp.value = '';
|
||||
toast.showToast({
|
||||
title: t('general.2fa'),
|
||||
message: t('login.2faWrong'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
} else if (data?.status === 'PENDING_LOGIN_EXPIRED') {
|
||||
toast.showToast({
|
||||
title: t('general.2fa'),
|
||||
message: t('login.loginExpired'),
|
||||
type: 'error',
|
||||
});
|
||||
await navigateTo('/login');
|
||||
return;
|
||||
}
|
||||
}
|
||||
authenticating.value = false;
|
||||
},
|
||||
noSuccessToast: true,
|
||||
}
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
if (!totp.value || authenticating.value) return;
|
||||
|
||||
authenticating.value = true;
|
||||
return _submit({ totpCode: totp.value });
|
||||
}
|
||||
|
||||
const _cancel = useSubmit(
|
||||
(data) =>
|
||||
$fetch('/api/auth/cancel', {
|
||||
method: 'post',
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
await navigateTo('/login');
|
||||
},
|
||||
noSuccessToast: true,
|
||||
}
|
||||
);
|
||||
|
||||
async function cancel() {
|
||||
return _cancel({});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5">
|
||||
<div
|
||||
v-if="authMethods && authMethods.oauthEnabled"
|
||||
class="flex flex-col gap-5"
|
||||
>
|
||||
<UiLoginOauthButton
|
||||
v-for="(info, provider) in authMethods.providers"
|
||||
:key="provider"
|
||||
:provider="provider"
|
||||
:info="info"
|
||||
/>
|
||||
|
||||
<!-- Divider -->
|
||||
<div
|
||||
v-if="authMethods.oauthEnabled && !authMethods.passwordDisabled"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div>
|
||||
<span class="text-xs text-gray-500 dark:text-neutral-400">
|
||||
{{ $t('login.or') }}
|
||||
</span>
|
||||
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Classic Login Form -->
|
||||
<form
|
||||
v-if="!authMethods?.passwordDisabled"
|
||||
class="flex flex-col gap-5"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="username"
|
||||
type="text"
|
||||
:placeholder="$t('general.username')"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
name="username"
|
||||
/>
|
||||
|
||||
<BaseInput
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
:placeholder="$t('general.password')"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<label
|
||||
class="flex gap-2 whitespace-nowrap"
|
||||
:title="$t('login.rememberMeDesc')"
|
||||
>
|
||||
<BaseSwitch v-model="remember" />
|
||||
<span class="text-sm">{{ $t('login.rememberMe') }}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="rounded bg-red-800 py-2 text-sm text-white shadow transition hover:bg-red-700 disabled:cursor-not-allowed disabled:bg-gray-200 dark:bg-red-800 dark:text-white dark:hover:bg-red-700 disabled:dark:bg-neutral-800"
|
||||
:disabled="!password || !username"
|
||||
>
|
||||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" />
|
||||
<span v-else>{{ $t('login.signIn') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const toast = useToast();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const authenticating = ref(false);
|
||||
const remember = ref(false);
|
||||
const username = ref<string>('');
|
||||
const password = ref<string>('');
|
||||
|
||||
const { data: authMethods } = await useFetch('/api/auth/methods');
|
||||
|
||||
watchEffect(() => {
|
||||
const autoLauchQuery =
|
||||
typeof route.query.auto_launch === 'string' && !!route.query.auto_launch
|
||||
? route.query.auto_launch
|
||||
: undefined;
|
||||
|
||||
if (authMethods.value?.autoLaunchProvider && !autoLauchQuery) {
|
||||
navigateTo(`/api/auth/${authMethods.value.autoLaunchProvider}`, {
|
||||
external: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
autoLauchQuery &&
|
||||
autoLauchQuery !== 'false' &&
|
||||
authMethods.value?.providers?.[autoLauchQuery as OAUTH_PROVIDER]
|
||||
) {
|
||||
navigateTo(`/api/auth/${autoLauchQuery}`, {
|
||||
external: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const _submit = useSubmit(
|
||||
(data) =>
|
||||
$fetch('/api/auth/password', {
|
||||
method: 'post',
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success, data) => {
|
||||
if (success) {
|
||||
if (data?.status === 'success') {
|
||||
await navigateTo('/');
|
||||
} else if (data?.status === 'TOTP_REQUIRED') {
|
||||
authenticating.value = false;
|
||||
await navigateTo('/login/2fa');
|
||||
return;
|
||||
} else if (data?.status === 'INVALID_TOTP_CODE') {
|
||||
authenticating.value = false;
|
||||
toast.showToast({
|
||||
title: t('general.2fa'),
|
||||
message: t('login.2faWrong'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
authenticating.value = false;
|
||||
password.value = '';
|
||||
},
|
||||
noSuccessToast: true,
|
||||
}
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
if (!username.value || !password.value || authenticating.value) return;
|
||||
|
||||
authenticating.value = true;
|
||||
|
||||
return _submit({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
remember: remember.value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
+113
-24
@@ -27,6 +27,7 @@
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('general.password') }}</FormHeading>
|
||||
<FormPasswordField
|
||||
v-if="hasPassword"
|
||||
id="current-password"
|
||||
v-model="currentPassword"
|
||||
autocomplete="current-password"
|
||||
@@ -46,24 +47,25 @@
|
||||
/>
|
||||
<FormSecondaryActionField
|
||||
type="submit"
|
||||
:label="$t('general.updatePassword')"
|
||||
:label="
|
||||
hasPassword
|
||||
? $t('general.updatePassword')
|
||||
: $t('general.addPassword')
|
||||
"
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormElement>
|
||||
<FormElement @submit.prevent>
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('general.2fa') }}</FormHeading>
|
||||
<div
|
||||
v-if="!authStore.userData?.totpVerified && !twofa"
|
||||
class="col-span-2 flex flex-col"
|
||||
>
|
||||
<template v-if="!authStore.userData?.totpVerified && !twofa">
|
||||
<FormSecondaryActionField
|
||||
:label="$t('me.enable2fa')"
|
||||
@click="setup2fa"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="!authStore.userData?.totpVerified && twofa"
|
||||
v-else-if="!authStore.userData?.totpVerified && twofa"
|
||||
class="col-span-2"
|
||||
>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -93,7 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authStore.userData?.totpVerified"
|
||||
v-else-if="authStore.userData?.totpVerified"
|
||||
class="col-span-2 flex flex-col gap-2"
|
||||
>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -113,6 +115,60 @@
|
||||
</div>
|
||||
</FormGroup>
|
||||
</FormElement>
|
||||
<FormElement v-if="authMethods?.oauthEnabled" @submit.prevent>
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('general.externalAuth') }}</FormHeading>
|
||||
<template v-if="oauthProvider && oauthProviderInfo?.enabled">
|
||||
<div class="flex items-center gap-2">
|
||||
<IconsBrandsProvider
|
||||
:provider="oauthProvider"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<span>
|
||||
{{ $t('me.linkedWith', [oauthProviderInfo.friendlyName]) }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('me.unlinkOauth')"
|
||||
class="w-full"
|
||||
:disabled="!hasPassword"
|
||||
@click="unlinkOauth"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="oauthProvider && !oauthProviderInfo?.enabled">
|
||||
<span class="flex items-center">
|
||||
{{ $t('me.providerDisabled') }}
|
||||
</span>
|
||||
<div>
|
||||
<FormSecondaryActionField
|
||||
:label="$t('me.unlinkOauth')"
|
||||
class="w-full"
|
||||
:disabled="!hasPassword"
|
||||
@click="unlinkOauth"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center">
|
||||
<span>{{ $t('me.linkOauth') }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<BaseFormSecondaryButton
|
||||
v-for="(info, provider) in authMethods.providers"
|
||||
:key="provider"
|
||||
class="flex items-center gap-2 p-2"
|
||||
as="a"
|
||||
:href="`/api/auth/${provider}?link=true`"
|
||||
>
|
||||
<IconsBrandsProvider :provider="provider" class="h-4 w-4" />
|
||||
<span>{{ info.friendlyName }}</span>
|
||||
</BaseFormSecondaryButton>
|
||||
</div>
|
||||
</template>
|
||||
</FormGroup>
|
||||
</FormElement>
|
||||
</PanelBody>
|
||||
</Panel>
|
||||
</main>
|
||||
@@ -123,14 +179,25 @@ import { encodeQR } from 'qr';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const { data: authMethods } = await useFetch('/api/auth/methods');
|
||||
|
||||
const name = ref(authStore.userData?.name);
|
||||
const email = ref(authStore.userData?.email);
|
||||
const hasPassword = computed(() => authStore.userData?.hasPassword);
|
||||
const oauthProvider = computed(() => authStore.userData?.oauthProvider);
|
||||
const oauthProviderInfo = computed(() => {
|
||||
if (!authStore.userData?.oauthProvider) {
|
||||
return null;
|
||||
}
|
||||
return authMethods.value?.providers?.[authStore.userData.oauthProvider];
|
||||
});
|
||||
|
||||
const _submit = useSubmit(
|
||||
`/api/me`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/me`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: () => {
|
||||
return authStore.update();
|
||||
@@ -147,22 +214,24 @@ const newPassword = ref('');
|
||||
const confirmPassword = ref('');
|
||||
|
||||
const _updatePassword = useSubmit(
|
||||
`/api/me/password`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/me/password`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
currentPassword.value = '';
|
||||
newPassword.value = '';
|
||||
confirmPassword.value = '';
|
||||
return authStore.update();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function updatePassword() {
|
||||
return _updatePassword({
|
||||
currentPassword: currentPassword.value,
|
||||
currentPassword: hasPassword.value ? currentPassword.value : null,
|
||||
newPassword: newPassword.value,
|
||||
confirmPassword: confirmPassword.value,
|
||||
});
|
||||
@@ -171,10 +240,11 @@ function updatePassword() {
|
||||
const twofa = ref<{ key: string; qrcode: string } | null>(null);
|
||||
|
||||
const _setup2fa = useSubmit(
|
||||
`/api/me/totp`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/me/totp`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success, data) => {
|
||||
if (success && data?.type === 'setup') {
|
||||
@@ -199,10 +269,11 @@ async function setup2fa() {
|
||||
const code = ref<string>('');
|
||||
|
||||
const _enable2fa = useSubmit(
|
||||
`/api/me/totp`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/me/totp`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success, data) => {
|
||||
if (success && data?.type === 'created') {
|
||||
@@ -224,10 +295,11 @@ async function enable2fa() {
|
||||
const disable2faPassword = ref('');
|
||||
|
||||
const _disable2fa = useSubmit(
|
||||
`/api/me/totp`,
|
||||
{
|
||||
(data) =>
|
||||
$fetch(`/api/me/totp`, {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success, data) => {
|
||||
if (success && data?.type === 'deleted') {
|
||||
@@ -244,4 +316,21 @@ async function disable2fa() {
|
||||
currentPassword: disable2faPassword.value,
|
||||
});
|
||||
}
|
||||
|
||||
const _unlinkOauth = useSubmit(
|
||||
(data) =>
|
||||
$fetch(`/api/auth/unlink`, {
|
||||
method: 'post',
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async () => {
|
||||
return authStore.update();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
async function unlinkOauth() {
|
||||
return _unlinkOauth({});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -50,10 +50,11 @@ const password = ref<string>('');
|
||||
const confirmPassword = ref<string>('');
|
||||
|
||||
const _submit = useSubmit(
|
||||
'/api/setup/2',
|
||||
{
|
||||
(data) =>
|
||||
$fetch('/api/setup/2', {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success) => {
|
||||
if (success) {
|
||||
|
||||
@@ -43,10 +43,11 @@ const host = ref<null | string>(null);
|
||||
const port = ref<number>(51820);
|
||||
|
||||
const _submit = useSubmit(
|
||||
'/api/setup/4',
|
||||
{
|
||||
(data) =>
|
||||
$fetch('/api/setup/4', {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success) => {
|
||||
if (success) {
|
||||
|
||||
@@ -36,10 +36,11 @@ function onChangeFile(evt: Event) {
|
||||
}
|
||||
|
||||
const _submit = useSubmit(
|
||||
'/api/setup/migrate',
|
||||
{
|
||||
(data) =>
|
||||
$fetch('/api/setup/migrate', {
|
||||
method: 'post',
|
||||
},
|
||||
body: data,
|
||||
}),
|
||||
{
|
||||
revert: async (success) => {
|
||||
if (success) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { H3Event } from 'h3';
|
||||
import type { SharedPublicUser } from '~~/shared/utils/permissions';
|
||||
|
||||
export const useAuthStore = defineStore('Auth', () => {
|
||||
const userData = useState<SharedPublicUser | null>('user-data', () => null);
|
||||
|
||||
@@ -31,9 +31,12 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
const clients = ref<null | LocalClient[]>(null);
|
||||
const clientsPersist = ref<Record<string, ClientPersist>>({});
|
||||
|
||||
const searchParams = ref({
|
||||
filter: undefined as string | undefined,
|
||||
});
|
||||
const filter = ref<string | undefined>(undefined);
|
||||
|
||||
const searchParams = computed(() => ({
|
||||
filter: filter.value,
|
||||
sort: globalStore.sortClient,
|
||||
}));
|
||||
|
||||
const { data: _clients, refresh: _refresh } = useFetch('/api/client', {
|
||||
method: 'get',
|
||||
@@ -43,7 +46,7 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
// TODO: rewrite
|
||||
async function refresh({ updateCharts = false } = {}) {
|
||||
await _refresh();
|
||||
let transformedClients = _clients.value?.map((client) => {
|
||||
const transformedClients = _clients.value?.map((client) => {
|
||||
let avatar = undefined;
|
||||
if (client.name.includes('@') && client.name.includes('.')) {
|
||||
avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`;
|
||||
@@ -126,21 +129,12 @@ export const useClientsStore = defineStore('Clients', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: move sort to backend
|
||||
if (transformedClients !== undefined) {
|
||||
transformedClients = sortByProperty(
|
||||
transformedClients,
|
||||
'name',
|
||||
globalStore.sortClient
|
||||
);
|
||||
}
|
||||
|
||||
clients.value = transformedClients ?? null;
|
||||
}
|
||||
|
||||
function setSearchQuery(filter: string) {
|
||||
function setSearchQuery(query: string) {
|
||||
clients.value = null;
|
||||
searchParams.value.filter = filter || undefined;
|
||||
filter.value = query || undefined;
|
||||
}
|
||||
|
||||
return { clients, clientsPersist, refresh, _clients, setSearchQuery };
|
||||
|
||||
@@ -6,7 +6,7 @@ export const useGlobalStore = defineStore('Global', () => {
|
||||
}
|
||||
);
|
||||
|
||||
const sortClient = ref(true); // Sort clients by name, true = asc, false = desc
|
||||
const sortClient = ref<'asc' | 'desc'>('asc');
|
||||
|
||||
const uiShowCharts = useCookie<boolean>('uiShowCharts', {
|
||||
default: () => false,
|
||||
|
||||
@@ -4,3 +4,5 @@ export type ToastParams = {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type OAUTH_PROVIDER = 'google' | 'github' | 'oidc';
|
||||
|
||||
@@ -2,8 +2,8 @@ import { defineCommand } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db, schema } from '../db';
|
||||
import { hashPassword } from '../../server/utils/password';
|
||||
import { db, schema } from '#cli/db';
|
||||
import { hashPassword } from '#server/utils/password';
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
|
||||
+46
-13
@@ -1,25 +1,58 @@
|
||||
// @ts-check
|
||||
|
||||
import { writeFile, mkdir } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
|
||||
esbuild.build({
|
||||
const isTrace =
|
||||
process.argv.slice(2).findIndex((arg) => arg === '--trace') !== -1;
|
||||
|
||||
/**
|
||||
* Writes files with needed imports to `../node_modules/.cache/wg-easy/trace.mjs`
|
||||
*
|
||||
* @param {import('esbuild').Metafile} metafile
|
||||
*/
|
||||
async function writeTrace(metafile) {
|
||||
const paths = [
|
||||
...new Set(
|
||||
Object.values(metafile.outputs).flatMap((v) =>
|
||||
v.imports.map((v) => v.path)
|
||||
)
|
||||
),
|
||||
];
|
||||
const imports = paths.map((v) => `import '${v}';`).join('\n');
|
||||
|
||||
const folderUrl = new URL('../node_modules/.cache/wg-easy/', import.meta.url);
|
||||
const folder = fileURLToPath(folderUrl);
|
||||
const filePath = fileURLToPath(new URL('./trace.mjs', folderUrl));
|
||||
|
||||
await mkdir(folder, { recursive: true });
|
||||
await writeFile(filePath, imports);
|
||||
}
|
||||
|
||||
function getOverrides() {
|
||||
if (isTrace) {
|
||||
return {
|
||||
outfile: undefined,
|
||||
write: false,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = await esbuild.build({
|
||||
entryPoints: [fileURLToPath(new URL('./index.ts', import.meta.url))],
|
||||
bundle: true,
|
||||
outfile: fileURLToPath(new URL('../.output/server/cli.mjs', import.meta.url)),
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
plugins: [
|
||||
{
|
||||
name: 'make-all-packages-external',
|
||||
setup(build) {
|
||||
let filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
||||
build.onResolve({ filter }, (args) => ({
|
||||
path: args.path,
|
||||
external: true,
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
packages: 'external',
|
||||
logLevel: 'info',
|
||||
metafile: true,
|
||||
...getOverrides(),
|
||||
});
|
||||
|
||||
if (isTrace) {
|
||||
await writeTrace(result.metafile);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineCommand } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
|
||||
import { db } from '../db';
|
||||
import { db } from '#cli/db';
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { defineCommand } from 'citty';
|
||||
import { consola } from 'consola';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { wg } from '../../server/utils/wgHelper';
|
||||
import { encodeQRCodeTerm } from '../../server/utils/qr';
|
||||
import { db, schema } from '../db';
|
||||
import { db, schema } from '#cli/db';
|
||||
import { wg } from '#server/utils/wgHelper';
|
||||
import { encodeQRCodeTerm } from '#server/utils/qr';
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
|
||||
import * as schema from '../server/database/schema';
|
||||
import * as schema from '#server/database/schema';
|
||||
|
||||
//const client = createClient({ url: 'file:../data/wg-easy.db' });
|
||||
const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' });
|
||||
|
||||
+4
-3
@@ -6,9 +6,10 @@ import { defineCommand, runMain } from 'citty';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
// Commands
|
||||
import dbAdminReset from './admin/reset';
|
||||
import clientsList from './clients/list';
|
||||
import clientsQr from './clients/qr';
|
||||
import clientsList from '#cli/clients/list';
|
||||
import clientsQr from '#cli/clients/qr';
|
||||
import dbAdminReset from '#cli/admin/reset';
|
||||
|
||||
const subCommands = [dbAdminReset, clientsList, clientsQr] as const;
|
||||
|
||||
// from citty
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"#cli/*": ["./*"],
|
||||
"#server/*": ["../server/*"],
|
||||
"#db/*": ["../server/database/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
import withNuxt from './.nuxt/eslint.config.mjs';
|
||||
|
||||
export default withNuxt([
|
||||
{
|
||||
rules: {
|
||||
'import/order': 'warn',
|
||||
'import/order': ['warn', { 'newlines-between': 'always' }],
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
|
||||
@@ -4,6 +4,7 @@ import uk from './locales/uk.json';
|
||||
import fr from './locales/fr.json';
|
||||
import de from './locales/de.json';
|
||||
import it from './locales/it.json';
|
||||
import ja from './locales/ja.json';
|
||||
import ru from './locales/ru.json';
|
||||
import zhhk from './locales/zh-HK.json';
|
||||
import zhcn from './locales/zh-CN.json';
|
||||
@@ -17,6 +18,7 @@ import id from './locales/id.json';
|
||||
import nl from './locales/nl.json';
|
||||
import nb from './locales/nb.json';
|
||||
import bg from './locales/bg.json';
|
||||
import hi from './locales/hi.json';
|
||||
import gl from './locales/gl.json';
|
||||
import cs from './locales/cs.json';
|
||||
import vi from './locales/vi.json';
|
||||
@@ -31,6 +33,7 @@ export default defineI18nConfig(() => ({
|
||||
fr,
|
||||
de,
|
||||
it,
|
||||
ja,
|
||||
ru,
|
||||
'zh-HK': zhhk,
|
||||
'zh-CN': zhcn,
|
||||
@@ -44,6 +47,7 @@ export default defineI18nConfig(() => ({
|
||||
nl,
|
||||
nb,
|
||||
bg,
|
||||
hi,
|
||||
gl,
|
||||
cs,
|
||||
vi,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
tryCookieLocale,
|
||||
tryHeaderLocale,
|
||||
tryQueryLocale,
|
||||
} from '@intlify/utils/h3';
|
||||
|
||||
import { defineI18nLocaleDetector } from '#imports';
|
||||
|
||||
export default defineI18nLocaleDetector((event, config) => {
|
||||
const query = tryQueryLocale(event, { lang: '' });
|
||||
if (query) {
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
"2faKey": "TOTP Key",
|
||||
"2faCodeDesc": "Enter the code from your authenticator app.",
|
||||
"disable2fa": "Disable Two Factor Authentication",
|
||||
"disable2faDesc": "Enter your password to disable Two Factor Authentication."
|
||||
"disable2faDesc": "Enter your password to disable Two Factor Authentication.",
|
||||
"linkOauth": "Link your account with an external provider",
|
||||
"unlinkOauth": "Unlink",
|
||||
"linkedWith": "Linked with {0}",
|
||||
"providerDisabled": "Your currently linked provider is not enabled"
|
||||
},
|
||||
"general": {
|
||||
"name": "Name",
|
||||
@@ -28,6 +32,7 @@
|
||||
"password": "Password",
|
||||
"newPassword": "New Password",
|
||||
"updatePassword": "Update Password",
|
||||
"addPassword": "Add Password",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "Allowed IPs",
|
||||
"dns": "DNS",
|
||||
@@ -41,7 +46,8 @@
|
||||
"confirmPassword": "Confirm Password",
|
||||
"loading": "Loading...",
|
||||
"2fa": "Two Factor Authentication",
|
||||
"2faCode": "TOTP Code"
|
||||
"2faCode": "TOTP Code",
|
||||
"externalAuth": "External Authentication"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Welcome to your first setup of wg-easy",
|
||||
@@ -72,11 +78,14 @@
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Sign In",
|
||||
"signInWith": "Sign in with {0}",
|
||||
"or": "or",
|
||||
"rememberMe": "Remember me",
|
||||
"rememberMeDesc": "Stay logged after closing the browser",
|
||||
"insecure": "You can't log in with an insecure connection. Use HTTPS.",
|
||||
"2faRequired": "Two Factor Authentication is required",
|
||||
"2faWrong": "Two Factor Authentication is wrong"
|
||||
"2faWrong": "Two Factor Authentication is wrong",
|
||||
"loginExpired": "Login expired. Try again"
|
||||
},
|
||||
"client": {
|
||||
"empty": "There are no clients yet.",
|
||||
@@ -196,7 +205,9 @@
|
||||
"validBoolean": "{0} must be a valid boolean",
|
||||
"validArray": "{0} must be a valid array",
|
||||
"stringMin": "{0} must be at least {1} Character",
|
||||
"numberMin": "{0} must be at least {1}"
|
||||
"stringMax": "{0} must be at most {1} Character",
|
||||
"numberMin": "{0} must be at least {1}",
|
||||
"numberMax": "{0} must be at most {1}"
|
||||
},
|
||||
"client": {
|
||||
"id": "Client ID",
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"pages": {
|
||||
"me": "खाता",
|
||||
"clients": "क्लाइंट",
|
||||
"admin": {
|
||||
"panel": "एडमिन पैनल",
|
||||
"general": "सामान्य",
|
||||
"config": "कॉन्फ़िगरेशन",
|
||||
"interface": "इंटरफ़ेस",
|
||||
"hooks": "हुक्स"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "ई-मेल"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "वर्तमान पासवर्ड",
|
||||
"enable2fa": "दो-कारक प्रमाणीकरण सक्षम करें",
|
||||
"enable2faDesc": "अपने प्रमाणक ऐप से QR कोड स्कैन करें या कुंजी मैन्युअल रूप से दर्ज करें।",
|
||||
"2faKey": "TOTP कुंजी",
|
||||
"2faCodeDesc": "अपने प्रमाणक ऐप से कोड दर्ज करें।",
|
||||
"disable2fa": "दो-कारक प्रमाणीकरण अक्षम करें",
|
||||
"disable2faDesc": "दो-कारक प्रमाणीकरण अक्षम करने के लिए अपना पासवर्ड दर्ज करें।"
|
||||
},
|
||||
"general": {
|
||||
"name": "नाम",
|
||||
"username": "उपयोगकर्ता नाम",
|
||||
"password": "पासवर्ड",
|
||||
"newPassword": "नया पासवर्ड",
|
||||
"updatePassword": "पासवर्ड अपडेट करें",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "अनुमत IPs",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "स्थायी कीपअलाइव",
|
||||
"logout": "लॉगआउट",
|
||||
"continue": "जारी रखें",
|
||||
"host": "होस्ट",
|
||||
"port": "पोर्ट",
|
||||
"yes": "हाँ",
|
||||
"no": "नहीं",
|
||||
"confirmPassword": "पासवर्ड की पुष्टि करें",
|
||||
"loading": "लोड हो रहा है...",
|
||||
"2fa": "दो-कारक प्रमाणीकरण",
|
||||
"2faCode": "TOTP कोड"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "wg-easy की प्रारंभिक सेटअप में आपका स्वागत है",
|
||||
"welcomeDesc": "आपने किसी भी Linux होस्ट पर WireGuard इंस्टॉल और प्रबंधित करने का सबसे आसान तरीका खोज लिया है",
|
||||
"existingSetup": "क्या आपके पास पहले से कोई सेटअप है?",
|
||||
"createAdminDesc": "कृपया पहले एडमिन उपयोगकर्ता नाम और एक मज़बूत सुरक्षित पासवर्ड दर्ज करें। इस जानकारी का उपयोग प्रशासन पैनल में लॉग इन करने के लिए किया जाएगा।",
|
||||
"setupConfigDesc": "कृपया होस्ट और पोर्ट जानकारी दर्ज करें। इसका उपयोग उनके डिवाइस पर WireGuard सेटअप करते समय क्लाइंट कॉन्फ़िगरेशन के लिए किया जाएगा।",
|
||||
"setupMigrationDesc": "यदि आप अपने पिछले wg-easy संस्करण से डेटा माइग्रेट करना चाहते हैं तो कृपया बैकअप फ़ाइल प्रदान करें।",
|
||||
"upload": "अपलोड",
|
||||
"migration": "बैकअप से पुनर्स्थापित करें:",
|
||||
"createAccount": "खाता बनाएं",
|
||||
"successful": "सेटअप सफल रहा",
|
||||
"hostDesc": "सार्वजनिक होस्टनाम जिससे क्लाइंट कनेक्ट होंगे",
|
||||
"portDesc": "सार्वजनिक UDP पोर्ट जिससे क्लाइंट कनेक्ट होंगे और WireGuard सुनेगा"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "एक अपडेट उपलब्ध है!",
|
||||
"update": "अपडेट करें"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "डार्क थीम",
|
||||
"light": "लाइट थीम",
|
||||
"system": "सिस्टम थीम"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "चार्ट दिखाएं/छिपाएं",
|
||||
"donate": "दान करें"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "साइन इन",
|
||||
"rememberMe": "मुझे याद रखें",
|
||||
"rememberMeDesc": "ब्राउज़र बंद करने के बाद भी लॉग इन रहें",
|
||||
"insecure": "आप असुरक्षित कनेक्शन से लॉग इन नहीं कर सकते। HTTPS का उपयोग करें।",
|
||||
"2faRequired": "दो-कारक प्रमाणीकरण आवश्यक है",
|
||||
"2faWrong": "दो-कारक प्रमाणीकरण गलत है"
|
||||
},
|
||||
"client": {
|
||||
"empty": "अभी तक कोई क्लाइंट नहीं है।",
|
||||
"newShort": "नया",
|
||||
"sort": "क्रमबद्ध करें",
|
||||
"create": "क्लाइंट बनाएं",
|
||||
"created": "क्लाइंट बनाया गया",
|
||||
"new": "नया क्लाइंट",
|
||||
"name": "नाम",
|
||||
"expireDate": "समाप्ति तिथि",
|
||||
"expireDateDesc": "वह तिथि जिसके बाद क्लाइंट अक्षम हो जाएगा। स्थायी के लिए खाली छोड़ें",
|
||||
"delete": "हटाएं",
|
||||
"deleteClient": "क्लाइंट हटाएं",
|
||||
"deleteDialog1": "क्या आप वाकई हटाना चाहते हैं",
|
||||
"deleteDialog2": "यह क्रिया पूर्ववत नहीं की जा सकती।",
|
||||
"enabled": "सक्षम",
|
||||
"address": "पता",
|
||||
"serverAllowedIps": "सर्वर द्वारा अनुमत IPs",
|
||||
"otlDesc": "छोटा एकबारगी लिंक उत्पन्न करें",
|
||||
"permanent": "स्थायी",
|
||||
"createdOn": "बनाया गया ",
|
||||
"lastSeen": "अंतिम बार देखा गया ",
|
||||
"totalDownload": "कुल डाउनलोड: ",
|
||||
"totalUpload": "कुल अपलोड: ",
|
||||
"newClient": "नया क्लाइंट",
|
||||
"disableClient": "क्लाइंट अक्षम करें",
|
||||
"enableClient": "क्लाइंट सक्षम करें",
|
||||
"noPrivKey": "इस क्लाइंट की कोई ज्ञात निजी कुंजी नहीं है। कॉन्फ़िगरेशन नहीं बना सकते।",
|
||||
"showQR": "QR कोड दिखाएं",
|
||||
"downloadConfig": "कॉन्फ़िगरेशन डाउनलोड करें",
|
||||
"allowedIpsDesc": "कौन से IPs VPN के माध्यम से रूट होंगे (वैश्विक कॉन्फ़िगरेशन को ओवरराइड करता है)",
|
||||
"serverAllowedIpsDesc": "कौन से IPs सर्वर क्लाइंट को रूट करेगा",
|
||||
"mtuDesc": "VPN टनल के लिए अधिकतम ट्रांसमिशन इकाई (पैकेट आकार) सेट करता है",
|
||||
"persistentKeepaliveDesc": "कीप-अलाइव पैकेट के लिए अंतराल (सेकंड में) सेट करता है। 0 इसे अक्षम करता है",
|
||||
"hooks": "हुक्स",
|
||||
"hooksDescription": "हुक्स केवल wg-quick के साथ काम करते हैं",
|
||||
"hooksLeaveEmpty": "केवल wg-quick के लिए। अन्यथा, खाली छोड़ें",
|
||||
"dnsDesc": "DNS सर्वर जिसे क्लाइंट उपयोग करेंगे (वैश्विक कॉन्फ़िगरेशन को ओवरराइड करता है)",
|
||||
"notConnected": "क्लाइंट कनेक्ट नहीं है",
|
||||
"endpoint": "एंडपॉइंट",
|
||||
"endpointDesc": "क्लाइंट का IP पता जहाँ से WireGuard कनेक्शन स्थापित होता है",
|
||||
"search": "क्लाइंट खोजें...",
|
||||
"config": "कॉन्फ़िगरेशन",
|
||||
"viewConfig": "कॉन्फ़िगरेशन देखें",
|
||||
"firewallIps": "फ़ायरवॉल द्वारा अनुमत IPs",
|
||||
"firewallIpsDesc": "गंतव्य IPs/CIDRs जिन तक यह क्लाइंट पहुँच सकता है (सर्वर-साइड नियंत्रण)। अनुमत IPs उपयोग करने के लिए खाली छोड़ें। वैकल्पिक पोर्ट और प्रोटोकॉल फ़िल्टरिंग का समर्थन करता है। सिंटैक्स के लिए दस्तावेज़ देखें।",
|
||||
"downloadPng": "PNG डाउनलोड करें",
|
||||
"copyPng": "PNG कॉपी करें"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "बदलें",
|
||||
"cancel": "रद्द करें",
|
||||
"create": "बनाएं"
|
||||
},
|
||||
"toast": {
|
||||
"success": "सफल",
|
||||
"saved": "सहेजा गया",
|
||||
"error": "त्रुटि",
|
||||
"unknown": "अज्ञात त्रुटि। अधिक जानकारी के लिए कंसोल देखें"
|
||||
},
|
||||
"form": {
|
||||
"actions": "क्रियाएं",
|
||||
"save": "सहेजें",
|
||||
"revert": "पूर्ववत करें",
|
||||
"sectionGeneral": "सामान्य",
|
||||
"sectionAdvanced": "उन्नत",
|
||||
"noItems": "कोई आइटम नहीं",
|
||||
"nullNoItems": "कोई आइटम नहीं। वैश्विक कॉन्फ़िगरेशन उपयोग में है",
|
||||
"add": "जोड़ें"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "सत्र समय समाप्ति",
|
||||
"sessionTimeoutDesc": "रिमेम्बर मी के लिए सत्र अवधि (सेकंड में)",
|
||||
"metrics": "मेट्रिक्स",
|
||||
"metricsPassword": "पासवर्ड",
|
||||
"metricsPasswordDesc": "मेट्रिक्स एंडपॉइंट के लिए बियरर पासवर्ड (पासवर्ड या argon2 हैश)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "JSON फ़ॉर्मेट में मेट्रिक्स का मार्ग",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Prometheus मेट्रिक्स का मार्ग"
|
||||
},
|
||||
"config": {
|
||||
"connection": "कनेक्शन",
|
||||
"hostDesc": "सार्वजनिक होस्टनाम जिससे क्लाइंट कनेक्ट होंगे (कॉन्फ़िगरेशन को अमान्य करता है)",
|
||||
"portDesc": "सार्वजनिक UDP पोर्ट जिससे क्लाइंट कनेक्ट होंगे (कॉन्फ़िगरेशन को अमान्य करता है, शायद आप इंटरफ़ेस पोर्ट भी बदलना चाहें)",
|
||||
"allowedIpsDesc": "क्लाइंट द्वारा उपयोग किए जाने वाले अनुमत IPs (वैश्विक कॉन्फ़िगरेशन)",
|
||||
"dnsDesc": "क्लाइंट द्वारा उपयोग किया जाने वाला DNS सर्वर (वैश्विक कॉन्फ़िगरेशन)",
|
||||
"mtuDesc": "क्लाइंट द्वारा उपयोग किया जाने वाला MTU (केवल नए क्लाइंट के लिए)",
|
||||
"persistentKeepaliveDesc": "सर्वर को कीपअलाइव भेजने का अंतराल सेकंड में। 0 = अक्षम (केवल नए क्लाइंट के लिए)",
|
||||
"suggest": "सुझाएं",
|
||||
"suggestDesc": "होस्ट फ़ील्ड के लिए IP पता या होस्टनाम चुनें"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR बदला गया",
|
||||
"device": "डिवाइस",
|
||||
"deviceDesc": "ईथरनेट डिवाइस जिसके माध्यम से WireGuard ट्रैफ़िक फ़ॉरवर्ड किया जाना चाहिए",
|
||||
"mtuDesc": "MTU जिसे WireGuard उपयोग करेगा",
|
||||
"portDesc": "UDP पोर्ट जिस पर WireGuard सुनेगा (शायद आप कॉन्फ़िग पोर्ट भी बदलना चाहें)",
|
||||
"changeCidr": "CIDR बदलें",
|
||||
"restart": "इंटरफ़ेस रीस्टार्ट करें",
|
||||
"restartDesc": "WireGuard इंटरफ़ेस को रीस्टार्ट करें",
|
||||
"restartWarn": "क्या आप वाकई इंटरफ़ेस रीस्टार्ट करना चाहते हैं? इससे सभी क्लाइंट डिस्कनेक्ट हो जाएंगे।",
|
||||
"restartSuccess": "इंटरफ़ेस रीस्टार्ट हो गया",
|
||||
"firewall": "ट्रैफ़िक फ़िल्टरिंग",
|
||||
"firewallEnabled": "प्रति-क्लाइंट फ़ायरवॉल सक्षम करें",
|
||||
"firewallEnabledDesc": "iptables का उपयोग करके क्लाइंट ट्रैफ़िक को विशिष्ट गंतव्य IPs तक सीमित करें। सक्षम होने पर, प्रत्येक क्लाइंट को अनुमत गंतव्यों के साथ कॉन्फ़िगर किया जा सकता है।"
|
||||
},
|
||||
"introText": "एडमिन पैनल में आपका स्वागत है।\n\nयहाँ आप सामान्य सेटिंग्स, कॉन्फ़िगरेशन, इंटरफ़ेस सेटिंग्स और हुक्स प्रबंधित कर सकते हैं।\n\nसाइडबार में किसी एक अनुभाग को चुनकर शुरू करें।"
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{0} आवश्यक है",
|
||||
"validNumber": "{0} एक वैध संख्या होनी चाहिए",
|
||||
"validNumberRange": "{0} एक वैध संख्या या संख्या श्रेणी होनी चाहिए",
|
||||
"validString": "{0} एक वैध स्ट्रिंग होनी चाहिए",
|
||||
"validBoolean": "{0} एक वैध बूलियन होना चाहिए",
|
||||
"validArray": "{0} एक वैध ऐरे होना चाहिए",
|
||||
"stringMin": "{0} कम से कम {1} अक्षर का होना चाहिए",
|
||||
"numberMin": "{0} कम से कम {1} होना चाहिए"
|
||||
},
|
||||
"client": {
|
||||
"id": "क्लाइंट ID",
|
||||
"name": "नाम",
|
||||
"expiresAt": "समाप्ति तिथि",
|
||||
"address4": "IPv4 पता",
|
||||
"address6": "IPv6 पता",
|
||||
"serverAllowedIps": "सर्वर द्वारा अनुमत IPs",
|
||||
"firewallIps": "फ़ायरवॉल द्वारा अनुमत IPs",
|
||||
"firewallIpsInvalid": "अमान्य फ़ायरवॉल IP प्रविष्टि। समर्थित सिंटैक्स के लिए दस्तावेज़ देखें।"
|
||||
},
|
||||
"user": {
|
||||
"username": "उपयोगकर्ता नाम",
|
||||
"password": "पासवर्ड",
|
||||
"remember": "याद रखें",
|
||||
"name": "नाम",
|
||||
"email": "ई-मेल",
|
||||
"emailInvalid": "ई-मेल एक वैध ईमेल होनी चाहिए",
|
||||
"passwordMatch": "पासवर्ड मेल खाने चाहिए",
|
||||
"totpEnable": "TOTP सक्षम करें",
|
||||
"totpEnableTrue": "TOTP सक्षम सत्य होना चाहिए",
|
||||
"totpCode": "TOTP कोड"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "होस्ट"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "सत्र समय समाप्ति",
|
||||
"metricsEnabled": "मेट्रिक्स",
|
||||
"metricsPassword": "मेट्रिक्स पासवर्ड"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "डिवाइस",
|
||||
"cidrValid": "CIDR वैध होना चाहिए"
|
||||
},
|
||||
"otl": "एकबारगी लिंक",
|
||||
"stringMalformed": "स्ट्रिंग विकृत है",
|
||||
"body": "बॉडी एक वैध ऑब्जेक्ट होनी चाहिए",
|
||||
"hook": "हुक",
|
||||
"enabled": "सक्षम",
|
||||
"mtu": "MTU",
|
||||
"port": "पोर्ट",
|
||||
"persistentKeepalive": "स्थायी कीपअलाइव",
|
||||
"address": "IP पता",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "अनुमत IPs",
|
||||
"file": "फ़ाइल"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "कॉपी समर्थित नहीं है",
|
||||
"copied": "कॉपी हो गया!",
|
||||
"failed": "कॉपी विफल रहा",
|
||||
"copy": "कॉपी करें"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "जंक पैकेट गणना (Jc)",
|
||||
"jCDescription": "भेजे जाने वाले जंक पैकेट की संख्या (1-128, अनुशंसित: 4-12)",
|
||||
"jMinLabel": "जंक पैकेट न्यूनतम आकार (Jmin)",
|
||||
"jMinDescription": "जंक पैकेट का न्यूनतम आकार (0-1279*, अनुशंसित: 8, Jmax से कम होना चाहिए)",
|
||||
"jMaxLabel": "जंक पैकेट अधिकतम आकार (Jmax)",
|
||||
"jMaxDescription": "जंक पैकेट का अधिकतम आकार (1-1280*, अनुशंसित: 80, Jmin से अधिक होना चाहिए)",
|
||||
"s1Label": "इनिट पैकेट जंक आकार (S1)",
|
||||
"s1Description": "इनिट पैकेट जंक आकार (0-1132[1280* - 148 = 1132], अनुशंसित: 15-150, S1+56 ≠ S2)",
|
||||
"s2Label": "रिस्पॉन्स पैकेट जंक आकार (S2)",
|
||||
"s2Description": "रिस्पॉन्स पैकेट जंक आकार (0-1188[1280* - 92 = 1188], अनुशंसित: 15-150)",
|
||||
"s3Label": "कुकी रिप्लाई पैकेट जंक आकार (S3)",
|
||||
"s3Description": "कुकी रिप्लाई पैकेट जंक आकार",
|
||||
"s4Label": "ट्रांसपोर्ट पैकेट जंक आकार (S4)",
|
||||
"s4Description": "ट्रांसपोर्ट पैकेट जंक आकार",
|
||||
"h1Label": "इनिट मैजिक हेडर (H1)",
|
||||
"h1Description": "इनिट पैकेट हेडर मान या श्रेणी (X या X-Y, जहाँ X<Y। न्यूनतम 5, अधिकतम 2147483647। मान या श्रेणी अन्य हेडर से ओवरलैप नहीं होनी चाहिए)",
|
||||
"h2Label": "रिस्पॉन्स मैजिक हेडर (H2)",
|
||||
"h2Description": "रिस्पॉन्स पैकेट हेडर मान या श्रेणी (X या X-Y, जहाँ X<Y। न्यूनतम 5, अधिकतम 2147483647। मान या श्रेणी अन्य हेडर से ओवरलैप नहीं होनी चाहिए)",
|
||||
"h3Label": "कुकी रिप्लाई मैजिक हेडर (H3)",
|
||||
"h3Description": "कुकी रिप्लाई पैकेट हेडर मान या श्रेणी (X या X-Y, जहाँ X<Y। न्यूनतम 5, अधिकतम 2147483647। मान या श्रेणी अन्य हेडर से ओवरलैप नहीं होनी चाहिए)",
|
||||
"h4Label": "ट्रांसपोर्ट मैजिक हेडर (H4)",
|
||||
"h4Description": "ट्रांसपोर्ट पैकेट हेडर मान या श्रेणी (X या X-Y, जहाँ X<Y। न्यूनतम 5, अधिकतम 2147483647। मान या श्रेणी अन्य हेडर से ओवरलैप नहीं होनी चाहिए)",
|
||||
"i1Label": "विशेष जंक पैकेट 1 (I1)",
|
||||
"i1Description": "hex फ़ॉर्मेट में प्रोटोकॉल मिमिक पैकेट: <b 0x...>",
|
||||
"i2Label": "विशेष जंक पैकेट 2 (I2)",
|
||||
"i2Description": "hex फ़ॉर्मेट में प्रोटोकॉल मिमिक पैकेट: <b 0x...>",
|
||||
"i3Label": "विशेष जंक पैकेट 3 (I3)",
|
||||
"i3Description": "hex फ़ॉर्मेट में प्रोटोकॉल मिमिक पैकेट: <b 0x...>",
|
||||
"i4Label": "विशेष जंक पैकेट 4 (I4)",
|
||||
"i4Description": "hex फ़ॉर्मेट में प्रोटोकॉल मिमिक पैकेट: <b 0x...>",
|
||||
"i5Label": "विशेष जंक पैकेट 5 (I5)",
|
||||
"i5Description": "hex फ़ॉर्मेट में प्रोटोकॉल मिमिक पैकेट: <b 0x...>",
|
||||
"mtuNote": "मान MTU पर निर्भर करते हैं",
|
||||
"obfuscationParameters": "AmneziaWG ऑबफस्केशन पैरामीटर"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"pages": {
|
||||
"me": "アカウント",
|
||||
"clients": "クライアント",
|
||||
"admin": {
|
||||
"panel": "管理パネル",
|
||||
"general": "一般",
|
||||
"config": "構成",
|
||||
"interface": "インターフェイス",
|
||||
"hooks": "フック"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "メール"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "現在のパスワード",
|
||||
"enable2fa": "二要素認証を有効化",
|
||||
"enable2faDesc": "QRコードを認証アプリでスキャンするか、キーを手動で入力してください。",
|
||||
"2faKey": "TOTPキー",
|
||||
"2faCodeDesc": "認証アプリのコードを入力してください。",
|
||||
"disable2fa": "二要素認証を無効化",
|
||||
"disable2faDesc": "二要素認証を無効にするにはパスワードを入力してください。"
|
||||
},
|
||||
"general": {
|
||||
"name": "名前",
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"newPassword": "新しいパスワード",
|
||||
"updatePassword": "パスワードを更新",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "許可 IP",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "永続的 Keepalive",
|
||||
"logout": "ログアウト",
|
||||
"continue": "続行",
|
||||
"host": "ホスト",
|
||||
"port": "ポート",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"confirmPassword": "パスワードの確認",
|
||||
"loading": "読み込み中...",
|
||||
"2fa": "二要素認証",
|
||||
"2faCode": "TOTPコード"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "wg-easy の初期セットアップへようこそ",
|
||||
"welcomeDesc": "Linux ホストで WireGuard をインストールして管理する最も簡単な方法です",
|
||||
"existingSetup": "既存のセットアップがありますか?",
|
||||
"createAdminDesc": "まず管理者ユーザー名と強力で安全なパスワードを入力してください。この情報は管理パネルへのログインに使用されます。",
|
||||
"setupConfigDesc": "ホストとポート情報を入力してください。これは各デバイスで WireGuard を設定するときのクライアント構成に使用されます。",
|
||||
"setupMigrationDesc": "新しいセットアップへ以前の wg-easy バージョンからデータを移行する場合は、バックアップファイルを指定してください。",
|
||||
"upload": "アップロード",
|
||||
"migration": "バックアップを復元:",
|
||||
"createAccount": "アカウントを作成",
|
||||
"successful": "セットアップが完了しました",
|
||||
"hostDesc": "クライアントが接続する公開ホスト名",
|
||||
"portDesc": "クライアントが接続し、WireGuard が待ち受ける公開 UDP ポート"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "利用可能な更新があります!",
|
||||
"update": "更新"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "ダークテーマ",
|
||||
"light": "ライトテーマ",
|
||||
"system": "システムテーマ"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "グラフの表示/非表示",
|
||||
"donate": "寄付"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "サインイン",
|
||||
"rememberMe": "ログイン状態を保持",
|
||||
"rememberMeDesc": "ブラウザーを閉じた後もログイン状態を保持します",
|
||||
"insecure": "安全でない接続ではログインできません。HTTPS を使用してください。",
|
||||
"2faRequired": "二要素認証が必要です",
|
||||
"2faWrong": "二要素認証が正しくありません"
|
||||
},
|
||||
"client": {
|
||||
"empty": "まだクライアントはありません。",
|
||||
"newShort": "新規",
|
||||
"sort": "並べ替え",
|
||||
"create": "クライアントを作成",
|
||||
"created": "クライアントを作成しました",
|
||||
"new": "新しいクライアント",
|
||||
"name": "名前",
|
||||
"expireDate": "有効期限",
|
||||
"expireDateDesc": "この日にクライアントが無効化されます。空欄の場合は無期限です",
|
||||
"delete": "削除",
|
||||
"deleteClient": "クライアントを削除",
|
||||
"deleteDialog1": "削除してもよろしいですか",
|
||||
"deleteDialog2": "この操作は元に戻せません。",
|
||||
"enabled": "有効",
|
||||
"address": "アドレス",
|
||||
"serverAllowedIps": "サーバー許可 IP",
|
||||
"otlDesc": "短いワンタイムリンクを生成",
|
||||
"permanent": "無期限",
|
||||
"createdOn": "作成日: ",
|
||||
"lastSeen": "最終接続: ",
|
||||
"totalDownload": "総ダウンロード: ",
|
||||
"totalUpload": "総アップロード: ",
|
||||
"newClient": "新しいクライアント",
|
||||
"disableClient": "クライアントを無効化",
|
||||
"enableClient": "クライアントを有効化",
|
||||
"noPrivKey": "このクライアントの秘密鍵は不明です。構成を作成できません。",
|
||||
"showQR": "QRコードを表示",
|
||||
"downloadConfig": "構成をダウンロード",
|
||||
"allowedIpsDesc": "VPN 経由でルーティングされる IP (グローバル設定を上書き)",
|
||||
"serverAllowedIpsDesc": "サーバーがこのクライアントへルーティングする IP",
|
||||
"mtuDesc": "VPN トンネルの最大転送単位 (パケットサイズ) を設定します",
|
||||
"persistentKeepaliveDesc": "Keepalive パケットを送信する間隔 (秒) を設定します。0 で無効化します",
|
||||
"hooks": "フック",
|
||||
"hooksDescription": "フックは wg-quick でのみ動作します",
|
||||
"hooksLeaveEmpty": "wg-quick 専用です。それ以外の場合は空のままにしてください",
|
||||
"dnsDesc": "クライアントが使用する DNS サーバー (グローバル設定を上書き)",
|
||||
"notConnected": "クライアントは接続されていません",
|
||||
"endpoint": "エンドポイント",
|
||||
"endpointDesc": "WireGuard 接続が確立されているクライアントの IP",
|
||||
"search": "クライアントを検索...",
|
||||
"config": "構成",
|
||||
"viewConfig": "構成を表示",
|
||||
"firewallIps": "ファイアウォール許可 IP",
|
||||
"firewallIpsDesc": "このクライアントがアクセスできる宛先 IP/CIDR (サーバー側で強制)。空欄の場合は Allowed IPs を使用します。任意のポートとプロトコルによるフィルタリングに対応しています。構文はドキュメントを参照してください。",
|
||||
"downloadPng": "PNG をダウンロード",
|
||||
"copyPng": "PNG をコピー"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "変更",
|
||||
"cancel": "キャンセル",
|
||||
"create": "作成"
|
||||
},
|
||||
"toast": {
|
||||
"success": "成功",
|
||||
"saved": "保存しました",
|
||||
"error": "エラー",
|
||||
"unknown": "不明なエラーです。詳細はコンソールを確認してください"
|
||||
},
|
||||
"form": {
|
||||
"actions": "アクション",
|
||||
"save": "保存",
|
||||
"revert": "元に戻す",
|
||||
"sectionGeneral": "一般",
|
||||
"sectionAdvanced": "詳細",
|
||||
"noItems": "項目なし",
|
||||
"nullNoItems": "項目なし。グローバル設定を使用しています",
|
||||
"add": "追加"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "セッションタイムアウト",
|
||||
"sessionTimeoutDesc": "ログイン状態を保持する場合のセッション期間 (秒)",
|
||||
"metrics": "メトリクス",
|
||||
"metricsPassword": "パスワード",
|
||||
"metricsPasswordDesc": "メトリクスエンドポイント用の Bearer パスワード (パスワードまたは argon2 ハッシュ)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "JSON 形式のメトリクスルート",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Prometheus メトリクスのルート"
|
||||
},
|
||||
"config": {
|
||||
"connection": "接続",
|
||||
"hostDesc": "クライアントが接続する公開ホスト名 (構成が無効化されます)",
|
||||
"portDesc": "クライアントが接続する公開 UDP ポート (構成が無効化されます。通常はインターフェイスのポートも変更します)",
|
||||
"allowedIpsDesc": "クライアントが使用する許可 IP (グローバル設定)",
|
||||
"dnsDesc": "クライアントが使用する DNS サーバー (グローバル設定)",
|
||||
"mtuDesc": "クライアントが使用する MTU (新規クライアントのみ)",
|
||||
"persistentKeepaliveDesc": "サーバーへ Keepalive を送信する間隔 (秒)。0 = 無効 (新規クライアントのみ)",
|
||||
"suggest": "候補",
|
||||
"suggestDesc": "ホスト欄に使用する IP アドレスまたはホスト名を選択してください"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR を変更しました",
|
||||
"device": "デバイス",
|
||||
"deviceDesc": "WireGuard トラフィックを転送する Ethernet デバイス",
|
||||
"mtuDesc": "WireGuard が使用する MTU",
|
||||
"portDesc": "WireGuard が待ち受ける UDP ポート (通常は構成ポートも変更します)",
|
||||
"changeCidr": "CIDR を変更",
|
||||
"restart": "インターフェイスを再起動",
|
||||
"restartDesc": "WireGuard インターフェイスを再起動します",
|
||||
"restartWarn": "インターフェイスを再起動してもよろしいですか?すべてのクライアントが切断されます。",
|
||||
"restartSuccess": "インターフェイスを再起動しました",
|
||||
"firewall": "トラフィックフィルタリング",
|
||||
"firewallEnabled": "クライアントごとのファイアウォールを有効化",
|
||||
"firewallEnabledDesc": "iptables を使用して、クライアントのトラフィックを特定の宛先 IP に制限します。有効にすると、クライアントごとに許可する宛先を設定できます。"
|
||||
},
|
||||
"introText": "管理パネルへようこそ。\n\nここでは一般設定、構成、インターフェイス設定、フックを管理できます。\n\nまずサイドバーからセクションを選択してください。"
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{0} は必須です",
|
||||
"validNumber": "{0} は有効な数値である必要があります",
|
||||
"validNumberRange": "{0} は有効な数値または数値範囲である必要があります",
|
||||
"validString": "{0} は有効な文字列である必要があります",
|
||||
"validBoolean": "{0} は有効な真偽値である必要があります",
|
||||
"validArray": "{0} は有効な配列である必要があります",
|
||||
"stringMin": "{0} は {1} 文字以上である必要があります",
|
||||
"numberMin": "{0} は {1} 以上である必要があります"
|
||||
},
|
||||
"client": {
|
||||
"id": "クライアント ID",
|
||||
"name": "名前",
|
||||
"expiresAt": "有効期限",
|
||||
"address4": "IPv4 アドレス",
|
||||
"address6": "IPv6 アドレス",
|
||||
"serverAllowedIps": "サーバー許可 IP",
|
||||
"firewallIps": "ファイアウォール許可 IP",
|
||||
"firewallIpsInvalid": "ファイアウォール IP の指定が無効です。対応している構文はドキュメントを参照してください。"
|
||||
},
|
||||
"user": {
|
||||
"username": "ユーザー名",
|
||||
"password": "パスワード",
|
||||
"remember": "ログイン状態を保持",
|
||||
"name": "名前",
|
||||
"email": "メール",
|
||||
"emailInvalid": "メールは有効なメールアドレスである必要があります",
|
||||
"passwordMatch": "パスワードが一致しません",
|
||||
"totpEnable": "TOTP 有効化",
|
||||
"totpEnableTrue": "TOTP 有効化は true である必要があります",
|
||||
"totpCode": "TOTPコード"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "ホスト"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "セッションタイムアウト",
|
||||
"metricsEnabled": "メトリクス",
|
||||
"metricsPassword": "メトリクスパスワード"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "デバイス",
|
||||
"cidrValid": "CIDR は有効である必要があります"
|
||||
},
|
||||
"otl": "ワンタイムリンク",
|
||||
"stringMalformed": "文字列の形式が正しくありません",
|
||||
"body": "本文は有効なオブジェクトである必要があります",
|
||||
"hook": "フック",
|
||||
"enabled": "有効",
|
||||
"mtu": "MTU",
|
||||
"port": "ポート",
|
||||
"persistentKeepalive": "永続的 Keepalive",
|
||||
"address": "IP アドレス",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "許可 IP",
|
||||
"file": "ファイル"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "コピーはサポートされていません",
|
||||
"copied": "コピーしました!",
|
||||
"failed": "コピーに失敗しました",
|
||||
"copy": "コピー"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "ジャンクパケット数 (Jc)",
|
||||
"jCDescription": "送信するジャンクパケット数 (1-128、推奨: 4-12)",
|
||||
"jMinLabel": "ジャンクパケット最小サイズ (Jmin)",
|
||||
"jMinDescription": "ジャンクパケットの最小サイズ (0-1279*、推奨: 8、Jmax 未満)",
|
||||
"jMaxLabel": "ジャンクパケット最大サイズ (Jmax)",
|
||||
"jMaxDescription": "ジャンクパケットの最大サイズ (1-1280*、推奨: 80、Jmin より大きい)",
|
||||
"s1Label": "初期化パケットのジャンクサイズ (S1)",
|
||||
"s1Description": "初期化パケットのジャンクサイズ (0-1132[1280* - 148 = 1132]、推奨: 15-150、S1+56 ≠ S2)",
|
||||
"s2Label": "応答パケットのジャンクサイズ (S2)",
|
||||
"s2Description": "応答パケットのジャンクサイズ (0-1188[1280* - 92 = 1188]、推奨: 15-150)",
|
||||
"s3Label": "Cookie 応答パケットのジャンクサイズ (S3)",
|
||||
"s3Description": "Cookie 応答パケットのジャンクサイズ",
|
||||
"s4Label": "トランスポートパケットのジャンクサイズ (S4)",
|
||||
"s4Description": "トランスポートパケットのジャンクサイズ",
|
||||
"h1Label": "初期化マジックヘッダー (H1)",
|
||||
"h1Description": "初期化パケットのヘッダー値または範囲 (X または X-Y、X<Y。最小 5、最大 2147483647。値または範囲は他のヘッダーと重複してはいけません)",
|
||||
"h2Label": "応答マジックヘッダー (H2)",
|
||||
"h2Description": "応答パケットのヘッダー値または範囲 (X または X-Y、X<Y。最小 5、最大 2147483647。値または範囲は他のヘッダーと重複してはいけません)",
|
||||
"h3Label": "Cookie 応答マジックヘッダー (H3)",
|
||||
"h3Description": "Cookie 応答パケットのヘッダー値または範囲 (X または X-Y、X<Y。最小 5、最大 2147483647。値または範囲は他のヘッダーと重複してはいけません)",
|
||||
"h4Label": "トランスポートマジックヘッダー (H4)",
|
||||
"h4Description": "トランスポートパケットのヘッダー値または範囲 (X または X-Y、X<Y。最小 5、最大 2147483647。値または範囲は他のヘッダーと重複してはいけません)",
|
||||
"i1Label": "特殊ジャンクパケット 1 (I1)",
|
||||
"i1Description": "16進数形式のプロトコル模倣パケット: <b 0x...>",
|
||||
"i2Label": "特殊ジャンクパケット 2 (I2)",
|
||||
"i2Description": "16進数形式のプロトコル模倣パケット: <b 0x...>",
|
||||
"i3Label": "特殊ジャンクパケット 3 (I3)",
|
||||
"i3Description": "16進数形式のプロトコル模倣パケット: <b 0x...>",
|
||||
"i4Label": "特殊ジャンクパケット 4 (I4)",
|
||||
"i4Description": "16進数形式のプロトコル模倣パケット: <b 0x...>",
|
||||
"i5Label": "特殊ジャンクパケット 5 (I5)",
|
||||
"i5Description": "16進数形式のプロトコル模倣パケット: <b 0x...>",
|
||||
"mtuNote": "値は MTU に依存します",
|
||||
"obfuscationParameters": "AmneziaWG 難読化パラメーター"
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,11 @@
|
||||
"2faKey": "TOTP 키",
|
||||
"2faCodeDesc": "인증기 앱에서 코드를 입력하십시오.",
|
||||
"disable2fa": "2단계 인증 비활성화",
|
||||
"disable2faDesc": "2단계 인증을 비활성화하려면 비밀번호를 입력하세요."
|
||||
"disable2faDesc": "2단계 인증을 비활성화하려면 비밀번호를 입력하세요.",
|
||||
"linkOauth": "외부 제공자와 계정 연결",
|
||||
"unlinkOauth": "연결 해제",
|
||||
"linkedWith": "{0}와(과) 연결됨",
|
||||
"providerDisabled": "현재 연결된 제공자가 활성화되어 있지 않습니다"
|
||||
},
|
||||
"general": {
|
||||
"name": "이름",
|
||||
@@ -28,6 +32,7 @@
|
||||
"password": "비밀번호",
|
||||
"newPassword": "새 비밀번호",
|
||||
"updatePassword": "비밀번호 업데이트",
|
||||
"addPassword": "비밀번호 추가",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "허용된 IP",
|
||||
"dns": "DNS",
|
||||
@@ -41,7 +46,8 @@
|
||||
"confirmPassword": "비밀번호 확인",
|
||||
"loading": "로딩 중...",
|
||||
"2fa": "2단계 인증",
|
||||
"2faCode": "TOTP 코드"
|
||||
"2faCode": "TOTP 코드",
|
||||
"externalAuth": "외부 인증"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "wg-easy의 첫 번째 설정에 오신 것을 환영합니다.",
|
||||
@@ -72,6 +78,8 @@
|
||||
},
|
||||
"login": {
|
||||
"signIn": "로그인",
|
||||
"signInWith": "{0}(으)로 로그인",
|
||||
"or": "또는",
|
||||
"rememberMe": "로그인 상태 유지",
|
||||
"rememberMeDesc": "브라우저를 닫은 후에도 로그인 유지",
|
||||
"insecure": "안전하지 않은 연결로 로그인할 수 없습니다. HTTPS를 사용하세요.",
|
||||
@@ -88,6 +96,7 @@
|
||||
"name": "이름",
|
||||
"expireDate": "만료 날짜",
|
||||
"expireDateDesc": "클라이언트가 비활성화될 날짜. 영구적으로 비워두기",
|
||||
"delete": "삭제",
|
||||
"deleteClient": "클라이언트 삭제",
|
||||
"deleteDialog1": "정말로 삭제하시겠습니까",
|
||||
"deleteDialog2": "이 작업은 실행 취소할 수 없습니다.",
|
||||
@@ -116,7 +125,14 @@
|
||||
"dnsDesc": "클라이언트가 사용할 DNS 서버(전역 구성 무시)",
|
||||
"notConnected": "클라이언트가 연결되지 않음",
|
||||
"endpoint": "엔드포인트",
|
||||
"endpointDesc": "WireGuard에 연결된 클라이언트의 IP 주소"
|
||||
"endpointDesc": "WireGuard에 연결된 클라이언트의 IP 주소",
|
||||
"search": "클라이언트 검색...",
|
||||
"config": "구성",
|
||||
"viewConfig": "구성 보기",
|
||||
"firewallIps": "방화벽 허용 IP",
|
||||
"firewallIpsDesc": "이 클라이언트가 접근할 수 있는 목적지 IP/CIDR(서버 측 적용). 허용된 IP를 사용하려면 비워 두십시오. 선택적 포트 및 프로토콜 필터링을 지원합니다. 구문은 문서를 참조하십시오.",
|
||||
"downloadPng": "PNG 다운로드",
|
||||
"copyPng": "PNG 복사"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "변경",
|
||||
@@ -126,7 +142,8 @@
|
||||
"toast": {
|
||||
"success": "성공",
|
||||
"saved": "저장됨",
|
||||
"error": "오류"
|
||||
"error": "오류",
|
||||
"unknown": "알 수 없는 오류. 자세한 내용은 콘솔을 참조하십시오"
|
||||
},
|
||||
"form": {
|
||||
"actions": "작업",
|
||||
@@ -171,7 +188,10 @@
|
||||
"restart": "인터페이스 재시작",
|
||||
"restartDesc": "WireGuard 인터페이스를 재시작합니다.",
|
||||
"restartWarn": "인터페이스를 재시작하시겠습니까? 이 작업은 모든 클라이언트를 연결 해제합니다.",
|
||||
"restartSuccess": "인터페이스가 재시작되었습니다"
|
||||
"restartSuccess": "인터페이스가 재시작되었습니다",
|
||||
"firewall": "트래픽 필터링",
|
||||
"firewallEnabled": "클라이언트별 방화벽 활성화",
|
||||
"firewallEnabledDesc": "iptables를 사용하여 클라이언트 트래픽을 특정 목적지 IP로 제한합니다. 활성화하면 각 클라이언트에 허용된 목적지를 구성할 수 있습니다."
|
||||
},
|
||||
"introText": "관리 패널에 오신 것을 환영합니다.\n\n여기에서 일반 설정, 구성, 인터페이스 설정 및 후크를 관리할 수 있습니다.\n\n사이드바에서 섹션 중 하나를 선택하여 시작하세요."
|
||||
},
|
||||
@@ -179,6 +199,7 @@
|
||||
"generic": {
|
||||
"required": "{0}은(는) 필수입니다.",
|
||||
"validNumber": "{0}은(는) 유효한 숫자여야 합니다.",
|
||||
"validNumberRange": "{0}은(는) 유효한 숫자 또는 숫자 범위여야 합니다",
|
||||
"validString": "{0}은 유효한 문자열이어야 합니다",
|
||||
"validBoolean": "{0}은(는) 유효한 불리언이어야 합니다.",
|
||||
"validArray": "{0}는 유효한 배열이어야 합니다",
|
||||
@@ -191,7 +212,9 @@
|
||||
"expiresAt": "만료 시간",
|
||||
"address4": "IPv4 주소",
|
||||
"address6": "IPv6 주소",
|
||||
"serverAllowedIps": "서버 허용된 IP"
|
||||
"serverAllowedIps": "서버 허용된 IP",
|
||||
"firewallIps": "방화벽 허용 IP",
|
||||
"firewallIpsInvalid": "잘못된 방화벽 IP 항목입니다. 지원되는 구문은 문서를 참조하십시오."
|
||||
},
|
||||
"user": {
|
||||
"username": "사용자 이름",
|
||||
@@ -236,5 +259,47 @@
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "복사가 지원되지 않습니다",
|
||||
"copied": "복사됨!",
|
||||
"failed": "복사 실패",
|
||||
"copy": "복사"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "정크 패킷 수 (Jc)",
|
||||
"jCDescription": "보낼 정크 패킷의 수 (1-128, 권장: 4-12)",
|
||||
"jMinLabel": "정크 패킷 최소 크기 (Jmin)",
|
||||
"jMinDescription": "정크 패킷의 최소 크기 (0-1279*, 권장: 8, Jmax보다 작아야 함)",
|
||||
"jMaxLabel": "정크 패킷 최대 크기 (Jmax)",
|
||||
"jMaxDescription": "정크 패킷의 최대 크기 (1-1280*, 권장: 80, Jmin보다 커야 함)",
|
||||
"s1Label": "초기화 패킷 정크 크기 (S1)",
|
||||
"s1Description": "초기화 패킷 정크 크기 (0-1132[1280* - 148 = 1132], 권장: 15-150, S1+56 ≠ S2)",
|
||||
"s2Label": "응답 패킷 정크 크기 (S2)",
|
||||
"s2Description": "응답 패킷 정크 크기 (0-1188[1280* - 92 = 1188], 권장: 15-150)",
|
||||
"s3Label": "쿠키 응답 패킷 정크 크기 (S3)",
|
||||
"s3Description": "쿠키 응답 패킷 정크 크기",
|
||||
"s4Label": "전송 패킷 정크 크기 (S4)",
|
||||
"s4Description": "전송 패킷 정크 크기",
|
||||
"h1Label": "초기화 매직 헤더 (H1)",
|
||||
"h1Description": "초기화 패킷 헤더 값 또는 범위 (X 또는 X-Y, X<Y. 최소 5, 최대 2147483647. 값 또는 범위가 다른 헤더와 겹치지 않아야 함)",
|
||||
"h2Label": "응답 매직 헤더 (H2)",
|
||||
"h2Description": "응답 패킷 헤더 값 또는 범위 (X 또는 X-Y, X<Y. 최소 5, 최대 2147483647. 값 또는 범위가 다른 헤더와 겹치지 않아야 함)",
|
||||
"h3Label": "쿠키 응답 매직 헤더 (H3)",
|
||||
"h3Description": "쿠키 응답 패킷 헤더 값 또는 범위 (X 또는 X-Y, X<Y. 최소 5, 최대 2147483647. 값 또는 범위가 다른 헤더와 겹치지 않아야 함)",
|
||||
"h4Label": "전송 매직 헤더 (H4)",
|
||||
"h4Description": "전송 패킷 헤더 값 또는 범위 (X 또는 X-Y, X<Y. 최소 5, 최대 2147483647. 값 또는 범위가 다른 헤더와 겹치지 않아야 함)",
|
||||
"i1Label": "특수 정크 패킷 1 (I1)",
|
||||
"i1Description": "16진수 형식의 프로토콜 모방 패킷: <b 0x...>",
|
||||
"i2Label": "특수 정크 패킷 2 (I2)",
|
||||
"i2Description": "16진수 형식의 프로토콜 모방 패킷: <b 0x...>",
|
||||
"i3Label": "특수 정크 패킷 3 (I3)",
|
||||
"i3Description": "16진수 형식의 프로토콜 모방 패킷: <b 0x...>",
|
||||
"i4Label": "특수 정크 패킷 4 (I4)",
|
||||
"i4Description": "16진수 형식의 프로토콜 모방 패킷: <b 0x...>",
|
||||
"i5Label": "특수 정크 패킷 5 (I5)",
|
||||
"i5Description": "16진수 형식의 프로토콜 모방 패킷: <b 0x...>",
|
||||
"mtuNote": "값은 MTU에 따라 달라집니다",
|
||||
"obfuscationParameters": "AmneziaWG 난독화 매개변수"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
"2faKey": "TOTP-sleutel",
|
||||
"2faCodeDesc": "Voer de code in van uw authenticator-app.",
|
||||
"disable2fa": "Twee-factor-authenticatie uitschakelen",
|
||||
"disable2faDesc": "Voer uw wachtwoord in om de twee-factor-authenticatie uit te schakelen."
|
||||
"disable2faDesc": "Voer uw wachtwoord in om de twee-factor-authenticatie uit te schakelen.",
|
||||
"linkOauth": "Koppel uw account met een externe provider",
|
||||
"unlinkOauth": "Ontkoppelen",
|
||||
"linkedWith": "Gekoppeld met {0}",
|
||||
"providerDisabled": "Uw huidige gekoppelde provider is niet ingeschakeld"
|
||||
},
|
||||
"general": {
|
||||
"name": "Naam",
|
||||
@@ -28,6 +32,7 @@
|
||||
"password": "Wachtwoord",
|
||||
"newPassword": "Nieuw wachtwoord",
|
||||
"updatePassword": "Wachtwoord bijwerken",
|
||||
"addPassword": "Add Password",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "Toegestane IP's",
|
||||
"dns": "DNS",
|
||||
@@ -41,7 +46,8 @@
|
||||
"confirmPassword": "Wachtwoord bevestigen",
|
||||
"loading": "Laden...",
|
||||
"2fa": "Twee-factor-authenticatie uitschakelen",
|
||||
"2faCode": "TOTP-code"
|
||||
"2faCode": "TOTP-code",
|
||||
"externalAuth": "Externe authenticatie"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Welkom bij uw eerste installatie van wg-easy",
|
||||
@@ -72,11 +78,14 @@
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Inloggen",
|
||||
"signInWith": "Sign in with {0}",
|
||||
"or": "or",
|
||||
"rememberMe": "Onthoud mij",
|
||||
"rememberMeDesc": "Ingelogd blijven na het sluiten van de browser",
|
||||
"insecure": "U kunt niet inloggen via een onveilige verbinding. Gebruik HTTPS.",
|
||||
"2faRequired": "Twee-factor-authenticatie is vereist",
|
||||
"2faWrong": "Twee-factor-authenticatiecode is fout"
|
||||
"2faWrong": "Twee-factor-authenticatiecode is fout",
|
||||
"loginExpired": "Login verlopen. Probeer het opnieuw"
|
||||
},
|
||||
"client": {
|
||||
"empty": "Er zijn nog geen cliënten.",
|
||||
@@ -196,7 +205,9 @@
|
||||
"validBoolean": "{0} moet een geldige boolean zijn",
|
||||
"validArray": "{0} moet een geldige array zijn",
|
||||
"stringMin": "{0} moet minstens {1} teken bevatten",
|
||||
"numberMin": "{0} moet minstens {1} zijn"
|
||||
"stringMax": "{0} mag maximaal {1} tekens bevatten",
|
||||
"numberMin": "{0} moet minstens {1} zijn",
|
||||
"numberMax": "{0} mag maximaal {1} zijn"
|
||||
},
|
||||
"client": {
|
||||
"id": "Cliënt-ID",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Zaloguj się",
|
||||
"or": "lub",
|
||||
"rememberMe": "Zapamiętaj mnie",
|
||||
"rememberMeDesc": "Pozostań zalogowany po zamknięciu przeglądarki",
|
||||
"insecure": "Nie możesz zalogować się przez niezabezpieczone połączenie. Użyj HTTPS.",
|
||||
|
||||
+21
-11
@@ -2,10 +2,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
future: {
|
||||
compatibilityVersion: 4,
|
||||
},
|
||||
compatibilityDate: '2026-02-06',
|
||||
compatibilityDate: '2026-06-19',
|
||||
devtools: { enabled: true },
|
||||
modules: [
|
||||
'@nuxtjs/i18n',
|
||||
@@ -51,6 +48,11 @@ export default defineNuxtConfig({
|
||||
language: 'it-IT',
|
||||
name: 'Italiano',
|
||||
},
|
||||
{
|
||||
code: 'ja',
|
||||
language: 'ja-JP',
|
||||
name: '日本語',
|
||||
},
|
||||
{
|
||||
code: 'fr',
|
||||
language: 'fr-FR',
|
||||
@@ -131,6 +133,11 @@ export default defineNuxtConfig({
|
||||
language: 'bg-BG',
|
||||
name: 'Български',
|
||||
},
|
||||
{
|
||||
code: 'hi',
|
||||
language: 'hi-IN',
|
||||
name: 'हिन्दी',
|
||||
},
|
||||
{
|
||||
code: 'gl',
|
||||
language: 'gl-ES',
|
||||
@@ -150,21 +157,24 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
nitro: {
|
||||
imports: {
|
||||
autoImport: false,
|
||||
},
|
||||
esbuild: {
|
||||
options: {
|
||||
// to support big int
|
||||
target: 'node20',
|
||||
target: 'node24',
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
'#db': fileURLToPath(new URL('./server/database/', import.meta.url)),
|
||||
},
|
||||
externals: {
|
||||
traceInclude: [fileURLToPath(new URL('./cli/index.ts', import.meta.url))],
|
||||
traceInclude: [
|
||||
fileURLToPath(
|
||||
new URL('./node_modules/.cache/wg-easy/trace.mjs', import.meta.url)
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
// for typecheck reasons (https://github.com/nuxt/cli/issues/323)
|
||||
'#db': fileURLToPath(new URL('./server/database/', import.meta.url)),
|
||||
'#cli': fileURLToPath(new URL('./cli', import.meta.url)),
|
||||
},
|
||||
});
|
||||
|
||||
+27
-25
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "wg-easy",
|
||||
"version": "15.3.0",
|
||||
"version": "15.4.0-beta.1",
|
||||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build && pnpm cli:build",
|
||||
"build": "pnpm cli:trace && nuxt build && pnpm cli:build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
@@ -16,6 +16,7 @@
|
||||
"typecheck": "nuxt typecheck",
|
||||
"check:all": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"cli:trace": "node cli/build.js --trace",
|
||||
"cli:build": "node cli/build.js",
|
||||
"cli:dev": "tsx cli/index.ts",
|
||||
"test:unit": "vitest run --project unit"
|
||||
@@ -23,32 +24,33 @@
|
||||
"dependencies": {
|
||||
"@eschricht/nuxt-color-mode": "^1.2.0",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@libsql/client": "^0.17.3",
|
||||
"@nuxtjs/i18n": "^10.3.0",
|
||||
"@libsql/client": "^0.17.4",
|
||||
"@nuxtjs/i18n": "^10.4.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@phc/format": "^1.0.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@pinia/nuxt": "^1.0.1",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"@vueuse/nuxt": "^14.3.0",
|
||||
"apexcharts": "^5.12.0",
|
||||
"argon2": "^0.44.0",
|
||||
"cidr-tools": "^12.0.1",
|
||||
"apexcharts": "^6.3.0",
|
||||
"argon2": "^0.45.0",
|
||||
"cidr-tools": "^12.1.2",
|
||||
"citty": "^0.2.2",
|
||||
"consola": "^3.4.2",
|
||||
"crc-32": "^1.2.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"ip-bigint": "^9.0.4",
|
||||
"ip-bigint": "^9.0.6",
|
||||
"is-cidr": "^7.0.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"js-sha256": "^0.11.1",
|
||||
"nuxt": "^3.21.5",
|
||||
"obug": "^2.1.1",
|
||||
"js-sha256": "^0.12.0",
|
||||
"nuxt": "^4.5.0",
|
||||
"obug": "^2.1.4",
|
||||
"openid-client": "^6.8.4",
|
||||
"otpauth": "^9.5.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia": "^4.0.2",
|
||||
"qr": "^0.6.0",
|
||||
"radix-vue": "^1.9.17",
|
||||
"semver": "^7.8.0",
|
||||
"semver": "^7.8.5",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"timeago.js": "^4.0.2",
|
||||
"vue": "latest",
|
||||
@@ -56,22 +58,22 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.15.2",
|
||||
"@nuxt/eslint": "^1.16.0",
|
||||
"@nuxt/test-utils": "^4.0.3",
|
||||
"@types/phc__format": "^1.0.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"@vitest/ui": "^4.1.6",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^9.39.4",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||
"tsx": "^4.22.1",
|
||||
"prettier": "^3.9.5",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tsx": "^4.23.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.6",
|
||||
"vue-tsc": "^3.2.9"
|
||||
"vitest": "^4.1.10",
|
||||
"vue-tsc": "^3.3.7"
|
||||
},
|
||||
"packageManager": "pnpm@11.1.2"
|
||||
"packageManager": "pnpm@11.15.1"
|
||||
}
|
||||
|
||||
Generated
+4092
-3750
File diff suppressed because it is too large
Load Diff
@@ -4,3 +4,12 @@ allowBuilds:
|
||||
esbuild: false
|
||||
unrs-resolver: false
|
||||
vue-demi: false
|
||||
|
||||
minimumReleaseAgeStrict: true
|
||||
|
||||
publicHoistPattern:
|
||||
- '@intlify/utils'
|
||||
- '@intlify/h3'
|
||||
- 'h3'
|
||||
- 'nitropack'
|
||||
- 'ofetch'
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const generalConfig = await Database.general.getConfig();
|
||||
return generalConfig;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { readValidatedBody } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
import { GeneralUpdateSchema } from '#db/repositories/general/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const hooks = await Database.hooks.get();
|
||||
return hooks;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { readValidatedBody } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import WireGuard from '#server/utils/WireGuard';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
import { HooksUpdateSchema } from '#db/repositories/hooks/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { readValidatedBody } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import WireGuard from '#server/utils/WireGuard';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
import { InterfaceCidrUpdateSchema } from '#db/repositories/interface/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { createError, readValidatedBody } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import WireGuard from '#server/utils/WireGuard';
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { firewall } from '#server/utils/firewall';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
import { InterfaceUpdateSchema } from '#db/repositories/interface/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import WireGuard from '#server/utils/WireGuard';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
await WireGuard.Restart();
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { cachedGetIpInformation } from '#server/utils/ip';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const result = await cachedGetIpInformation();
|
||||
return result;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const userConfig = await Database.userConfigs.get();
|
||||
return userConfig;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { readValidatedBody } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import WireGuard from '#server/utils/WireGuard';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
import { UserConfigUpdateSchema } from '#db/repositories/userConfig/types';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createError, defineEventHandler, sendRedirect } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import { SERVER_DEBUG } from '#server/utils/config';
|
||||
import { buildOauthConfig, getUserInfo } from '#server/utils/oauth';
|
||||
import { useWGSession } from '#server/utils/session';
|
||||
import { assertUnreachable } from '#server/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { config, provider, providerConfig } = await buildOauthConfig(event);
|
||||
|
||||
const session = await useWGSession(event);
|
||||
if (
|
||||
!session.data.oauth_nonce ||
|
||||
!session.data.oauth_verifier ||
|
||||
!session.data.oauth_state
|
||||
) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing OAuth State',
|
||||
});
|
||||
}
|
||||
|
||||
const userInfo = await getUserInfo(
|
||||
event,
|
||||
config,
|
||||
{
|
||||
oauth_nonce: session.data.oauth_nonce,
|
||||
oauth_verifier: session.data.oauth_verifier,
|
||||
oauth_state: session.data.oauth_state,
|
||||
},
|
||||
providerConfig
|
||||
);
|
||||
|
||||
const result = await Database.users.loginWithOAuth(
|
||||
provider,
|
||||
userInfo.sub,
|
||||
userInfo.preferred_username || userInfo.email,
|
||||
userInfo.email,
|
||||
userInfo.name || 'User'
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
switch (result.error) {
|
||||
case 'TOTP_REQUIRED':
|
||||
await session.update({
|
||||
pendingLogin: {
|
||||
type: 'oauth',
|
||||
userId: result.userId,
|
||||
remember: false,
|
||||
// 5min
|
||||
expires_at: Date.now() + 5 * 60 * 1000,
|
||||
},
|
||||
oauth_nonce: undefined,
|
||||
oauth_state: undefined,
|
||||
oauth_verifier: undefined,
|
||||
});
|
||||
return sendRedirect(event, '/login/2fa');
|
||||
case 'USER_DISABLED':
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'User disabled',
|
||||
});
|
||||
case 'USER_ALREADY_LINKED':
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage:
|
||||
'User already linked with different account or provider',
|
||||
});
|
||||
case 'AUTO_REGISTER_DISABLED':
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Auto registration is disabled',
|
||||
});
|
||||
case 'UNEXPECTED_ERROR':
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Unexpected error',
|
||||
});
|
||||
}
|
||||
assertUnreachable(result);
|
||||
}
|
||||
|
||||
// Create session
|
||||
const data = await session.update({
|
||||
userId: result.user.id,
|
||||
oauth_nonce: undefined,
|
||||
oauth_state: undefined,
|
||||
oauth_verifier: undefined,
|
||||
});
|
||||
|
||||
SERVER_DEBUG(
|
||||
`New OAuth Session: ${data.id} for ${result.user.id} (${result.user.username}) with ${provider}`
|
||||
);
|
||||
|
||||
return sendRedirect(event, '/');
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
defineEventHandler,
|
||||
getRequestHost,
|
||||
getValidatedQuery,
|
||||
sendRedirect,
|
||||
} from 'h3';
|
||||
import * as client from 'openid-client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { buildOauthConfig } from '#server/utils/oauth';
|
||||
import { useWGSession } from '#server/utils/session';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
|
||||
const OauthQuerySchema = z.object({
|
||||
link: z.coerce.boolean().optional(),
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const params = await getValidatedQuery(
|
||||
event,
|
||||
validateZod(OauthQuerySchema, event)
|
||||
);
|
||||
|
||||
const { config, provider, providerConfig } = await buildOauthConfig(event);
|
||||
|
||||
const host = getRequestHost(event);
|
||||
const protocol = WG_ENV.INSECURE ? 'http' : 'https';
|
||||
const baseUri = `${protocol}://${host}/api/auth/${provider}`;
|
||||
|
||||
let redirectUri = `${baseUri}/callback`;
|
||||
if (params.link) {
|
||||
redirectUri = `${baseUri}/link`;
|
||||
}
|
||||
|
||||
const codeVerifier = client.randomPKCECodeVerifier();
|
||||
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
|
||||
const nonce = client.randomNonce();
|
||||
const state = client.randomState();
|
||||
|
||||
const parameters: Record<string, string> = {
|
||||
...providerConfig.params,
|
||||
redirect_uri: redirectUri,
|
||||
scope: providerConfig.scope,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
nonce: nonce,
|
||||
state: state,
|
||||
};
|
||||
|
||||
const session = await useWGSession(event);
|
||||
await session.update({
|
||||
oauth_nonce: nonce,
|
||||
oauth_verifier: codeVerifier,
|
||||
oauth_state: state,
|
||||
});
|
||||
|
||||
const redirectTo = client.buildAuthorizationUrl(config, parameters);
|
||||
|
||||
return sendRedirect(event, redirectTo.toString());
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createError, sendRedirect } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { buildOauthConfig, getUserInfo } from '#server/utils/oauth';
|
||||
import { useWGSession } from '#server/utils/session';
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'me',
|
||||
'update',
|
||||
async ({ event, user, checkPermissions }) => {
|
||||
checkPermissions(user);
|
||||
|
||||
const { config, provider, providerConfig } = await buildOauthConfig(event);
|
||||
|
||||
const session = await useWGSession(event);
|
||||
if (
|
||||
!session.data.oauth_nonce ||
|
||||
!session.data.oauth_verifier ||
|
||||
!session.data.oauth_state
|
||||
) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing OAuth State',
|
||||
});
|
||||
}
|
||||
|
||||
const userInfo = await getUserInfo(
|
||||
event,
|
||||
config,
|
||||
{
|
||||
oauth_nonce: session.data.oauth_nonce,
|
||||
oauth_verifier: session.data.oauth_verifier,
|
||||
oauth_state: session.data.oauth_state,
|
||||
},
|
||||
providerConfig
|
||||
);
|
||||
|
||||
await Database.users.linkOauth(user.id, provider, userInfo.sub);
|
||||
|
||||
return sendRedirect(event, '/me');
|
||||
}
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user