├── .dockerignore ├── .editorconfig ├── .env ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── How_to_generate_an_bcrypt_hash.md ├── LICENSE ├── README.md ├── assets └── screenshot.png ├── docker-compose.yml ├── docs └── changelog.json ├── package-lock.json ├── package.json └── src ├── .eslintrc.json ├── config.js ├── lib ├── Server.js ├── ServerError.js ├── Util.js └── WireGuard.js ├── package-lock.json ├── package.json ├── server.js ├── services ├── Server.js └── WireGuard.js ├── tailwind.config.js ├── wgpw.mjs ├── wgpw.sh └── www ├── css └── app.css ├── img ├── apple-touch-icon.png ├── favicon.ico ├── logo.png └── logo.svg ├── index.html ├── js ├── api.js ├── app.js ├── i18n.js └── vendor │ ├── apexcharts.min.js │ ├── sha256.min.js │ ├── timeago.full.min.js │ ├── vue-apexcharts.min.js │ ├── vue-i18n.min.js │ └── vue.min.js ├── manifest.json └── src └── css └── app.css /.dockerignore: -------------------------------------------------------------------------------- 1 | /src/node_modules -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | # Minified JavaScript files shouldn't be changed 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | WG_HOST=🚨YOUR_SERVER_IP 2 | # (Supports: en, ru, tr, no, pl, fr, de, ca, es) 3 | LANGUAGE=en 4 | PORT=51821 5 | WG_DEVICE=eth0 6 | WG_PORT=51820 7 | WG_DEFAULT_ADDRESS=10.8.0.x 8 | WG_DEFAULT_DNS=1.1.1.1 9 | WG_ALLOWED_IPS=0.0.0.0/0, ::/0 10 | DICEBEAR_TYPE=bottts 11 | USE_GRAVATAR=true -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish Latest 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | deploy: 11 | name: Build & Deploy 12 | runs-on: ubuntu-latest 13 | if: github.repository_owner == 'w0rng' 14 | permissions: 15 | packages: write 16 | contents: read 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | ref: master 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Login to GitHub Container Registry 29 | uses: docker/login-action@v3 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Set environment variables 36 | run: echo RELEASE=$(cat ./src/package.json | jq -r .release | jq -r .version) >> $GITHUB_ENV 37 | 38 | - name: Build & Publish Docker Image 39 | uses: docker/build-push-action@v6 40 | with: 41 | push: true 42 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 43 | tags: ghcr.io/w0rng/amnezia-wg-easy:latest, ghcr.io/w0rng/amnezia-wg-easy:${{ env.RELEASE }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config 2 | /wg0.conf 3 | /wg0.json 4 | /src/node_modules 5 | .DS_Store 6 | *.swp 7 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # As a workaround we have to build on nodejs 18 2 | # nodejs 20 hangs on build with armv6/armv7 3 | FROM docker.io/library/node:18-alpine AS build_node_modules 4 | 5 | # Update npm to latest 6 | RUN npm install -g npm@latest 7 | 8 | # Copy Web UI 9 | COPY src /app 10 | WORKDIR /app 11 | RUN npm ci --omit=dev &&\ 12 | mv node_modules /node_modules 13 | 14 | # Copy build result to a new image. 15 | # This saves a lot of disk space. 16 | FROM amneziavpn/amnezia-wg:latest 17 | HEALTHCHECK CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1" --interval=1m --timeout=5s --retries=3 18 | COPY --from=build_node_modules /app /app 19 | 20 | # Move node_modules one directory up, so during development 21 | # we don't have to mount it in a volume. 22 | # This results in much faster reloading! 23 | # 24 | # Also, some node_modules might be native, and 25 | # the architecture & OS of your development machine might differ 26 | # than what runs inside of docker. 27 | COPY --from=build_node_modules /node_modules /node_modules 28 | 29 | # Copy the needed wg-password scripts 30 | COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw 31 | RUN chmod +x /bin/wgpw 32 | 33 | # Install Linux packages 34 | RUN apk add --no-cache \ 35 | dpkg \ 36 | dumb-init \ 37 | iptables \ 38 | nodejs \ 39 | npm 40 | 41 | # Use iptables-legacy 42 | RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save 43 | 44 | # Set Environment 45 | ENV DEBUG=Server,WireGuard 46 | 47 | # Run Web UI 48 | WORKDIR /app 49 | CMD ["/usr/bin/dumb-init", "node", "server.js"] 50 | -------------------------------------------------------------------------------- /How_to_generate_an_bcrypt_hash.md: -------------------------------------------------------------------------------- 1 | # wg-password 2 | 3 | `wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `wg-easy`, enhancing security by requiring passwords. 4 | 5 | ## Features 6 | 7 | - Generate bcrypt password hashes. 8 | - Easily integrate with `wg-easy` to enforce password requirements. 9 | 10 | ## Usage with Docker 11 | 12 | To generate a bcrypt password hash using docker, run the following command : 13 | 14 | ```sh 15 | docker run -it ghcr.io/w0rng/amnezia-wg-easy wgpw YOUR_PASSWORD 16 | PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD 17 | ``` 18 | If a password is not provided, the tool will prompt you for one : 19 | ```sh 20 | docker run -it ghcr.io/wg-easy/wg-easy wgpw 21 | Enter your password: // hidden prompt, type in your password 22 | PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' 23 | ``` 24 | 25 | **Important** : make sure to enclose your password in **single quotes** when you run `docker run` command : 26 | 27 | ```bash 28 | $ echo $2b$12$coPqCsPtcF <-- not correct 29 | b2 30 | $ echo "$2b$12$coPqCsPtcF" <-- not correct 31 | b2 32 | $ echo '$2b$12$coPqCsPtcF' <-- correct 33 | $2b$12$coPqCsPtcF 34 | ``` 35 | 36 | **Important** : Please note: don't wrap the generated hash password in single quotes when you use `docker-compose.yml`. Instead, replace each `$` symbol with two `$$` symbols. For example: 37 | 38 | ``` yaml 39 | - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG 40 | ``` 41 | 42 | This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw foobar123` and then inserted an additional `$` before each existing `$` symbol. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AmnewziaWG Easy 2 | 3 | You have found the easiest way to install & manage WireGuard on any Linux host! 4 | 5 |

6 | 7 |

8 | 9 | ## Features 10 | 11 | * All-in-one: AmneziaWG + Web UI. 12 | * Easy installation, simple to use. 13 | * List, create, edit, delete, enable & disable clients. 14 | * Show a client's QR code. 15 | * Download a client's configuration file. 16 | * Statistics for which clients are connected. 17 | * Tx/Rx charts for each connected client. 18 | * Gravatar support or random avatars. 19 | * Automatic Light / Dark Mode 20 | * Multilanguage Support 21 | * Traffic Stats (default off) 22 | * One Time Links (default off) 23 | * Client Expiry (default off) 24 | * Prometheus metrics support 25 | 26 | ## Requirements 27 | 28 | * A host with Docker installed. 29 | 30 | ## Installation 31 | 32 | ### 1. Install Docker 33 | 34 | If you haven't installed Docker yet, install it by running: 35 | 36 | ```bash 37 | curl -sSL https://get.docker.com | sh 38 | sudo usermod -aG docker $(whoami) 39 | exit 40 | ``` 41 | 42 | And log in again. 43 | 44 | ### 2. Run AmneziaWG Easy 45 | 46 | To automatically install & run wg-easy, simply run: 47 | 48 | ``` 49 | docker run -d \ 50 | --name=amnezia-wg-easy \ 51 | -e LANG=en \ 52 | -e WG_HOST=<🚨YOUR_SERVER_IP> \ 53 | -e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \ 54 | -e PORT=51821 \ 55 | -e WG_PORT=51820 \ 56 | -v ~/.amnezia-wg-easy:/etc/wireguard \ 57 | -p 51820:51820/udp \ 58 | -p 51821:51821/tcp \ 59 | --cap-add=NET_ADMIN \ 60 | --cap-add=SYS_MODULE \ 61 | --sysctl="net.ipv4.conf.all.src_valid_mark=1" \ 62 | --sysctl="net.ipv4.ip_forward=1" \ 63 | --device=/dev/net/tun:/dev/net/tun \ 64 | --restart unless-stopped \ 65 | ghcr.io/w0rng/amnezia-wg-easy 66 | ``` 67 | 68 | > 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname. 69 | > 70 | > 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI. 71 | > See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash. 72 | 73 | The Web UI will now be available on `http://0.0.0.0:51821`. 74 | 75 | The Prometheus metrics will now be available on `http://0.0.0.0:51821/metrics`. Grafana dashboard [21733](https://grafana.com/grafana/dashboards/21733-wireguard/) 76 | 77 | > 💡 Your configuration files will be saved in `~/.amnezia-wg-easy` 78 | 79 | ## Options 80 | 81 | These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command. 82 | 83 | | Env | Default | Example | Description | 84 | |-------------------------------|-------------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 85 | | `PORT` | `51821` | `6789` | TCP port for Web UI. | 86 | | `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. | 87 | | `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. | 88 | | `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. | 89 | | `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. | 90 | | `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. | 91 | | `WG_CONFIG_PORT` | `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) | 92 | | `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. | 93 | | `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. | 94 | | `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. | 95 | | `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. | 96 | | `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. | 97 | | `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. | 98 | | `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. | 99 | | `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. | 100 | | `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. | 101 | | `WG_ENABLE_EXPIRES_TIME` | `false` | `true` | Enable expire time for clients | 102 | | `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). | 103 | | `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI | 104 | | `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart | 105 | | `DICEBEAR_TYPE` | `false` | `bottts` | see [dicebear types](https://www.dicebear.com/styles/) | 106 | | `USE_GRAVATAR` | `false` | `true` | Use or not GRAVATAR service | 107 | | `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) | 108 | | `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. | 109 | | `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name | 110 | | `ENABLE_PROMETHEUS_METRICS` | `false` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json` | 111 | | `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. | 112 | | `JC` | `random` | `5` | Junk packet count — number of packets with random data that are sent before the start of the session. | 113 | | `JMIN` | `50` | `25` | Junk packet minimum size — minimum packet size for Junk packet. That is, all randomly generated packets will have a size no smaller than Jmin. | 114 | | `JMAX` | `1000` | `250` | Junk packet maximum size — maximum size for Junk packets. | 115 | | `S1` | `random` | `75` | Init packet junk size — the size of random data that will be added to the init packet, the size of which is initially fixed. | 116 | | `S2` | `random` | `75` | Response packet junk size — the size of random data that will be added to the response packet, the size of which is initially fixed. | 117 | | `H1` | `random` | `1234567891` | Init packet magic header — the header of the first byte of the handshake. Must be < uint_max. | 118 | | `H2` | `random` | `1234567892` | Response packet magic header — header of the first byte of the handshake response. Must be < uint_max. | 119 | | `H3` | `random` | `1234567893` | Underload packet magic header — UnderLoad packet header. Must be < uint_max. | 120 | | `H4` | `random` | `1234567894` | Transport packet magic header — header of the packet of the data packet. Must be < uint_max. | 121 | 122 | > If you change `WG_PORT`, make sure to also change the exposed port. 123 | 124 | ## Updating 125 | 126 | To update to the latest version, simply run: 127 | 128 | ```bash 129 | docker stop amnezia-wg-easy 130 | docker rm amnezia-wg-easy 131 | docker pull ghcr.io/w0rng/amnezia-wg-easy 132 | ``` 133 | 134 | And then run the `docker run -d \ ...` command above again. 135 | 136 | ## Thanks 137 | 138 | Based on [wg-easy](https://github.com/wg-easy/wg-easy) by Emile Nijssen. 139 | Use integrations with AmneziaWg from [amnezia-wg-easy](https://github.com/spcfox/amnezia-wg-easy) by Viktor Yudov. 140 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w0rng/amnezia-wg-easy/139126743757157f6c51b10132c0b8cb3824735a/assets/screenshot.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | etc_wireguard: 3 | 4 | services: 5 | amnezia-wg-easy: 6 | env_file: 7 | - .env 8 | image: ghcr.io/w0rng/amnezia-wg-easy 9 | container_name: amnezia-wg-easy 10 | volumes: 11 | - etc_wireguard:/etc/wireguard 12 | ports: 13 | - "${WG_PORT}:${WG_PORT}/udp" 14 | - "${PORT}:${PORT}/tcp" 15 | restart: unless-stopped 16 | cap_add: 17 | - NET_ADMIN 18 | - SYS_MODULE 19 | # - NET_RAW # ⚠️ Uncomment if using Podman 20 | sysctls: 21 | - net.ipv4.ip_forward=1 22 | - net.ipv4.conf.all.src_valid_mark=1 23 | devices: 24 | - /dev/net/tun:/dev/net/tun 25 | -------------------------------------------------------------------------------- /docs/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "Initial version. Enjoy!", 3 | "2": "You can now rename a client & update the address. Enjoy!", 4 | "3": "Many improvements and small changes. Enjoy!", 5 | "4": "Now with pretty charts for client's network speed. Enjoy!", 6 | "5": "Many small improvements & feature requests. Enjoy!", 7 | "6": "Many small performance improvements & bug fixes. Enjoy!", 8 | "7": "Improved the look & performance of the upload/download chart.", 9 | "8": "Updated to Node.js v18.", 10 | "9": "Fixed issue running on devices with older kernels.", 11 | "10": "Added sessionless HTTP API auth & automatic dark mode.", 12 | "11": "Multilanguage Support & various bugfixes.", 13 | "12": "UI_TRAFFIC_STATS, Import json configurations with no PreShared-Key, allow clients with no privateKey & more.", 14 | "13": "New framework (h3), UI_CHART_TYPE, some bugfixes & more.", 15 | "14": "Home Assistent support, PASSWORD_HASH (inc. Helper), translation updates bugfixes & more." 16 | } 17 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wg-easy", 3 | "version": "1.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.1" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.1", 3 | "scripts": { 4 | "sudobuild": "DOCKER_BUILDKIT=1 sudo docker build --tag wg-easy .", 5 | "build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .", 6 | "serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up", 7 | "sudostart": "sudo docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy", 8 | "start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy" 9 | } 10 | } -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "athom", 3 | "ignorePatterns": [ 4 | "**/vendor/*.js" 5 | ], 6 | "rules": { 7 | "consistent-return": "off", 8 | "no-shadow": "off", 9 | "max-len": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { release: { version } } = require('./package.json'); 4 | 5 | module.exports.RELEASE = version; 6 | module.exports.PORT = process.env.PORT || '51821'; 7 | module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; 8 | module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH; 9 | module.exports.MAX_AGE = parseInt(process.env.MAX_AGE, 10) * 1000 * 60 || 0; 10 | module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/'; 11 | module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0'; 12 | module.exports.WG_HOST = process.env.WG_HOST; 13 | module.exports.WG_PORT = process.env.WG_PORT || '51820'; 14 | module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820'; 15 | module.exports.WG_MTU = process.env.WG_MTU || null; 16 | module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0'; 17 | module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x'; 18 | module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string' 19 | ? process.env.WG_DEFAULT_DNS 20 | : '1.1.1.1'; 21 | module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; 22 | 23 | module.exports.WG_PRE_UP = process.env.WG_PRE_UP || ''; 24 | module.exports.WG_POST_UP = process.env.WG_POST_UP || ` 25 | iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; 26 | iptables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; 27 | iptables -A FORWARD -i wg0 -j ACCEPT; 28 | iptables -A FORWARD -o wg0 -j ACCEPT; 29 | `.split('\n').join(' '); 30 | 31 | module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; 32 | module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ` 33 | iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; 34 | iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; 35 | iptables -D FORWARD -i wg0 -j ACCEPT; 36 | iptables -D FORWARD -o wg0 -j ACCEPT; 37 | `.split('\n').join(' '); 38 | module.exports.LANG = process.env.LANG || 'en'; 39 | module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false'; 40 | module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; 41 | module.exports.WG_ENABLE_ONE_TIME_LINKS = process.env.WG_ENABLE_ONE_TIME_LINKS || 'false'; 42 | module.exports.UI_ENABLE_SORT_CLIENTS = process.env.UI_ENABLE_SORT_CLIENTS || 'false'; 43 | module.exports.WG_ENABLE_EXPIRES_TIME = process.env.WG_ENABLE_EXPIRES_TIME || 'false'; 44 | module.exports.ENABLE_PROMETHEUS_METRICS = process.env.ENABLE_PROMETHEUS_METRICS || 'false'; 45 | module.exports.PROMETHEUS_METRICS_PASSWORD = process.env.PROMETHEUS_METRICS_PASSWORD; 46 | 47 | module.exports.DICEBEAR_TYPE = process.env.DICEBEAR_TYPE || false; 48 | module.exports.USE_GRAVATAR = process.env.USE_GRAVATAR || false; 49 | 50 | const getRandomInt = (min, max) => min + Math.floor(Math.random() * (max - min)); 51 | const getRandomJunkSize = () => getRandomInt(15, 150); 52 | const getRandomHeader = () => getRandomInt(1, 2_147_483_647); 53 | 54 | module.exports.JC = process.env.JC || getRandomInt(3, 10); 55 | module.exports.JMIN = process.env.JMIN || 50; 56 | module.exports.JMAX = process.env.JMAX || 1000; 57 | module.exports.S1 = process.env.S1 || getRandomJunkSize(); 58 | module.exports.S2 = process.env.S2 || getRandomJunkSize(); 59 | module.exports.H1 = process.env.H1 || getRandomHeader(); 60 | module.exports.H2 = process.env.H2 || getRandomHeader(); 61 | module.exports.H3 = process.env.H3 || getRandomHeader(); 62 | module.exports.H4 = process.env.H4 || getRandomHeader(); 63 | -------------------------------------------------------------------------------- /src/lib/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bcrypt = require('bcryptjs'); 4 | const crypto = require('node:crypto'); 5 | const basicAuth = require('basic-auth'); 6 | const { createServer } = require('node:http'); 7 | const { stat, readFile } = require('node:fs/promises'); 8 | const { resolve, sep } = require('node:path'); 9 | 10 | const expressSession = require('express-session'); 11 | const debug = require('debug')('Server'); 12 | 13 | const { 14 | createApp, 15 | createError, 16 | createRouter, 17 | defineEventHandler, 18 | fromNodeMiddleware, 19 | getRouterParam, 20 | toNodeListener, 21 | readBody, 22 | setHeader, 23 | serveStatic, 24 | } = require('h3'); 25 | 26 | const WireGuard = require('../services/WireGuard'); 27 | 28 | const { 29 | PORT, 30 | WEBUI_HOST, 31 | RELEASE, 32 | PASSWORD_HASH, 33 | MAX_AGE, 34 | LANG, 35 | UI_TRAFFIC_STATS, 36 | UI_CHART_TYPE, 37 | WG_ENABLE_ONE_TIME_LINKS, 38 | UI_ENABLE_SORT_CLIENTS, 39 | WG_ENABLE_EXPIRES_TIME, 40 | ENABLE_PROMETHEUS_METRICS, 41 | PROMETHEUS_METRICS_PASSWORD, 42 | DICEBEAR_TYPE, 43 | USE_GRAVATAR, 44 | } = require('../config'); 45 | 46 | const requiresPassword = !!PASSWORD_HASH; 47 | const requiresPrometheusPassword = !!PROMETHEUS_METRICS_PASSWORD; 48 | 49 | /** 50 | * Checks if `password` matches the PASSWORD_HASH. 51 | * 52 | * If environment variable is not set, the password is always invalid. 53 | * 54 | * @param {string} password String to test 55 | * @returns {boolean} true if matching environment, otherwise false 56 | */ 57 | const isPasswordValid = (password, hash) => { 58 | if (typeof password !== 'string') { 59 | return false; 60 | } 61 | if (hash) { 62 | return bcrypt.compareSync(password, hash); 63 | } 64 | 65 | return false; 66 | }; 67 | 68 | const cronJobEveryMinute = async () => { 69 | await WireGuard.cronJobEveryMinute(); 70 | setTimeout(cronJobEveryMinute, 60 * 1000); 71 | }; 72 | 73 | module.exports = class Server { 74 | 75 | constructor() { 76 | const app = createApp(); 77 | this.app = app; 78 | 79 | app.use(fromNodeMiddleware(expressSession({ 80 | secret: crypto.randomBytes(256).toString('hex'), 81 | resave: true, 82 | saveUninitialized: true, 83 | }))); 84 | 85 | const router = createRouter(); 86 | app.use(router); 87 | 88 | router 89 | .get('/api/release', defineEventHandler((event) => { 90 | setHeader(event, 'Content-Type', 'application/json'); 91 | return RELEASE; 92 | })) 93 | 94 | .get('/api/lang', defineEventHandler((event) => { 95 | setHeader(event, 'Content-Type', 'application/json'); 96 | return `"${LANG}"`; 97 | })) 98 | 99 | .get('/api/remember-me', defineEventHandler((event) => { 100 | setHeader(event, 'Content-Type', 'application/json'); 101 | return MAX_AGE > 0; 102 | })) 103 | 104 | .get('/api/ui-traffic-stats', defineEventHandler((event) => { 105 | setHeader(event, 'Content-Type', 'application/json'); 106 | return `${UI_TRAFFIC_STATS}`; 107 | })) 108 | 109 | .get('/api/ui-chart-type', defineEventHandler((event) => { 110 | setHeader(event, 'Content-Type', 'application/json'); 111 | return `"${UI_CHART_TYPE}"`; 112 | })) 113 | 114 | .get('/api/wg-enable-one-time-links', defineEventHandler((event) => { 115 | setHeader(event, 'Content-Type', 'application/json'); 116 | return `${WG_ENABLE_ONE_TIME_LINKS}`; 117 | })) 118 | 119 | .get('/api/ui-sort-clients', defineEventHandler((event) => { 120 | setHeader(event, 'Content-Type', 'application/json'); 121 | return `${UI_ENABLE_SORT_CLIENTS}`; 122 | })) 123 | 124 | .get('/api/wg-enable-expire-time', defineEventHandler((event) => { 125 | setHeader(event, 'Content-Type', 'application/json'); 126 | return `${WG_ENABLE_EXPIRES_TIME}`; 127 | })) 128 | 129 | .get('/api/ui-avatar-settings', defineEventHandler((event) => { 130 | setHeader(event, 'Content-Type', 'application/json'); 131 | return { 132 | dicebear: DICEBEAR_TYPE, 133 | gravatar: USE_GRAVATAR, 134 | } 135 | })) 136 | 137 | // Authentication 138 | .get('/api/session', defineEventHandler((event) => { 139 | const authenticated = requiresPassword 140 | ? !!(event.node.req.session && event.node.req.session.authenticated) 141 | : true; 142 | 143 | return { 144 | requiresPassword, 145 | authenticated, 146 | }; 147 | })) 148 | .get('/cnf/:clientOneTimeLink', defineEventHandler(async (event) => { 149 | if (WG_ENABLE_ONE_TIME_LINKS === 'false') { 150 | throw createError({ 151 | status: 404, 152 | message: 'Invalid state', 153 | }); 154 | } 155 | const clientOneTimeLink = getRouterParam(event, 'clientOneTimeLink'); 156 | const clients = await WireGuard.getClients(); 157 | const client = clients.find((client) => client.oneTimeLink === clientOneTimeLink); 158 | if (!client) return; 159 | const clientId = client.id; 160 | const config = await WireGuard.getClientConfiguration({ clientId }); 161 | await WireGuard.eraseOneTimeLink({ clientId }); 162 | setHeader(event, 'Content-Disposition', `attachment; filename="${clientOneTimeLink}.conf"`); 163 | setHeader(event, 'Content-Type', 'text/plain'); 164 | return config; 165 | })) 166 | .post('/api/session', defineEventHandler(async (event) => { 167 | const { password, remember } = await readBody(event); 168 | 169 | if (!requiresPassword) { 170 | // if no password is required, the API should never be called. 171 | // Do not automatically authenticate the user. 172 | throw createError({ 173 | status: 401, 174 | message: 'Invalid state', 175 | }); 176 | } 177 | 178 | if (!isPasswordValid(password, PASSWORD_HASH)) { 179 | throw createError({ 180 | status: 401, 181 | message: 'Incorrect Password', 182 | }); 183 | } 184 | 185 | if (MAX_AGE && remember) { 186 | event.node.req.session.cookie.maxAge = MAX_AGE; 187 | } 188 | event.node.req.session.authenticated = true; 189 | event.node.req.session.save(); 190 | 191 | debug(`New Session: ${event.node.req.session.id}`); 192 | 193 | return { success: true }; 194 | })); 195 | 196 | // WireGuard 197 | app.use( 198 | fromNodeMiddleware((req, res, next) => { 199 | if (!requiresPassword || !req.url.startsWith('/api/')) { 200 | return next(); 201 | } 202 | 203 | if (req.session && req.session.authenticated) { 204 | return next(); 205 | } 206 | 207 | if (req.url.startsWith('/api/') && req.headers['authorization']) { 208 | if (isPasswordValid(req.headers['authorization'], PASSWORD_HASH)) { 209 | return next(); 210 | } 211 | return res.status(401).json({ 212 | error: 'Incorrect Password', 213 | }); 214 | } 215 | 216 | return res.status(401).json({ 217 | error: 'Not Logged In', 218 | }); 219 | }), 220 | ); 221 | 222 | const router2 = createRouter(); 223 | app.use(router2); 224 | 225 | router2 226 | .delete('/api/session', defineEventHandler((event) => { 227 | const sessionId = event.node.req.session.id; 228 | 229 | event.node.req.session.destroy(); 230 | 231 | debug(`Deleted Session: ${sessionId}`); 232 | return { success: true }; 233 | })) 234 | .get('/api/wireguard/client', defineEventHandler(() => { 235 | return WireGuard.getClients(); 236 | })) 237 | .get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => { 238 | const clientId = getRouterParam(event, 'clientId'); 239 | const svg = await WireGuard.getClientQRCodeSVG({ clientId }); 240 | setHeader(event, 'Content-Type', 'image/svg+xml'); 241 | return svg; 242 | })) 243 | .get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => { 244 | const clientId = getRouterParam(event, 'clientId'); 245 | const client = await WireGuard.getClient({ clientId }); 246 | const config = await WireGuard.getClientConfiguration({ clientId }); 247 | const configName = client.name 248 | .replace(/[^a-zA-Z0-9_=+.-]/g, '-') 249 | .replace(/(-{2,}|-$)/g, '-') 250 | .replace(/-$/, '') 251 | .substring(0, 32); 252 | setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`); 253 | setHeader(event, 'Content-Type', 'text/plain'); 254 | return config; 255 | })) 256 | .post('/api/wireguard/client', defineEventHandler(async (event) => { 257 | const { name } = await readBody(event); 258 | const { expiredDate } = await readBody(event); 259 | await WireGuard.createClient({ name, expiredDate }); 260 | return { success: true }; 261 | })) 262 | .delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => { 263 | const clientId = getRouterParam(event, 'clientId'); 264 | await WireGuard.deleteClient({ clientId }); 265 | return { success: true }; 266 | })) 267 | .post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => { 268 | const clientId = getRouterParam(event, 'clientId'); 269 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { 270 | throw createError({ status: 403 }); 271 | } 272 | await WireGuard.enableClient({ clientId }); 273 | return { success: true }; 274 | })) 275 | .post('/api/wireguard/client/:clientId/generateOneTimeLink', defineEventHandler(async (event) => { 276 | if (WG_ENABLE_ONE_TIME_LINKS === 'false') { 277 | throw createError({ 278 | status: 404, 279 | message: 'Invalid state', 280 | }); 281 | } 282 | const clientId = getRouterParam(event, 'clientId'); 283 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { 284 | throw createError({ status: 403 }); 285 | } 286 | await WireGuard.generateOneTimeLink({ clientId }); 287 | return { success: true }; 288 | })) 289 | .post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => { 290 | const clientId = getRouterParam(event, 'clientId'); 291 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { 292 | throw createError({ status: 403 }); 293 | } 294 | await WireGuard.disableClient({ clientId }); 295 | return { success: true }; 296 | })) 297 | .put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => { 298 | const clientId = getRouterParam(event, 'clientId'); 299 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { 300 | throw createError({ status: 403 }); 301 | } 302 | const { name } = await readBody(event); 303 | await WireGuard.updateClientName({ clientId, name }); 304 | return { success: true }; 305 | })) 306 | .put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => { 307 | const clientId = getRouterParam(event, 'clientId'); 308 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { 309 | throw createError({ status: 403 }); 310 | } 311 | const { address } = await readBody(event); 312 | await WireGuard.updateClientAddress({ clientId, address }); 313 | return { success: true }; 314 | })) 315 | .put('/api/wireguard/client/:clientId/expireDate', defineEventHandler(async (event) => { 316 | const clientId = getRouterParam(event, 'clientId'); 317 | if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { 318 | throw createError({ status: 403 }); 319 | } 320 | const { expireDate } = await readBody(event); 321 | await WireGuard.updateClientExpireDate({ clientId, expireDate }); 322 | return { success: true }; 323 | })); 324 | 325 | const safePathJoin = (base, target) => { 326 | // Manage web root (edge case) 327 | if (target === '/') { 328 | return `${base}${sep}`; 329 | } 330 | 331 | // Prepend './' to prevent absolute paths 332 | const targetPath = `.${sep}${target}`; 333 | 334 | // Resolve the absolute path 335 | const resolvedPath = resolve(base, targetPath); 336 | 337 | // Check if resolvedPath is a subpath of base 338 | if (resolvedPath.startsWith(`${base}${sep}`)) { 339 | return resolvedPath; 340 | } 341 | 342 | throw createError({ 343 | status: 400, 344 | message: 'Bad Request', 345 | }); 346 | }; 347 | 348 | // Check Prometheus credentials 349 | app.use( 350 | fromNodeMiddleware((req, res, next) => { 351 | if (!requiresPrometheusPassword || !req.url.startsWith('/metrics')) { 352 | return next(); 353 | } 354 | const user = basicAuth(req); 355 | if (!user) { 356 | res.statusCode = 401; 357 | return { error: 'Not Logged In' }; 358 | } 359 | if (user.pass) { 360 | if (isPasswordValid(user.pass, PROMETHEUS_METRICS_PASSWORD)) { 361 | return next(); 362 | } 363 | res.statusCode = 401; 364 | return { error: 'Incorrect Password' }; 365 | } 366 | res.statusCode = 401; 367 | return { error: 'Not Logged In' }; 368 | }), 369 | ); 370 | 371 | // Prometheus Metrics API 372 | const routerPrometheusMetrics = createRouter(); 373 | app.use(routerPrometheusMetrics); 374 | 375 | // Prometheus Routes 376 | routerPrometheusMetrics 377 | .get('/metrics', defineEventHandler(async (event) => { 378 | setHeader(event, 'Content-Type', 'text/plain'); 379 | if (ENABLE_PROMETHEUS_METRICS === 'true') { 380 | return WireGuard.getMetrics(); 381 | } 382 | return ''; 383 | })) 384 | .get('/metrics/json', defineEventHandler(async (event) => { 385 | setHeader(event, 'Content-Type', 'application/json'); 386 | if (ENABLE_PROMETHEUS_METRICS === 'true') { 387 | return WireGuard.getMetricsJSON(); 388 | } 389 | return ''; 390 | })); 391 | 392 | // backup_restore 393 | const router3 = createRouter(); 394 | app.use(router3); 395 | 396 | router3 397 | .get('/api/wireguard/backup', defineEventHandler(async (event) => { 398 | const config = await WireGuard.backupConfiguration(); 399 | setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"'); 400 | setHeader(event, 'Content-Type', 'text/json'); 401 | return config; 402 | })) 403 | .put('/api/wireguard/restore', defineEventHandler(async (event) => { 404 | const { file } = await readBody(event); 405 | await WireGuard.restoreConfiguration(file); 406 | return { success: true }; 407 | })); 408 | 409 | // Static assets 410 | const publicDir = '/app/www'; 411 | app.use( 412 | defineEventHandler((event) => { 413 | return serveStatic(event, { 414 | getContents: (id) => { 415 | return readFile(safePathJoin(publicDir, id)); 416 | }, 417 | getMeta: async (id) => { 418 | const filePath = safePathJoin(publicDir, id); 419 | 420 | const stats = await stat(filePath).catch(() => {}); 421 | if (!stats || !stats.isFile()) { 422 | return; 423 | } 424 | 425 | if (id.endsWith('.html')) setHeader(event, 'Content-Type', 'text/html'); 426 | if (id.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript'); 427 | if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json'); 428 | if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css'); 429 | if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png'); 430 | if (id.endsWith('.svg')) setHeader(event, 'Content-Type', 'image/svg+xml'); 431 | 432 | return { 433 | size: stats.size, 434 | mtime: stats.mtimeMs, 435 | }; 436 | }, 437 | }); 438 | }), 439 | ); 440 | 441 | createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST); 442 | debug(`Listening on http://${WEBUI_HOST}:${PORT}`); 443 | 444 | cronJobEveryMinute(); 445 | } 446 | 447 | }; 448 | -------------------------------------------------------------------------------- /src/lib/ServerError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class ServerError extends Error { 4 | 5 | constructor(message, statusCode = 500) { 6 | super(message); 7 | this.statusCode = statusCode; 8 | } 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/Util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | 5 | module.exports = class Util { 6 | 7 | static isValidIPv4(str) { 8 | const blocks = str.split('.'); 9 | if (blocks.length !== 4) return false; 10 | 11 | for (let value of blocks) { 12 | value = parseInt(value, 10); 13 | if (Number.isNaN(value)) return false; 14 | if (value < 0 || value > 255) return false; 15 | } 16 | 17 | return true; 18 | } 19 | 20 | static promisify(fn) { 21 | // eslint-disable-next-line func-names 22 | return function(req, res) { 23 | Promise.resolve().then(async () => fn(req, res)) 24 | .then((result) => { 25 | if (res.headersSent) return; 26 | 27 | if (typeof result === 'undefined') { 28 | return res 29 | .status(204) 30 | .end(); 31 | } 32 | 33 | return res 34 | .status(200) 35 | .json(result); 36 | }) 37 | .catch((error) => { 38 | if (typeof error === 'string') { 39 | error = new Error(error); 40 | } 41 | 42 | // eslint-disable-next-line no-console 43 | console.error(error); 44 | 45 | return res 46 | .status(error.statusCode || 500) 47 | .json({ 48 | error: error.message || error.toString(), 49 | stack: error.stack, 50 | }); 51 | }); 52 | }; 53 | } 54 | 55 | static async exec(cmd, { 56 | log = true, 57 | } = {}) { 58 | if (typeof log === 'string') { 59 | // eslint-disable-next-line no-console 60 | console.log(`$ ${log}`); 61 | } else if (log === true) { 62 | // eslint-disable-next-line no-console 63 | console.log(`$ ${cmd}`); 64 | } 65 | 66 | if (process.platform !== 'linux') { 67 | return ''; 68 | } 69 | 70 | return new Promise((resolve, reject) => { 71 | childProcess.exec(cmd, { 72 | shell: 'bash', 73 | }, (err, stdout) => { 74 | if (err) return reject(err); 75 | return resolve(String(stdout).trim()); 76 | }); 77 | }); 78 | } 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /src/lib/WireGuard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs/promises'); 4 | const path = require('path'); 5 | const debug = require('debug')('WireGuard'); 6 | const crypto = require('node:crypto'); 7 | const QRCode = require('qrcode'); 8 | const CRC32 = require('crc-32'); 9 | 10 | const Util = require('./Util'); 11 | const ServerError = require('./ServerError'); 12 | 13 | const { 14 | WG_PATH, 15 | WG_HOST, 16 | WG_PORT, 17 | WG_CONFIG_PORT, 18 | WG_MTU, 19 | WG_DEFAULT_DNS, 20 | WG_DEFAULT_ADDRESS, 21 | WG_PERSISTENT_KEEPALIVE, 22 | WG_ALLOWED_IPS, 23 | WG_PRE_UP, 24 | WG_POST_UP, 25 | WG_PRE_DOWN, 26 | WG_POST_DOWN, 27 | WG_ENABLE_EXPIRES_TIME, 28 | WG_ENABLE_ONE_TIME_LINKS, 29 | JC, 30 | JMIN, 31 | JMAX, 32 | S1, 33 | S2, 34 | H1, 35 | H2, 36 | H3, 37 | H4, 38 | } = require('../config'); 39 | 40 | module.exports = class WireGuard { 41 | 42 | async __buildConfig() { 43 | this.__configPromise = Promise.resolve().then(async () => { 44 | if (!WG_HOST) { 45 | throw new Error('WG_HOST Environment Variable Not Set!'); 46 | } 47 | 48 | debug('Loading configuration...'); 49 | let config; 50 | try { 51 | config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8'); 52 | config = JSON.parse(config); 53 | debug('Configuration loaded.'); 54 | } catch (err) { 55 | const privateKey = await Util.exec('wg genkey'); 56 | const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, { 57 | log: 'echo ***hidden*** | wg pubkey', 58 | }); 59 | const address = WG_DEFAULT_ADDRESS.replace('x', '1'); 60 | 61 | config = { 62 | server: { 63 | privateKey, 64 | publicKey, 65 | address, 66 | jc: JC, 67 | jmin: JMIN, 68 | jmax: JMAX, 69 | s1: S1, 70 | s2: S2, 71 | h1: H1, 72 | h2: H2, 73 | h3: H3, 74 | h4: H4, 75 | }, 76 | clients: {}, 77 | }; 78 | debug('Configuration generated.'); 79 | } 80 | 81 | return config; 82 | }); 83 | 84 | return this.__configPromise; 85 | } 86 | 87 | async getConfig() { 88 | if (!this.__configPromise) { 89 | const config = await this.__buildConfig(); 90 | 91 | await this.__saveConfig(config); 92 | await Util.exec('wg-quick down wg0').catch(() => {}); 93 | await Util.exec('wg-quick up wg0').catch((err) => { 94 | if (err && err.message && err.message.includes('Cannot find device "wg0"')) { 95 | throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!'); 96 | } 97 | 98 | throw err; 99 | }); 100 | // await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`); 101 | // await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT'); 102 | // await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT'); 103 | // await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT'); 104 | await this.__syncConfig(); 105 | } 106 | 107 | return this.__configPromise; 108 | } 109 | 110 | async saveConfig() { 111 | const config = await this.getConfig(); 112 | await this.__saveConfig(config); 113 | await this.__syncConfig(); 114 | } 115 | 116 | async __saveConfig(config) { 117 | let result = ` 118 | # Note: Do not edit this file directly. 119 | # Your changes will be overwritten! 120 | 121 | # Server 122 | [Interface] 123 | PrivateKey = ${config.server.privateKey} 124 | Address = ${config.server.address}/24 125 | ListenPort = ${WG_PORT} 126 | PreUp = ${WG_PRE_UP} 127 | PostUp = ${WG_POST_UP} 128 | PreDown = ${WG_PRE_DOWN} 129 | PostDown = ${WG_POST_DOWN} 130 | Jc = ${config.server.jc} 131 | Jmin = ${config.server.jmin} 132 | Jmax = ${config.server.jmax} 133 | S1 = ${config.server.s1} 134 | S2 = ${config.server.s2} 135 | H1 = ${config.server.h1} 136 | H2 = ${config.server.h2} 137 | H3 = ${config.server.h3} 138 | H4 = ${config.server.h4} 139 | `; 140 | 141 | for (const [clientId, client] of Object.entries(config.clients)) { 142 | if (!client.enabled) continue; 143 | 144 | result += ` 145 | 146 | # Client: ${client.name} (${clientId}) 147 | [Peer] 148 | PublicKey = ${client.publicKey} 149 | ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' 150 | }AllowedIPs = ${client.address}/32`; 151 | } 152 | 153 | debug('Config saving...'); 154 | await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(config, false, 2), { 155 | mode: 0o660, 156 | }); 157 | await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, { 158 | mode: 0o600, 159 | }); 160 | debug('Config saved.'); 161 | } 162 | 163 | async __syncConfig() { 164 | debug('Config syncing...'); 165 | await Util.exec('wg syncconf wg0 <(wg-quick strip wg0)'); 166 | debug('Config synced.'); 167 | } 168 | 169 | async getClients() { 170 | const config = await this.getConfig(); 171 | const clients = Object.entries(config.clients).map(([clientId, client]) => ({ 172 | id: clientId, 173 | name: client.name, 174 | enabled: client.enabled, 175 | address: client.address, 176 | publicKey: client.publicKey, 177 | createdAt: new Date(client.createdAt), 178 | updatedAt: new Date(client.updatedAt), 179 | expiredAt: client.expiredAt !== null 180 | ? new Date(client.expiredAt) 181 | : null, 182 | allowedIPs: client.allowedIPs, 183 | oneTimeLink: client.oneTimeLink ?? null, 184 | oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null, 185 | downloadableConfig: 'privateKey' in client, 186 | persistentKeepalive: null, 187 | latestHandshakeAt: null, 188 | transferRx: null, 189 | transferTx: null, 190 | endpoint: null, 191 | })); 192 | 193 | // Loop WireGuard status 194 | const dump = await Util.exec('wg show wg0 dump', { 195 | log: false, 196 | }); 197 | dump 198 | .trim() 199 | .split('\n') 200 | .slice(1) 201 | .forEach((line) => { 202 | const [ 203 | publicKey, 204 | preSharedKey, // eslint-disable-line no-unused-vars 205 | endpoint, // eslint-disable-line no-unused-vars 206 | allowedIps, // eslint-disable-line no-unused-vars 207 | latestHandshakeAt, 208 | transferRx, 209 | transferTx, 210 | persistentKeepalive, 211 | ] = line.split('\t'); 212 | 213 | const client = clients.find((client) => client.publicKey === publicKey); 214 | if (!client) return; 215 | 216 | client.latestHandshakeAt = latestHandshakeAt === '0' 217 | ? null 218 | : new Date(Number(`${latestHandshakeAt}000`)); 219 | client.endpoint = endpoint === '(none)' ? null : endpoint; 220 | client.transferRx = Number(transferRx); 221 | client.transferTx = Number(transferTx); 222 | client.persistentKeepalive = persistentKeepalive; 223 | }); 224 | 225 | return clients; 226 | } 227 | 228 | async getClient({ clientId }) { 229 | const config = await this.getConfig(); 230 | const client = config.clients[clientId]; 231 | if (!client) { 232 | throw new ServerError(`Client Not Found: ${clientId}`, 404); 233 | } 234 | 235 | return client; 236 | } 237 | 238 | async getClientConfiguration({ clientId }) { 239 | const config = await this.getConfig(); 240 | const client = await this.getClient({ clientId }); 241 | 242 | return ` 243 | [Interface] 244 | PrivateKey = ${client.privateKey ? `${client.privateKey}` : 'REPLACE_ME'} 245 | Address = ${client.address}/24 246 | ${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\ 247 | ${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\ 248 | Jc = ${config.server.jc} 249 | Jmin = ${config.server.jmin} 250 | Jmax = ${config.server.jmax} 251 | S1 = ${config.server.s1} 252 | S2 = ${config.server.s2} 253 | H1 = ${config.server.h1} 254 | H2 = ${config.server.h2} 255 | H3 = ${config.server.h3} 256 | H4 = ${config.server.h4} 257 | 258 | [Peer] 259 | PublicKey = ${config.server.publicKey} 260 | ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' 261 | }AllowedIPs = ${WG_ALLOWED_IPS} 262 | PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE} 263 | Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; 264 | } 265 | 266 | async getClientQRCodeSVG({ clientId }) { 267 | const config = await this.getClientConfiguration({ clientId }); 268 | return QRCode.toString(config, { 269 | type: 'svg', 270 | width: 512, 271 | }); 272 | } 273 | 274 | async createClient({ name, expiredDate }) { 275 | if (!name) { 276 | throw new Error('Missing: Name'); 277 | } 278 | 279 | const config = await this.getConfig(); 280 | 281 | const privateKey = await Util.exec('wg genkey'); 282 | const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, { 283 | log: 'echo ***hidden*** | wg pubkey', 284 | }); 285 | const preSharedKey = await Util.exec('wg genpsk'); 286 | 287 | // Calculate next IP 288 | let address; 289 | for (let i = 2; i < 255; i++) { 290 | const client = Object.values(config.clients).find((client) => { 291 | return client.address === WG_DEFAULT_ADDRESS.replace('x', i); 292 | }); 293 | 294 | if (!client) { 295 | address = WG_DEFAULT_ADDRESS.replace('x', i); 296 | break; 297 | } 298 | } 299 | 300 | if (!address) { 301 | throw new Error('Maximum number of clients reached.'); 302 | } 303 | // Create Client 304 | const id = crypto.randomUUID(); 305 | const client = { 306 | id, 307 | name, 308 | address, 309 | privateKey, 310 | publicKey, 311 | preSharedKey, 312 | 313 | createdAt: new Date(), 314 | updatedAt: new Date(), 315 | expiredAt: null, 316 | enabled: true, 317 | }; 318 | if (expiredDate) { 319 | client.expiredAt = new Date(expiredDate); 320 | client.expiredAt.setHours(23); 321 | client.expiredAt.setMinutes(59); 322 | client.expiredAt.setSeconds(59); 323 | } 324 | config.clients[id] = client; 325 | 326 | await this.saveConfig(); 327 | 328 | return client; 329 | } 330 | 331 | async deleteClient({ clientId }) { 332 | const config = await this.getConfig(); 333 | 334 | if (config.clients[clientId]) { 335 | delete config.clients[clientId]; 336 | await this.saveConfig(); 337 | } 338 | } 339 | 340 | async enableClient({ clientId }) { 341 | const client = await this.getClient({ clientId }); 342 | 343 | client.enabled = true; 344 | client.updatedAt = new Date(); 345 | 346 | await this.saveConfig(); 347 | } 348 | 349 | async generateOneTimeLink({ clientId }) { 350 | const client = await this.getClient({ clientId }); 351 | const key = `${clientId}-${Math.floor(Math.random() * 1000)}`; 352 | client.oneTimeLink = Math.abs(CRC32.str(key)).toString(16); 353 | client.oneTimeLinkExpiresAt = new Date(Date.now() + 5 * 60 * 1000); 354 | client.updatedAt = new Date(); 355 | await this.saveConfig(); 356 | } 357 | 358 | async eraseOneTimeLink({ clientId }) { 359 | const client = await this.getClient({ clientId }); 360 | // client.oneTimeLink = null; 361 | client.oneTimeLinkExpiresAt = new Date(Date.now() + 10 * 1000); 362 | client.updatedAt = new Date(); 363 | await this.saveConfig(); 364 | } 365 | 366 | async disableClient({ clientId }) { 367 | const client = await this.getClient({ clientId }); 368 | 369 | client.enabled = false; 370 | client.updatedAt = new Date(); 371 | 372 | await this.saveConfig(); 373 | } 374 | 375 | async updateClientName({ clientId, name }) { 376 | const client = await this.getClient({ clientId }); 377 | 378 | client.name = name; 379 | client.updatedAt = new Date(); 380 | 381 | await this.saveConfig(); 382 | } 383 | 384 | async updateClientAddress({ clientId, address }) { 385 | const client = await this.getClient({ clientId }); 386 | 387 | if (!Util.isValidIPv4(address)) { 388 | throw new ServerError(`Invalid Address: ${address}`, 400); 389 | } 390 | 391 | client.address = address; 392 | client.updatedAt = new Date(); 393 | 394 | await this.saveConfig(); 395 | } 396 | 397 | async updateClientExpireDate({ clientId, expireDate }) { 398 | const client = await this.getClient({ clientId }); 399 | 400 | if (expireDate) { 401 | client.expiredAt = new Date(expireDate); 402 | client.expiredAt.setHours(23); 403 | client.expiredAt.setMinutes(59); 404 | client.expiredAt.setSeconds(59); 405 | } else { 406 | client.expiredAt = null; 407 | } 408 | client.updatedAt = new Date(); 409 | 410 | await this.saveConfig(); 411 | } 412 | 413 | async __reloadConfig() { 414 | await this.__buildConfig(); 415 | await this.__syncConfig(); 416 | } 417 | 418 | async restoreConfiguration(config) { 419 | debug('Starting configuration restore process.'); 420 | const _config = JSON.parse(config); 421 | await this.__saveConfig(_config); 422 | await this.__reloadConfig(); 423 | debug('Configuration restore process completed.'); 424 | } 425 | 426 | async backupConfiguration() { 427 | debug('Starting configuration backup.'); 428 | const config = await this.getConfig(); 429 | const backup = JSON.stringify(config, null, 2); 430 | debug('Configuration backup completed.'); 431 | return backup; 432 | } 433 | 434 | // Shutdown wireguard 435 | async Shutdown() { 436 | await Util.exec('wg-quick down wg0').catch(() => {}); 437 | } 438 | 439 | async cronJobEveryMinute() { 440 | const config = await this.getConfig(); 441 | let needSaveConfig = false; 442 | // Expires Feature 443 | if (WG_ENABLE_EXPIRES_TIME === 'true') { 444 | for (const client of Object.values(config.clients)) { 445 | if (client.enabled !== true) continue; 446 | if (client.expiredAt !== null && new Date() > new Date(client.expiredAt)) { 447 | debug(`Client ${client.id} expired.`); 448 | needSaveConfig = true; 449 | client.enabled = false; 450 | client.updatedAt = new Date(); 451 | } 452 | } 453 | } 454 | // One Time Link Feature 455 | if (WG_ENABLE_ONE_TIME_LINKS === 'true') { 456 | for (const client of Object.values(config.clients)) { 457 | if (client.oneTimeLink !== null && new Date() > new Date(client.oneTimeLinkExpiresAt)) { 458 | debug(`Client ${client.id} One Time Link expired.`); 459 | needSaveConfig = true; 460 | client.oneTimeLink = null; 461 | client.oneTimeLinkExpiresAt = null; 462 | client.updatedAt = new Date(); 463 | } 464 | } 465 | } 466 | if (needSaveConfig) { 467 | await this.saveConfig(); 468 | } 469 | } 470 | 471 | async getMetrics() { 472 | const clients = await this.getClients(); 473 | let wireguardPeerCount = 0; 474 | let wireguardEnabledPeersCount = 0; 475 | let wireguardConnectedPeersCount = 0; 476 | let wireguardSentBytes = ''; 477 | let wireguardReceivedBytes = ''; 478 | let wireguardLatestHandshakeSeconds = ''; 479 | for (const client of Object.values(clients)) { 480 | wireguardPeerCount++; 481 | if (client.enabled === true) { 482 | wireguardEnabledPeersCount++; 483 | } 484 | if (client.endpoint !== null) { 485 | wireguardConnectedPeersCount++; 486 | } 487 | wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferTx)}\n`; 488 | wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferRx)}\n`; 489 | wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`; 490 | } 491 | 492 | let returnText = '# HELP wg-easy and wireguard metrics\n'; 493 | 494 | returnText += '\n# HELP wireguard_configured_peers\n'; 495 | returnText += '# TYPE wireguard_configured_peers gauge\n'; 496 | returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`; 497 | 498 | returnText += '\n# HELP wireguard_enabled_peers\n'; 499 | returnText += '# TYPE wireguard_enabled_peers gauge\n'; 500 | returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`; 501 | 502 | returnText += '\n# HELP wireguard_connected_peers\n'; 503 | returnText += '# TYPE wireguard_connected_peers gauge\n'; 504 | returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`; 505 | 506 | returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n'; 507 | returnText += '# TYPE wireguard_sent_bytes counter\n'; 508 | returnText += `${wireguardSentBytes}`; 509 | 510 | returnText += '\n# HELP wireguard_received_bytes Bytes received from the peer\n'; 511 | returnText += '# TYPE wireguard_received_bytes counter\n'; 512 | returnText += `${wireguardReceivedBytes}`; 513 | 514 | returnText += '\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n'; 515 | returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n'; 516 | returnText += `${wireguardLatestHandshakeSeconds}`; 517 | 518 | return returnText; 519 | } 520 | 521 | async getMetricsJSON() { 522 | const clients = await this.getClients(); 523 | let wireguardPeerCount = 0; 524 | let wireguardEnabledPeersCount = 0; 525 | let wireguardConnectedPeersCount = 0; 526 | for (const client of Object.values(clients)) { 527 | wireguardPeerCount++; 528 | if (client.enabled === true) { 529 | wireguardEnabledPeersCount++; 530 | } 531 | if (client.endpoint !== null) { 532 | wireguardConnectedPeersCount++; 533 | } 534 | } 535 | return { 536 | wireguard_configured_peers: Number(wireguardPeerCount), 537 | wireguard_enabled_peers: Number(wireguardEnabledPeersCount), 538 | wireguard_connected_peers: Number(wireguardConnectedPeersCount), 539 | }; 540 | } 541 | 542 | }; 543 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "release": { 3 | "version": "14" 4 | }, 5 | "name": "wg-easy", 6 | "version": "1.0.1", 7 | "description": "The easiest way to run WireGuard VPN + Web-based Admin UI.", 8 | "main": "server.js", 9 | "scripts": { 10 | "serve": "DEBUG=Server,WireGuard npx nodemon server.js", 11 | "serve-with-password": "PASSWORD=wg npm run serve", 12 | "lint": "eslint .", 13 | "buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css" 14 | }, 15 | "author": "Emile Nijssen", 16 | "license": "CC BY-NC-SA 4.0", 17 | "dependencies": { 18 | "basic-auth": "^2.0.1", 19 | "bcryptjs": "^2.4.3", 20 | "crc-32": "^1.2.2", 21 | "debug": "^4.3.7", 22 | "express-session": "^1.18.0", 23 | "h3": "^1.12.0", 24 | "qrcode": "^1.5.4" 25 | }, 26 | "devDependencies": { 27 | "@tailwindcss/forms": "^0.5.9", 28 | "eslint-config-athom": "^3.1.3", 29 | "nodemon": "^3.1.4", 30 | "tailwindcss": "^3.4.10" 31 | }, 32 | "nodemonConfig": { 33 | "ignore": [ 34 | "www/*" 35 | ] 36 | }, 37 | "engines": { 38 | "node": ">=18" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./services/Server'); 4 | 5 | const WireGuard = require('./services/WireGuard'); 6 | 7 | WireGuard.getConfig() 8 | .catch((err) => { 9 | // eslint-disable-next-line no-console 10 | console.error(err); 11 | 12 | // eslint-disable-next-line no-process-exit 13 | process.exit(1); 14 | }); 15 | 16 | // Handle terminate signal 17 | process.on('SIGTERM', async () => { 18 | // eslint-disable-next-line no-console 19 | console.log('SIGTERM signal received.'); 20 | await WireGuard.Shutdown(); 21 | // eslint-disable-next-line no-process-exit 22 | process.exit(0); 23 | }); 24 | 25 | // Handle interrupt signal 26 | process.on('SIGINT', () => { 27 | // eslint-disable-next-line no-console 28 | console.log('SIGINT signal received.'); 29 | }); 30 | -------------------------------------------------------------------------------- /src/services/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Server = require('../lib/Server'); 4 | 5 | module.exports = new Server(); 6 | -------------------------------------------------------------------------------- /src/services/WireGuard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WireGuard = require('../lib/WireGuard'); 4 | 5 | module.exports = new WireGuard(); 6 | -------------------------------------------------------------------------------- /src/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | darkMode: 'selector', 7 | content: ['./www/**/*.{html,js}'], 8 | theme: { 9 | screens: { 10 | xxs: '450px', 11 | xs: '576px', 12 | sm: '640px', 13 | md: '768px', 14 | lg: '1024px', 15 | xl: '1280px', 16 | '2xl': '1536px', 17 | }, 18 | }, 19 | plugins: [ 20 | function addDisabledClass({ addUtilities }) { 21 | const newUtilities = { 22 | '.is-disabled': { 23 | opacity: '0.25', 24 | cursor: 'default', 25 | }, 26 | }; 27 | addUtilities(newUtilities); 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /src/wgpw.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Import needed libraries 4 | import bcrypt from 'bcryptjs'; 5 | import { Writable } from 'stream'; 6 | import readline from 'readline'; 7 | 8 | // Function to generate hash 9 | const generateHash = async (password) => { 10 | try { 11 | const salt = await bcrypt.genSalt(12); 12 | const hash = await bcrypt.hash(password, salt); 13 | // eslint-disable-next-line no-console 14 | console.log(`PASSWORD_HASH='${hash}'`); 15 | } catch (error) { 16 | throw new Error(`Failed to generate hash : ${error}`); 17 | } 18 | }; 19 | 20 | // Function to compare password with hash 21 | const comparePassword = async (password, hash) => { 22 | try { 23 | const match = await bcrypt.compare(password, hash); 24 | if (match) { 25 | // eslint-disable-next-line no-console 26 | console.log('Password matches the hash !'); 27 | } else { 28 | // eslint-disable-next-line no-console 29 | console.log('Password does not match the hash.'); 30 | } 31 | } catch (error) { 32 | throw new Error(`Failed to compare password and hash : ${error}`); 33 | } 34 | }; 35 | 36 | const readStdinPassword = () => { 37 | return new Promise((resolve) => { 38 | process.stdout.write('Enter your password: '); 39 | 40 | const rl = readline.createInterface({ 41 | input: process.stdin, 42 | output: new Writable({ 43 | write(_chunk, _encoding, callback) { 44 | callback(); 45 | }, 46 | }), 47 | terminal: true, 48 | }); 49 | 50 | rl.question('', (answer) => { 51 | rl.close(); 52 | // Print a new line after password prompt 53 | process.stdout.write('\n'); 54 | resolve(answer); 55 | }); 56 | }); 57 | }; 58 | 59 | (async () => { 60 | try { 61 | // Retrieve command line arguments 62 | const args = process.argv.slice(2); // Ignore the first two arguments 63 | if (args.length > 2) { 64 | throw new Error('Usage : wgpw [YOUR_PASSWORD] [HASH]'); 65 | } 66 | 67 | const [password, hash] = args; 68 | if (password && hash) { 69 | await comparePassword(password, hash); 70 | } else if (password) { 71 | await generateHash(password); 72 | } else { 73 | const password = await readStdinPassword(); 74 | await generateHash(password); 75 | } 76 | } catch (error) { 77 | // eslint-disable-next-line no-console 78 | console.error(error); 79 | // eslint-disable-next-line no-process-exit 80 | process.exit(1); 81 | } 82 | })(); 83 | -------------------------------------------------------------------------------- /src/wgpw.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is intended to be run only inside a docker container, not on the development host machine 3 | set -e 4 | # proxy command 5 | node /app/wgpw.mjs "$@" -------------------------------------------------------------------------------- /src/www/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w0rng/amnezia-wg-easy/139126743757157f6c51b10132c0b8cb3824735a/src/www/img/apple-touch-icon.png -------------------------------------------------------------------------------- /src/www/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w0rng/amnezia-wg-easy/139126743757157f6c51b10132c0b8cb3824735a/src/www/img/favicon.ico -------------------------------------------------------------------------------- /src/www/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w0rng/amnezia-wg-easy/139126743757157f6c51b10132c0b8cb3824735a/src/www/img/logo.png -------------------------------------------------------------------------------- /src/www/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/www/js/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-undef */ 3 | 4 | 'use strict'; 5 | 6 | class API { 7 | 8 | async call({ method, path, body }) { 9 | const res = await fetch(`./api${path}`, { 10 | method, 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: body 15 | ? JSON.stringify(body) 16 | : undefined, 17 | }); 18 | 19 | if (res.status === 204) { 20 | return undefined; 21 | } 22 | 23 | const json = await res.json(); 24 | 25 | if (!res.ok) { 26 | throw new Error(json.error || res.statusText); 27 | } 28 | 29 | return json; 30 | } 31 | 32 | async getRelease() { 33 | return this.call({ 34 | method: 'get', 35 | path: '/release', 36 | }); 37 | } 38 | 39 | async getLang() { 40 | return this.call({ 41 | method: 'get', 42 | path: '/lang', 43 | }); 44 | } 45 | 46 | async getRememberMeEnabled() { 47 | return this.call({ 48 | method: 'get', 49 | path: '/remember-me', 50 | }); 51 | } 52 | 53 | async getuiTrafficStats() { 54 | return this.call({ 55 | method: 'get', 56 | path: '/ui-traffic-stats', 57 | }); 58 | } 59 | 60 | async getChartType() { 61 | return this.call({ 62 | method: 'get', 63 | path: '/ui-chart-type', 64 | }); 65 | } 66 | 67 | async getWGEnableOneTimeLinks() { 68 | return this.call({ 69 | method: 'get', 70 | path: '/wg-enable-one-time-links', 71 | }); 72 | } 73 | 74 | async getWGEnableExpireTime() { 75 | return this.call({ 76 | method: 'get', 77 | path: '/wg-enable-expire-time', 78 | }); 79 | } 80 | 81 | async getAvatarSettings() { 82 | return this.call({ 83 | method: 'get', 84 | path: '/ui-avatar-settings', 85 | }); 86 | } 87 | 88 | async getSession() { 89 | return this.call({ 90 | method: 'get', 91 | path: '/session', 92 | }); 93 | } 94 | 95 | async createSession({ password, remember }) { 96 | return this.call({ 97 | method: 'post', 98 | path: '/session', 99 | body: { password, remember }, 100 | }); 101 | } 102 | 103 | async deleteSession() { 104 | return this.call({ 105 | method: 'delete', 106 | path: '/session', 107 | }); 108 | } 109 | 110 | async getClients() { 111 | return this.call({ 112 | method: 'get', 113 | path: '/wireguard/client', 114 | }).then((clients) => clients.map((client) => ({ 115 | ...client, 116 | createdAt: new Date(client.createdAt), 117 | updatedAt: new Date(client.updatedAt), 118 | expiredAt: client.expiredAt !== null 119 | ? new Date(client.expiredAt) 120 | : null, 121 | latestHandshakeAt: client.latestHandshakeAt !== null 122 | ? new Date(client.latestHandshakeAt) 123 | : null, 124 | }))); 125 | } 126 | 127 | async createClient({ name, expiredDate }) { 128 | return this.call({ 129 | method: 'post', 130 | path: '/wireguard/client', 131 | body: { name, expiredDate }, 132 | }); 133 | } 134 | 135 | async deleteClient({ clientId }) { 136 | return this.call({ 137 | method: 'delete', 138 | path: `/wireguard/client/${clientId}`, 139 | }); 140 | } 141 | 142 | async showOneTimeLink({ clientId }) { 143 | return this.call({ 144 | method: 'post', 145 | path: `/wireguard/client/${clientId}/generateOneTimeLink`, 146 | }); 147 | } 148 | 149 | async enableClient({ clientId }) { 150 | return this.call({ 151 | method: 'post', 152 | path: `/wireguard/client/${clientId}/enable`, 153 | }); 154 | } 155 | 156 | async disableClient({ clientId }) { 157 | return this.call({ 158 | method: 'post', 159 | path: `/wireguard/client/${clientId}/disable`, 160 | }); 161 | } 162 | 163 | async updateClientName({ clientId, name }) { 164 | return this.call({ 165 | method: 'put', 166 | path: `/wireguard/client/${clientId}/name/`, 167 | body: { name }, 168 | }); 169 | } 170 | 171 | async updateClientAddress({ clientId, address }) { 172 | return this.call({ 173 | method: 'put', 174 | path: `/wireguard/client/${clientId}/address/`, 175 | body: { address }, 176 | }); 177 | } 178 | 179 | async updateClientExpireDate({ clientId, expireDate }) { 180 | return this.call({ 181 | method: 'put', 182 | path: `/wireguard/client/${clientId}/expireDate/`, 183 | body: { expireDate }, 184 | }); 185 | } 186 | 187 | async restoreConfiguration(file) { 188 | return this.call({ 189 | method: 'put', 190 | path: '/wireguard/restore', 191 | body: { file }, 192 | }); 193 | } 194 | 195 | async getUiSortClients() { 196 | return this.call({ 197 | method: 'get', 198 | path: '/ui-sort-clients', 199 | }); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/www/js/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-alert */ 3 | /* eslint-disable no-undef */ 4 | /* eslint-disable no-new */ 5 | 6 | 'use strict'; 7 | 8 | function bytes(bytes, decimals, kib, maxunit) { 9 | kib = kib || false; 10 | if (bytes === 0) return '0 B'; 11 | if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'NaN'; 12 | const k = kib ? 1024 : 1000; 13 | const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2; 14 | const sizes = kib 15 | ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB'] 16 | : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB']; 17 | let i = Math.floor(Math.log(bytes) / Math.log(k)); 18 | if (maxunit !== undefined) { 19 | const index = sizes.indexOf(maxunit); 20 | if (index !== -1) i = index; 21 | } 22 | // eslint-disable-next-line no-restricted-properties 23 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 24 | } 25 | 26 | /** 27 | * Sorts an array of objects by a specified property in ascending or descending order. 28 | * 29 | * @param {Array} array - The array of objects to be sorted. 30 | * @param {string} property - The property to sort the array by. 31 | * @param {boolean} [sort=true] - Whether to sort the array in ascending (default) or descending order. 32 | * @return {Array} - The sorted array of objects. 33 | */ 34 | function sortByProperty(array, property, sort = true) { 35 | if (sort) { 36 | return array.sort((a, b) => (typeof a[property] === 'string' ? a[property].localeCompare(b[property]) : a[property] - b[property])); 37 | } 38 | 39 | return array.sort((a, b) => (typeof a[property] === 'string' ? b[property].localeCompare(a[property]) : b[property] - a[property])); 40 | } 41 | 42 | const i18n = new VueI18n({ 43 | locale: localStorage.getItem('lang') || 'en', 44 | fallbackLocale: 'en', 45 | messages, 46 | }); 47 | 48 | const UI_CHART_TYPES = [ 49 | { type: false, strokeWidth: 0 }, 50 | { type: 'line', strokeWidth: 3 }, 51 | { type: 'area', strokeWidth: 0 }, 52 | { type: 'bar', strokeWidth: 0 }, 53 | ]; 54 | 55 | const CHART_COLORS = { 56 | rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' }, 57 | tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' }, 58 | gradient: { light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'] }, 59 | }; 60 | 61 | new Vue({ 62 | el: '#app', 63 | components: { 64 | apexchart: VueApexCharts, 65 | }, 66 | i18n, 67 | data: { 68 | authenticated: null, 69 | authenticating: false, 70 | password: null, 71 | requiresPassword: null, 72 | remember: false, 73 | rememberMeEnabled: false, 74 | 75 | clients: null, 76 | clientsPersist: {}, 77 | clientDelete: null, 78 | clientCreate: null, 79 | clientCreateName: '', 80 | clientExpiredDate: '', 81 | clientEditName: null, 82 | clientEditNameId: null, 83 | clientEditAddress: null, 84 | clientEditAddressId: null, 85 | clientEditExpireDate: null, 86 | clientEditExpireDateId: null, 87 | qrcode: null, 88 | 89 | currentRelease: null, 90 | latestRelease: null, 91 | 92 | uiTrafficStats: false, 93 | 94 | uiChartType: 0, 95 | avatarSettings: { 96 | 'dicebear': null, 97 | 'gravatar': false, 98 | }, 99 | enableOneTimeLinks: false, 100 | enableSortClient: false, 101 | sortClient: true, // Sort clients by name, true = asc, false = desc 102 | enableExpireTime: false, 103 | 104 | uiShowCharts: localStorage.getItem('uiShowCharts') === '1', 105 | uiTheme: localStorage.theme || 'auto', 106 | prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'), 107 | 108 | chartOptions: { 109 | chart: { 110 | background: 'transparent', 111 | stacked: false, 112 | toolbar: { 113 | show: false, 114 | }, 115 | animations: { 116 | enabled: false, 117 | }, 118 | parentHeightOffset: 0, 119 | sparkline: { 120 | enabled: true, 121 | }, 122 | }, 123 | colors: [], 124 | stroke: { 125 | curve: 'smooth', 126 | }, 127 | fill: { 128 | type: 'gradient', 129 | gradient: { 130 | shade: 'dark', 131 | type: 'vertical', 132 | shadeIntensity: 0, 133 | gradientToColors: CHART_COLORS.gradient[this.theme], 134 | inverseColors: false, 135 | opacityTo: 0, 136 | stops: [0, 100], 137 | }, 138 | }, 139 | dataLabels: { 140 | enabled: false, 141 | }, 142 | plotOptions: { 143 | bar: { 144 | horizontal: false, 145 | }, 146 | }, 147 | xaxis: { 148 | labels: { 149 | show: false, 150 | }, 151 | axisTicks: { 152 | show: false, 153 | }, 154 | axisBorder: { 155 | show: false, 156 | }, 157 | }, 158 | yaxis: { 159 | labels: { 160 | show: false, 161 | }, 162 | min: 0, 163 | }, 164 | tooltip: { 165 | enabled: false, 166 | }, 167 | legend: { 168 | show: false, 169 | }, 170 | grid: { 171 | show: false, 172 | padding: { 173 | left: -10, 174 | right: 0, 175 | bottom: -15, 176 | top: -15, 177 | }, 178 | column: { 179 | opacity: 0, 180 | }, 181 | xaxis: { 182 | lines: { 183 | show: false, 184 | }, 185 | }, 186 | }, 187 | }, 188 | 189 | }, 190 | methods: { 191 | dateTime: (value) => { 192 | return new Intl.DateTimeFormat(undefined, { 193 | year: 'numeric', 194 | month: 'short', 195 | day: 'numeric', 196 | hour: 'numeric', 197 | minute: 'numeric', 198 | }).format(value); 199 | }, 200 | async refresh({ 201 | updateCharts = false, 202 | } = {}) { 203 | if (!this.authenticated) return; 204 | 205 | const clients = await this.api.getClients(); 206 | this.clients = clients.map((client) => { 207 | if (client.name.includes('@') && client.name.includes('.') && this.avatarSettings.gravatar) { 208 | client.avatar = `https://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`; 209 | } else if (this.avatarSettings.dicebear) { 210 | client.avatar = `https://api.dicebear.com/9.x/${this.avatarSettings.dicebear}/svg?seed=${sha256(client.name.toLowerCase().trim())}` 211 | } 212 | 213 | if (!this.clientsPersist[client.id]) { 214 | this.clientsPersist[client.id] = {}; 215 | this.clientsPersist[client.id].transferRxHistory = Array(50).fill(0); 216 | this.clientsPersist[client.id].transferRxPrevious = client.transferRx; 217 | this.clientsPersist[client.id].transferTxHistory = Array(50).fill(0); 218 | this.clientsPersist[client.id].transferTxPrevious = client.transferTx; 219 | } 220 | 221 | // Debug 222 | // client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000; 223 | // client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000; 224 | // client.latestHandshakeAt = new Date(); 225 | // this.requiresPassword = true; 226 | 227 | this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious; 228 | this.clientsPersist[client.id].transferRxPrevious = client.transferRx; 229 | this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious; 230 | this.clientsPersist[client.id].transferTxPrevious = client.transferTx; 231 | 232 | if (updateCharts) { 233 | this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent); 234 | this.clientsPersist[client.id].transferRxHistory.shift(); 235 | 236 | this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent); 237 | this.clientsPersist[client.id].transferTxHistory.shift(); 238 | 239 | this.clientsPersist[client.id].transferTxSeries = [{ 240 | name: 'Tx', 241 | data: this.clientsPersist[client.id].transferTxHistory, 242 | }]; 243 | 244 | this.clientsPersist[client.id].transferRxSeries = [{ 245 | name: 'Rx', 246 | data: this.clientsPersist[client.id].transferRxHistory, 247 | }]; 248 | 249 | client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory; 250 | client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory; 251 | client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory); 252 | 253 | client.transferTxSeries = this.clientsPersist[client.id].transferTxSeries; 254 | client.transferRxSeries = this.clientsPersist[client.id].transferRxSeries; 255 | } 256 | 257 | client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent; 258 | client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent; 259 | 260 | client.hoverTx = this.clientsPersist[client.id].hoverTx; 261 | client.hoverRx = this.clientsPersist[client.id].hoverRx; 262 | 263 | return client; 264 | }); 265 | 266 | if (this.enableSortClient) { 267 | this.clients = sortByProperty(this.clients, 'name', this.sortClient); 268 | } 269 | }, 270 | login(e) { 271 | e.preventDefault(); 272 | 273 | if (!this.password) return; 274 | if (this.authenticating) return; 275 | 276 | this.authenticating = true; 277 | this.api.createSession({ 278 | password: this.password, 279 | remember: this.remember, 280 | }) 281 | .then(async () => { 282 | const session = await this.api.getSession(); 283 | this.authenticated = session.authenticated; 284 | this.requiresPassword = session.requiresPassword; 285 | return this.refresh(); 286 | }) 287 | .catch((err) => { 288 | alert(err.message || err.toString()); 289 | }) 290 | .finally(() => { 291 | this.authenticating = false; 292 | this.password = null; 293 | }); 294 | }, 295 | logout(e) { 296 | e.preventDefault(); 297 | 298 | this.api.deleteSession() 299 | .then(() => { 300 | this.authenticated = false; 301 | this.clients = null; 302 | }) 303 | .catch((err) => { 304 | alert(err.message || err.toString()); 305 | }); 306 | }, 307 | createClient() { 308 | const name = this.clientCreateName; 309 | const expiredDate = this.clientExpiredDate; 310 | if (!name) return; 311 | 312 | this.api.createClient({ name, expiredDate }) 313 | .catch((err) => alert(err.message || err.toString())) 314 | .finally(() => this.refresh().catch(console.error)); 315 | }, 316 | deleteClient(client) { 317 | this.api.deleteClient({ clientId: client.id }) 318 | .catch((err) => alert(err.message || err.toString())) 319 | .finally(() => this.refresh().catch(console.error)); 320 | }, 321 | showOneTimeLink(client) { 322 | this.api.showOneTimeLink({ clientId: client.id }) 323 | .catch((err) => alert(err.message || err.toString())) 324 | .finally(() => this.refresh().catch(console.error)); 325 | }, 326 | enableClient(client) { 327 | this.api.enableClient({ clientId: client.id }) 328 | .catch((err) => alert(err.message || err.toString())) 329 | .finally(() => this.refresh().catch(console.error)); 330 | }, 331 | disableClient(client) { 332 | this.api.disableClient({ clientId: client.id }) 333 | .catch((err) => alert(err.message || err.toString())) 334 | .finally(() => this.refresh().catch(console.error)); 335 | }, 336 | updateClientName(client, name) { 337 | this.api.updateClientName({ clientId: client.id, name }) 338 | .catch((err) => alert(err.message || err.toString())) 339 | .finally(() => this.refresh().catch(console.error)); 340 | }, 341 | updateClientAddress(client, address) { 342 | this.api.updateClientAddress({ clientId: client.id, address }) 343 | .catch((err) => alert(err.message || err.toString())) 344 | .finally(() => this.refresh().catch(console.error)); 345 | }, 346 | updateClientExpireDate(client, expireDate) { 347 | this.api.updateClientExpireDate({ clientId: client.id, expireDate }) 348 | .catch((err) => alert(err.message || err.toString())) 349 | .finally(() => this.refresh().catch(console.error)); 350 | }, 351 | restoreConfig(e) { 352 | e.preventDefault(); 353 | const file = e.currentTarget.files.item(0); 354 | if (file) { 355 | file.text() 356 | .then((content) => { 357 | this.api.restoreConfiguration(content) 358 | .then((_result) => alert('The configuration was updated.')) 359 | .catch((err) => alert(err.message || err.toString())) 360 | .finally(() => this.refresh().catch(console.error)); 361 | }) 362 | .catch((err) => alert(err.message || err.toString())); 363 | } else { 364 | alert('Failed to load your file!'); 365 | } 366 | }, 367 | toggleTheme() { 368 | const themes = ['light', 'dark', 'auto']; 369 | const currentIndex = themes.indexOf(this.uiTheme); 370 | const newIndex = (currentIndex + 1) % themes.length; 371 | this.uiTheme = themes[newIndex]; 372 | localStorage.theme = this.uiTheme; 373 | this.setTheme(this.uiTheme); 374 | }, 375 | setTheme(theme) { 376 | const { classList } = document.documentElement; 377 | const shouldAddDarkClass = theme === 'dark' || (theme === 'auto' && this.prefersDarkScheme.matches); 378 | classList.toggle('dark', shouldAddDarkClass); 379 | }, 380 | handlePrefersChange(e) { 381 | if (localStorage.theme === 'auto') { 382 | this.setTheme(e.matches ? 'dark' : 'light'); 383 | } 384 | }, 385 | toggleCharts() { 386 | localStorage.setItem('uiShowCharts', this.uiShowCharts ? 1 : 0); 387 | }, 388 | }, 389 | filters: { 390 | bytes, 391 | timeago: (value) => { 392 | return timeago.format(value, i18n.locale); 393 | }, 394 | expiredDateFormat: (value) => { 395 | if (value === null) return i18n.t('Permanent'); 396 | const dateTime = new Date(value); 397 | const options = { year: 'numeric', month: 'long', day: 'numeric' }; 398 | return dateTime.toLocaleDateString(i18n.locale, options); 399 | }, 400 | expiredDateEditFormat: (value) => { 401 | if (value === null) return 'yyyy-MM-dd'; 402 | }, 403 | }, 404 | mounted() { 405 | this.prefersDarkScheme.addListener(this.handlePrefersChange); 406 | this.setTheme(this.uiTheme); 407 | 408 | this.api = new API(); 409 | this.api.getSession() 410 | .then((session) => { 411 | this.authenticated = session.authenticated; 412 | this.requiresPassword = session.requiresPassword; 413 | this.refresh({ 414 | updateCharts: this.updateCharts, 415 | }).catch((err) => { 416 | alert(err.message || err.toString()); 417 | }); 418 | }) 419 | .catch((err) => { 420 | alert(err.message || err.toString()); 421 | }); 422 | 423 | this.api.getRememberMeEnabled() 424 | .then((rememberMeEnabled) => { 425 | this.rememberMeEnabled = rememberMeEnabled; 426 | }); 427 | 428 | setInterval(() => { 429 | this.refresh({ 430 | updateCharts: this.updateCharts, 431 | }).catch(console.error); 432 | }, 1000); 433 | 434 | this.api.getuiTrafficStats() 435 | .then((res) => { 436 | this.uiTrafficStats = res; 437 | }) 438 | .catch(() => { 439 | this.uiTrafficStats = false; 440 | }); 441 | 442 | this.api.getChartType() 443 | .then((res) => { 444 | this.uiChartType = parseInt(res, 10); 445 | }) 446 | .catch(() => { 447 | this.uiChartType = 0; 448 | }); 449 | 450 | this.api.getWGEnableOneTimeLinks() 451 | .then((res) => { 452 | this.enableOneTimeLinks = res; 453 | }) 454 | .catch(() => { 455 | this.enableOneTimeLinks = false; 456 | }); 457 | 458 | this.api.getUiSortClients() 459 | .then((res) => { 460 | this.enableSortClient = res; 461 | }) 462 | .catch(() => { 463 | this.enableSortClient = false; 464 | }); 465 | 466 | this.api.getWGEnableExpireTime() 467 | .then((res) => { 468 | this.enableExpireTime = res; 469 | }) 470 | .catch(() => { 471 | this.enableExpireTime = false; 472 | }); 473 | 474 | this.api.getAvatarSettings() 475 | .then((res) => { 476 | this.avatarSettings = res; 477 | }) 478 | .catch(() => { 479 | this.avatarSettings = { 480 | 'dicebear': null, 481 | 'gravatar': false, 482 | }; 483 | }); 484 | 485 | Promise.resolve().then(async () => { 486 | const lang = await this.api.getLang(); 487 | if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) { 488 | localStorage.setItem('lang', lang); 489 | i18n.locale = lang; 490 | } 491 | 492 | const currentRelease = await this.api.getRelease(); 493 | const latestRelease = await fetch('https://wg-easy.github.io/wg-easy/changelog.json') 494 | .then((res) => res.json()) 495 | .then((releases) => { 496 | const releasesArray = Object.entries(releases).map(([version, changelog]) => ({ 497 | version: parseInt(version, 10), 498 | changelog, 499 | })); 500 | releasesArray.sort((a, b) => { 501 | return b.version - a.version; 502 | }); 503 | 504 | return releasesArray[0]; 505 | }); 506 | 507 | if (currentRelease >= latestRelease.version) return; 508 | 509 | this.currentRelease = currentRelease; 510 | this.latestRelease = latestRelease; 511 | }).catch((err) => console.error(err)); 512 | }, 513 | computed: { 514 | chartOptionsTX() { 515 | const opts = { 516 | ...this.chartOptions, 517 | colors: [CHART_COLORS.tx[this.theme]], 518 | }; 519 | opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false; 520 | opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth; 521 | return opts; 522 | }, 523 | chartOptionsRX() { 524 | const opts = { 525 | ...this.chartOptions, 526 | colors: [CHART_COLORS.rx[this.theme]], 527 | }; 528 | opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false; 529 | opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth; 530 | return opts; 531 | }, 532 | updateCharts() { 533 | return this.uiChartType > 0 && this.uiShowCharts; 534 | }, 535 | theme() { 536 | if (this.uiTheme === 'auto') { 537 | return this.prefersDarkScheme.matches ? 'dark' : 'light'; 538 | } 539 | return this.uiTheme; 540 | }, 541 | }, 542 | }); 543 | -------------------------------------------------------------------------------- /src/www/js/i18n.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const messages = { // eslint-disable-line no-unused-vars 4 | en: { 5 | name: 'Name', 6 | password: 'Password', 7 | signIn: 'Sign In', 8 | logout: 'Logout', 9 | updateAvailable: 'There is an update available!', 10 | update: 'Update', 11 | clients: 'Clients', 12 | new: 'New', 13 | deleteClient: 'Delete Client', 14 | deleteDialog1: 'Are you sure you want to delete', 15 | deleteDialog2: 'This action cannot be undone.', 16 | cancel: 'Cancel', 17 | create: 'Create', 18 | createdOn: 'Created on ', 19 | lastSeen: 'Last seen on ', 20 | totalDownload: 'Total Download: ', 21 | totalUpload: 'Total Upload: ', 22 | newClient: 'New Client', 23 | disableClient: 'Disable Client', 24 | enableClient: 'Enable Client', 25 | noClients: 'There are no clients yet.', 26 | noPrivKey: 'This client has no known private key. Cannot create Configuration.', 27 | showQR: 'Show QR Code', 28 | downloadConfig: 'Download Configuration', 29 | madeBy: 'Made by', 30 | donate: 'Donate', 31 | toggleCharts: 'Show/hide Charts', 32 | theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' }, 33 | restore: 'Restore', 34 | backup: 'Backup', 35 | titleRestoreConfig: 'Restore your configuration', 36 | titleBackupConfig: 'Backup your configuration', 37 | rememberMe: 'Remember me', 38 | titleRememberMe: 'Stay logged after closing the browser', 39 | sort: 'Sort', 40 | ExpireDate: 'Expire Date', 41 | Permanent: 'Permanent', 42 | OneTimeLink: 'Generate short one time link', 43 | }, 44 | ua: { 45 | name: 'Ім`я', 46 | password: 'Пароль', 47 | signIn: 'Увійти', 48 | logout: 'Вихід', 49 | updateAvailable: 'Доступне оновлення!', 50 | update: 'Оновити', 51 | clients: 'Клієнти', 52 | new: 'Новий', 53 | deleteClient: 'Видалити клієнта', 54 | deleteDialog1: 'Ви впевнені, що бажаєте видалити', 55 | deleteDialog2: 'Цю дію неможливо скасувати.', 56 | cancel: 'Скасувати', 57 | create: 'Створити', 58 | createdOn: 'Створено ', 59 | lastSeen: 'Останнє підключення в ', 60 | totalDownload: 'Всього завантажено: ', 61 | totalUpload: 'Всього відправлено: ', 62 | newClient: 'Новий клієнт', 63 | disableClient: 'Вимкнути клієнта', 64 | enableClient: 'Увімкнути клієнта', 65 | noClients: 'Ще немає клієнтів.', 66 | noPrivKey: 'У цього клієнта немає відомого приватного ключа. Неможливо створити конфігурацію.', 67 | showQR: 'Показати QR-код', 68 | downloadConfig: 'Завантажити конфігурацію', 69 | madeBy: 'Зроблено', 70 | donate: 'Пожертвувати', 71 | toggleCharts: 'Показати/сховати діаграми', 72 | theme: { dark: 'Темна тема', light: 'Світла тема', auto: 'Автоматична тема' }, 73 | restore: 'Відновити', 74 | backup: 'Резервна копія', 75 | titleRestoreConfig: 'Відновити конфігурацію', 76 | titleBackupConfig: 'Створити резервну копію конфігурації', 77 | }, 78 | ru: { 79 | name: 'Имя', 80 | password: 'Пароль', 81 | signIn: 'Войти', 82 | logout: 'Выйти', 83 | updateAvailable: 'Доступно обновление!', 84 | update: 'Обновить', 85 | clients: 'Клиенты', 86 | new: 'Создать', 87 | deleteClient: 'Удалить клиента', 88 | deleteDialog1: 'Вы уверены, что хотите удалить', 89 | deleteDialog2: 'Это действие невозможно отменить.', 90 | cancel: 'Закрыть', 91 | create: 'Создать', 92 | createdOn: 'Создано в ', 93 | lastSeen: 'Последнее подключение в ', 94 | totalDownload: 'Всего скачано: ', 95 | totalUpload: 'Всего загружено: ', 96 | newClient: 'Создать клиента', 97 | disableClient: 'Выключить клиента', 98 | enableClient: 'Включить клиента', 99 | noClients: 'Пока нет клиентов.', 100 | noPrivKey: 'Невозможно создать конфигурацию: у клиента нет известного приватного ключа.', 101 | showQR: 'Показать QR-код', 102 | downloadConfig: 'Скачать конфигурацию', 103 | madeBy: 'Автор', 104 | donate: 'Поблагодарить', 105 | toggleCharts: 'Показать/скрыть графики', 106 | theme: { dark: 'Темная тема', light: 'Светлая тема', auto: 'Как в системе' }, 107 | restore: 'Восстановить', 108 | backup: 'Резервная копия', 109 | titleRestoreConfig: 'Восстановить конфигурацию', 110 | titleBackupConfig: 'Создать резервную копию конфигурации', 111 | rememberMe: 'Запомнить меня', 112 | titleRememberMe: 'Оставаться в системе после закрытия браузера', 113 | sort: 'Сортировка', 114 | ExpireDate: 'Дата истечения срока', 115 | Permanent: 'Бессрочно', 116 | OneTimeLink: 'Создать короткую одноразовую ссылку', 117 | }, 118 | tr: { // Müslüm Barış Korkmazer @babico 119 | name: 'İsim', 120 | password: 'Şifre', 121 | signIn: 'Giriş Yap', 122 | logout: 'Çıkış Yap', 123 | updateAvailable: 'Mevcut bir güncelleme var!', 124 | update: 'Güncelle', 125 | clients: 'Kullanıcılar', 126 | new: 'Yeni', 127 | deleteClient: 'Kullanıcı Sil', 128 | deleteDialog1: 'Silmek istediğine emin misin', 129 | deleteDialog2: 'Bu işlem geri alınamaz.', 130 | cancel: 'İptal', 131 | create: 'Oluştur', 132 | createdOn: 'Şu saatte oluşturuldu: ', 133 | lastSeen: 'Son görülme tarihi: ', 134 | totalDownload: 'Toplam İndirme: ', 135 | totalUpload: 'Toplam Yükleme: ', 136 | newClient: 'Yeni Kullanıcı', 137 | disableClient: 'Kullanıcıyı Devre Dışı Bırak', 138 | enableClient: 'Kullanıcıyı Etkinleştir', 139 | noClients: 'Henüz kullanıcı yok.', 140 | noPrivKey: 'Bu istemcinin bilinen bir özel anahtarı yok. Yapılandırma oluşturulamıyor.', 141 | showQR: 'QR Kodunu Göster', 142 | downloadConfig: 'Yapılandırmayı İndir', 143 | madeBy: 'Yapan Kişi: ', 144 | donate: 'Bağış Yap', 145 | toggleCharts: 'Grafiği göster/gizle', 146 | theme: { dark: 'Karanlık tema', light: 'Açık tema', auto: 'Otomatik tema' }, 147 | restore: 'Geri yükle', 148 | backup: 'Yedekle', 149 | titleRestoreConfig: 'Yapılandırmanızı geri yükleyin', 150 | titleBackupConfig: 'Yapılandırmanızı yedekleyin', 151 | }, 152 | no: { // github.com/digvalley 153 | name: 'Navn', 154 | password: 'Passord', 155 | signIn: 'Logg Inn', 156 | logout: 'Logg Ut', 157 | updateAvailable: 'En ny oppdatering er tilgjengelig!', 158 | update: 'Oppdater', 159 | clients: 'Klienter', 160 | new: 'Ny', 161 | deleteClient: 'Slett Klient', 162 | deleteDialog1: 'Er du sikker på at du vil slette?', 163 | deleteDialog2: 'Denne handlingen kan ikke angres', 164 | cancel: 'Avbryt', 165 | create: 'Opprett', 166 | createdOn: 'Opprettet ', 167 | lastSeen: 'Sist sett ', 168 | totalDownload: 'Total Nedlasting: ', 169 | totalUpload: 'Total Opplasting: ', 170 | newClient: 'Ny Klient', 171 | disableClient: 'Deaktiver Klient', 172 | enableClient: 'Aktiver Klient', 173 | noClients: 'Ingen klienter opprettet enda.', 174 | showQR: 'Vis QR Kode', 175 | downloadConfig: 'Last Ned Konfigurasjon', 176 | madeBy: 'Laget av', 177 | donate: 'Doner', 178 | }, 179 | pl: { // github.com/archont94 180 | name: 'Nazwa', 181 | password: 'Hasło', 182 | signIn: 'Zaloguj się', 183 | logout: 'Wyloguj się', 184 | updateAvailable: 'Dostępna aktualizacja!', 185 | update: 'Aktualizuj', 186 | clients: 'Klienci', 187 | new: 'Stwórz klienta', 188 | deleteClient: 'Usuń klienta', 189 | deleteDialog1: 'Jesteś pewny że chcesz usunąć', 190 | deleteDialog2: 'Tej akcji nie da się cofnąć.', 191 | cancel: 'Anuluj', 192 | create: 'Stwórz', 193 | createdOn: 'Utworzono ', 194 | lastSeen: 'Ostatnio widziany ', 195 | totalDownload: 'Całkowite pobieranie: ', 196 | totalUpload: 'Całkowite wysyłanie: ', 197 | newClient: 'Nowy klient', 198 | disableClient: 'Wyłączenie klienta', 199 | enableClient: 'Włączenie klienta', 200 | noClients: 'Nie ma jeszcze klientów.', 201 | showQR: 'Pokaż kod QR', 202 | downloadConfig: 'Pobierz konfigurację', 203 | madeBy: 'Stworzone przez', 204 | donate: 'Wsparcie autora', 205 | }, 206 | fr: { // github.com/clem3109 207 | name: 'Nom', 208 | password: 'Mot de passe', 209 | signIn: 'Se Connecter', 210 | logout: 'Se déconnecter', 211 | updateAvailable: 'Une mise à jour est disponible !', 212 | update: 'Mise à jour', 213 | clients: 'Clients', 214 | new: 'Nouveau', 215 | deleteClient: 'Supprimer ce client', 216 | deleteDialog1: 'Êtes-vous que vous voulez supprimer', 217 | deleteDialog2: 'Cette action ne peut pas être annulée.', 218 | cancel: 'Annuler', 219 | create: 'Créer', 220 | createdOn: 'Créé le ', 221 | lastSeen: 'Dernière connexion le ', 222 | totalDownload: 'Téléchargement total : ', 223 | totalUpload: 'Téléversement total : ', 224 | newClient: 'Nouveau client', 225 | disableClient: 'Désactiver ce client', 226 | enableClient: 'Activer ce client', 227 | noClients: 'Aucun client pour le moment.', 228 | showQR: 'Afficher le code à réponse rapide (QR Code)', 229 | downloadConfig: 'Télécharger la configuration', 230 | madeBy: 'Développé par', 231 | donate: 'Soutenir', 232 | restore: 'Restaurer', 233 | backup: 'Sauvegarder', 234 | titleRestoreConfig: 'Restaurer votre configuration', 235 | titleBackupConfig: 'Sauvegarder votre configuration', 236 | }, 237 | de: { // github.com/florian-asche 238 | name: 'Name', 239 | password: 'Passwort', 240 | signIn: 'Anmelden', 241 | logout: 'Abmelden', 242 | updateAvailable: 'Eine Aktualisierung steht zur Verfügung!', 243 | update: 'Aktualisieren', 244 | clients: 'Clients', 245 | new: 'Neu', 246 | deleteClient: 'Client löschen', 247 | deleteDialog1: 'Möchtest du wirklich löschen?', 248 | deleteDialog2: 'Diese Aktion kann nicht rückgängig gemacht werden.', 249 | cancel: 'Abbrechen', 250 | create: 'Erstellen', 251 | createdOn: 'Erstellt am ', 252 | lastSeen: 'Zuletzt Online ', 253 | totalDownload: 'Gesamt Download: ', 254 | totalUpload: 'Gesamt Upload: ', 255 | newClient: 'Neuer Client', 256 | disableClient: 'Client deaktivieren', 257 | enableClient: 'Client aktivieren', 258 | noClients: 'Es wurden noch keine Clients konfiguriert.', 259 | noPrivKey: 'Es ist kein Private Key für diesen Client bekannt. Eine Konfiguration kann nicht erstellt werden.', 260 | showQR: 'Zeige den QR Code', 261 | downloadConfig: 'Konfiguration herunterladen', 262 | madeBy: 'Erstellt von', 263 | donate: 'Spenden', 264 | restore: 'Wiederherstellen', 265 | backup: 'Sichern', 266 | titleRestoreConfig: 'Stelle deine Konfiguration wieder her', 267 | titleBackupConfig: 'Sichere deine Konfiguration', 268 | }, 269 | ca: { // github.com/guillembonet 270 | name: 'Nom', 271 | password: 'Contrasenya', 272 | signIn: 'Iniciar sessió', 273 | logout: 'Tanca sessió', 274 | updateAvailable: 'Hi ha una actualització disponible!', 275 | update: 'Actualitza', 276 | clients: 'Clients', 277 | new: 'Nou', 278 | deleteClient: 'Esborra client', 279 | deleteDialog1: 'Estàs segur que vols esborrar aquest client?', 280 | deleteDialog2: 'Aquesta acció no es pot desfer.', 281 | cancel: 'Cancel·la', 282 | create: 'Crea', 283 | createdOn: 'Creat el ', 284 | lastSeen: 'Última connexió el ', 285 | totalDownload: 'Baixada total: ', 286 | totalUpload: 'Pujada total: ', 287 | newClient: 'Nou client', 288 | disableClient: 'Desactiva client', 289 | enableClient: 'Activa client', 290 | noClients: 'Encara no hi ha cap client.', 291 | showQR: 'Mostra codi QR', 292 | downloadConfig: 'Descarrega configuració', 293 | madeBy: 'Fet per', 294 | donate: 'Donatiu', 295 | }, 296 | es: { // github.com/amarqz 297 | name: 'Nombre', 298 | password: 'Contraseña', 299 | signIn: 'Iniciar sesión', 300 | logout: 'Cerrar sesión', 301 | updateAvailable: '¡Hay una actualización disponible!', 302 | update: 'Actualizar', 303 | clients: 'Clientes', 304 | new: 'Nuevo', 305 | deleteClient: 'Eliminar cliente', 306 | deleteDialog1: '¿Estás seguro de que quieres borrar este cliente?', 307 | deleteDialog2: 'Esta acción no podrá ser revertida.', 308 | cancel: 'Cancelar', 309 | create: 'Crear', 310 | createdOn: 'Creado el ', 311 | lastSeen: 'Última conexión el ', 312 | totalDownload: 'Total descargado: ', 313 | totalUpload: 'Total subido: ', 314 | newClient: 'Nuevo cliente', 315 | disableClient: 'Desactivar cliente', 316 | enableClient: 'Activar cliente', 317 | noClients: 'Aún no hay ningún cliente.', 318 | showQR: 'Mostrar código QR', 319 | downloadConfig: 'Descargar configuración', 320 | madeBy: 'Hecho por', 321 | donate: 'Donar', 322 | toggleCharts: 'Mostrar/Ocultar gráficos', 323 | theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' }, 324 | restore: 'Restaurar', 325 | backup: 'Realizar copia de seguridad', 326 | titleRestoreConfig: 'Restaurar su configuración', 327 | titleBackupConfig: 'Realizar copia de seguridad de su configuración', 328 | }, 329 | ko: { 330 | name: '이름', 331 | password: '암호', 332 | signIn: '로그인', 333 | logout: '로그아웃', 334 | updateAvailable: '업데이트가 있습니다!', 335 | update: '업데이트', 336 | clients: '클라이언트', 337 | new: '추가', 338 | deleteClient: '클라이언트 삭제', 339 | deleteDialog1: '삭제 하시겠습니까?', 340 | deleteDialog2: '이 작업은 취소할 수 없습니다.', 341 | cancel: '취소', 342 | create: '생성', 343 | createdOn: '생성일: ', 344 | lastSeen: '마지막 사용 날짜: ', 345 | totalDownload: '총 다운로드: ', 346 | totalUpload: '총 업로드: ', 347 | newClient: '새로운 클라이언트', 348 | disableClient: '클라이언트 비활성화', 349 | enableClient: '클라이언트 활성화', 350 | noClients: '아직 클라이언트가 없습니다.', 351 | showQR: 'QR 코드 표시', 352 | downloadConfig: '구성 다운로드', 353 | madeBy: '만든 사람', 354 | donate: '기부', 355 | toggleCharts: '차트 표시/숨기기', 356 | theme: { dark: '어두운 테마', light: '밝은 테마', auto: '자동 테마' }, 357 | restore: '복원', 358 | backup: '백업', 359 | titleRestoreConfig: '구성 파일 복원', 360 | titleBackupConfig: '구성 파일 백업', 361 | }, 362 | vi: { // https://github.com/hoangneeee 363 | name: 'Tên', 364 | password: 'Mật khẩu', 365 | signIn: 'Đăng nhập', 366 | logout: 'Đăng xuất', 367 | updateAvailable: 'Có bản cập nhật mới!', 368 | update: 'Cập nhật', 369 | clients: 'Danh sách người dùng', 370 | new: 'Mới', 371 | deleteClient: 'Xóa người dùng', 372 | deleteDialog1: 'Bạn có chắc chắn muốn xóa', 373 | deleteDialog2: 'Thao tác này không thể hoàn tác.', 374 | cancel: 'Huỷ', 375 | create: 'Tạo', 376 | createdOn: 'Được tạo lúc ', 377 | lastSeen: 'Lần xem cuối vào ', 378 | totalDownload: 'Tổng dung lượng tải xuống: ', 379 | totalUpload: 'Tổng dung lượng tải lên: ', 380 | newClient: 'Người dùng mới', 381 | disableClient: 'Vô hiệu hóa người dùng', 382 | enableClient: 'Kích hoạt người dùng', 383 | noClients: 'Hiện chưa có người dùng nào.', 384 | showQR: 'Hiển thị mã QR', 385 | downloadConfig: 'Tải xuống cấu hình', 386 | madeBy: 'Được tạo bởi', 387 | donate: 'Ủng hộ', 388 | toggleCharts: 'Mở/Ẩn Biểu đồ', 389 | theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' }, 390 | restore: 'Khôi phục', 391 | backup: 'Sao lưu', 392 | titleRestoreConfig: 'Khôi phục cấu hình của bạn', 393 | titleBackupConfig: 'Sao lưu cấu hình của bạn', 394 | sort: 'Sắp xếp', 395 | }, 396 | nl: { 397 | name: 'Naam', 398 | password: 'Wachtwoord', 399 | signIn: 'Inloggen', 400 | logout: 'Uitloggen', 401 | updateAvailable: 'Nieuw update beschikbaar!', 402 | update: 'update', 403 | clients: 'clients', 404 | new: 'Nieuw', 405 | deleteClient: 'client verwijderen', 406 | deleteDialog1: 'Weet je zeker dat je wilt verwijderen', 407 | deleteDialog2: 'Deze actie kan niet ongedaan worden gemaakt.', 408 | cancel: 'Annuleren', 409 | create: 'Creëren', 410 | createdOn: 'Gemaakt op ', 411 | lastSeen: 'Laatst gezien op ', 412 | totalDownload: 'Totaal Gedownload: ', 413 | totalUpload: 'Totaal Geupload: ', 414 | newClient: 'Nieuwe client', 415 | disableClient: 'client uitschakelen', 416 | enableClient: 'client inschakelen', 417 | noClients: 'Er zijn nog geen clients.', 418 | showQR: 'QR-code weergeven', 419 | downloadConfig: 'Configuratie downloaden', 420 | madeBy: 'Gemaakt door', 421 | donate: 'Doneren', 422 | }, 423 | is: { 424 | name: 'Nafn', 425 | password: 'Lykilorð', 426 | signIn: 'Skrá inn', 427 | logout: 'Útskráning', 428 | updateAvailable: 'Það er uppfærsla í boði!', 429 | update: 'Uppfæra', 430 | clients: 'Viðskiptavinir', 431 | new: 'Nýtt', 432 | deleteClient: 'Eyða viðskiptavin', 433 | deleteDialog1: 'Ertu viss um að þú viljir eyða', 434 | deleteDialog2: 'Þessi aðgerð getur ekki verið afturkallað.', 435 | cancel: 'Hætta við', 436 | create: 'Búa til', 437 | createdOn: 'Búið til á ', 438 | lastSeen: 'Síðast séð á ', 439 | totalDownload: 'Samtals Niðurhlaða: ', 440 | totalUpload: 'Samtals Upphlaða: ', 441 | newClient: 'Nýr Viðskiptavinur', 442 | disableClient: 'Gera viðskiptavin óvirkan', 443 | enableClient: 'Gera viðskiptavin virkan', 444 | noClients: 'Engir viðskiptavinir ennþá.', 445 | showQR: 'Sýna QR-kóða', 446 | downloadConfig: 'Niðurhal Stillingar', 447 | madeBy: 'Gert af', 448 | donate: 'Gefa', 449 | }, 450 | pt: { 451 | name: 'Nome', 452 | password: 'Palavra Chave', 453 | signIn: 'Entrar', 454 | logout: 'Sair', 455 | updateAvailable: 'Existe uma atualização disponível!', 456 | update: 'Atualizar', 457 | clients: 'Clientes', 458 | new: 'Novo', 459 | deleteClient: 'Apagar Clientes', 460 | deleteDialog1: 'Tem certeza que pretende apagar', 461 | deleteDialog2: 'Esta ação não pode ser revertida.', 462 | cancel: 'Cancelar', 463 | create: 'Criar', 464 | createdOn: 'Criado em ', 465 | lastSeen: 'Último acesso em ', 466 | totalDownload: 'Total Download: ', 467 | totalUpload: 'Total Upload: ', 468 | newClient: 'Novo Cliente', 469 | disableClient: 'Desativar Cliente', 470 | enableClient: 'Ativar Cliente', 471 | noClients: 'Não existem ainda clientes.', 472 | showQR: 'Apresentar o código QR', 473 | downloadConfig: 'Descarregar Configuração', 474 | madeBy: 'Feito por', 475 | donate: 'Doar', 476 | }, 477 | chs: { 478 | name: '名称', 479 | password: '密码', 480 | signIn: '登录', 481 | logout: '退出', 482 | updateAvailable: '有新版本可用!', 483 | update: '更新', 484 | clients: '客户端', 485 | new: '新建', 486 | deleteClient: '删除客户端', 487 | deleteDialog1: '您确定要删除', 488 | deleteDialog2: '此操作无法撤销。', 489 | cancel: '取消', 490 | create: '创建', 491 | createdOn: '创建于 ', 492 | lastSeen: '最后访问于 ', 493 | totalDownload: '总下载: ', 494 | totalUpload: '总上传: ', 495 | newClient: '新建客户端', 496 | disableClient: '禁用客户端', 497 | enableClient: '启用客户端', 498 | noClients: '目前没有客户端。', 499 | noPrivKey: '此客户端没有已知的私钥。无法创建配置。', 500 | showQR: '显示二维码', 501 | downloadConfig: '下载配置', 502 | madeBy: '由', 503 | donate: '捐赠', 504 | toggleCharts: '显示/隐藏图表', 505 | theme: { dark: '暗黑主题', light: '明亮主题', auto: '自动主题' }, 506 | restore: '恢复', 507 | backup: '备份', 508 | titleRestoreConfig: '恢复您的配置', 509 | titleBackupConfig: '备份您的配置', 510 | rememberMe: '记住我', 511 | titleRememberMe: '关闭浏览器后保持登录', 512 | sort: '排序', 513 | ExpireDate: '到期日期', 514 | Permanent: '永久', 515 | OneTimeLink: '生成一次性短链接', 516 | }, 517 | cht: { 518 | name: '名字', 519 | password: '密碼', 520 | signIn: '登入', 521 | logout: '登出', 522 | updateAvailable: '有新版本可以使用!', 523 | update: '更新', 524 | clients: '使用者', 525 | new: '建立', 526 | deleteClient: '刪除使用者', 527 | deleteDialog1: '您確定要刪除', 528 | deleteDialog2: '此作業無法復原。', 529 | cancel: '取消', 530 | create: '建立', 531 | createdOn: '建立於 ', 532 | lastSeen: '最後存取於 ', 533 | totalDownload: '總下載: ', 534 | totalUpload: '總上傳: ', 535 | newClient: '新用戶', 536 | disableClient: '停用使用者', 537 | enableClient: '啟用使用者', 538 | noClients: '目前沒有使用者。', 539 | noPrivKey: '此使用者沒有已知的私鑰。無法創建配置。', 540 | showQR: '顯示 QR Code', 541 | downloadConfig: '下載 Config 檔', 542 | madeBy: '由', 543 | donate: '抖內', 544 | toggleCharts: '顯示/隱藏圖表', 545 | theme: { dark: '暗黑主題', light: '明亮主題', auto: '自動主題' }, 546 | restore: '恢復', 547 | backup: '備份', 548 | titleRestoreConfig: '恢復您的配置', 549 | titleBackupConfig: '備份您的配置', 550 | rememberMe: '記住我', 551 | titleRememberMe: '關閉瀏覽器後保持登錄', 552 | sort: '排序', 553 | ExpireDate: '到期日期', 554 | Permanent: '永久', 555 | OneTimeLink: '生成一次性短鏈接', 556 | }, 557 | it: { 558 | name: 'Nome', 559 | password: 'Password', 560 | signIn: 'Accedi', 561 | logout: 'Esci', 562 | updateAvailable: 'È disponibile un aggiornamento!', 563 | update: 'Aggiorna', 564 | clients: 'Client', 565 | new: 'Nuovo', 566 | deleteClient: 'Elimina Client', 567 | deleteDialog1: 'Sei sicuro di voler eliminare', 568 | deleteDialog2: 'Questa azione non può essere annullata.', 569 | cancel: 'Annulla', 570 | create: 'Crea', 571 | createdOn: 'Creato il ', 572 | lastSeen: 'Visto l\'ultima volta il ', 573 | totalDownload: 'Totale Download: ', 574 | totalUpload: 'Totale Upload: ', 575 | newClient: 'Nuovo Client', 576 | disableClient: 'Disabilita Client', 577 | enableClient: 'Abilita Client', 578 | noClients: 'Non ci sono ancora client.', 579 | showQR: 'Mostra codice QR', 580 | downloadConfig: 'Scarica configurazione', 581 | madeBy: 'Realizzato da', 582 | donate: 'Donazione', 583 | restore: 'Ripristina', 584 | backup: 'Backup', 585 | titleRestoreConfig: 'Ripristina la tua configurazione', 586 | titleBackupConfig: 'Esegui il backup della tua configurazione', 587 | }, 588 | th: { 589 | name: 'ชื่อ', 590 | password: 'รหัสผ่าน', 591 | signIn: 'ลงชื่อเข้าใช้', 592 | logout: 'ออกจากระบบ', 593 | updateAvailable: 'มีอัปเดตพร้อมใช้งาน!', 594 | update: 'อัปเดต', 595 | clients: 'Clients', 596 | new: 'ใหม่', 597 | deleteClient: 'ลบ Client', 598 | deleteDialog1: 'คุณแน่ใจหรือไม่ว่าต้องการลบ', 599 | deleteDialog2: 'การกระทำนี้;ไม่สามารถยกเลิกได้', 600 | cancel: 'ยกเลิก', 601 | create: 'สร้าง', 602 | createdOn: 'สร้างเมื่อ ', 603 | lastSeen: 'เห็นครั้งสุดท้ายเมื่อ ', 604 | totalDownload: 'ดาวน์โหลดทั้งหมด: ', 605 | totalUpload: 'อัพโหลดทั้งหมด: ', 606 | newClient: 'Client ใหม่', 607 | disableClient: 'ปิดการใช้งาน Client', 608 | enableClient: 'เปิดการใช้งาน Client', 609 | noClients: 'ยังไม่มี Clients เลย', 610 | showQR: 'แสดงรหัส QR', 611 | downloadConfig: 'ดาวน์โหลดการตั้งค่า', 612 | madeBy: 'สร้างโดย', 613 | donate: 'บริจาค', 614 | }, 615 | hi: { // github.com/rahilarious 616 | name: 'नाम', 617 | password: 'पासवर्ड', 618 | signIn: 'लॉगिन', 619 | logout: 'लॉगआउट', 620 | updateAvailable: 'अपडेट उपलब्ध है!', 621 | update: 'अपडेट', 622 | clients: 'उपयोगकर्ताये', 623 | new: 'नया', 624 | deleteClient: 'उपयोगकर्ता हटाएँ', 625 | deleteDialog1: 'क्या आपको पक्का हटाना है', 626 | deleteDialog2: 'यह निर्णय पलट नहीं सकता।', 627 | cancel: 'कुछ ना करें', 628 | create: 'बनाएं', 629 | createdOn: 'सर्जन तारीख ', 630 | lastSeen: 'पिछली बार देखे गए थे ', 631 | totalDownload: 'कुल डाउनलोड: ', 632 | totalUpload: 'कुल अपलोड: ', 633 | newClient: 'नया उपयोगकर्ता', 634 | disableClient: 'उपयोगकर्ता स्थगित कीजिये', 635 | enableClient: 'उपयोगकर्ता शुरू कीजिये', 636 | noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।', 637 | noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।', 638 | showQR: 'क्यू आर कोड देखिये', 639 | downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन', 640 | madeBy: 'सर्जक', 641 | donate: 'दान करें', 642 | }, 643 | }; 644 | -------------------------------------------------------------------------------- /src/www/js/vendor/sha256.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * [js-sha256]{@link https://github.com/emn178/js-sha256} 3 | * 4 | * @version 0.11.0 5 | * @author Chen, Yi-Cyuan [emn178@gmail.com] 6 | * @copyright Chen, Yi-Cyuan 2014-2024 7 | * @license MIT 8 | */ 9 | !function(){"use strict";function t(t,i){i?(d[0]=d[16]=d[1]=d[2]=d[3]=d[4]=d[5]=d[6]=d[7]=d[8]=d[9]=d[10]=d[11]=d[12]=d[13]=d[14]=d[15]=0,this.blocks=d):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],t?(this.h0=3238371032,this.h1=914150663,this.h2=812702999,this.h3=4144912697,this.h4=4290775857,this.h5=1750603025,this.h6=1694076839,this.h7=3204075428):(this.h0=1779033703,this.h1=3144134277,this.h2=1013904242,this.h3=2773480762,this.h4=1359893119,this.h5=2600822924,this.h6=528734635,this.h7=1541459225),this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0,this.is224=t}function i(i,r,s){var e,n=typeof i;if("string"===n){var o,a=[],u=i.length,c=0;for(e=0;e>>6,a[c++]=128|63&o):o<55296||o>=57344?(a[c++]=224|o>>>12,a[c++]=128|o>>>6&63,a[c++]=128|63&o):(o=65536+((1023&o)<<10|1023&i.charCodeAt(++e)),a[c++]=240|o>>>18,a[c++]=128|o>>>12&63,a[c++]=128|o>>>6&63,a[c++]=128|63&o);i=a}else{if("object"!==n)throw new Error(h);if(null===i)throw new Error(h);if(f&&i.constructor===ArrayBuffer)i=new Uint8Array(i);else if(!(Array.isArray(i)||f&&ArrayBuffer.isView(i)))throw new Error(h)}i.length>64&&(i=new t(r,!0).update(i).array());var y=[],p=[];for(e=0;e<64;++e){var l=i[e]||0;y[e]=92^l,p[e]=54^l}t.call(this,r,s),this.update(p),this.oKeyPad=y,this.inner=!0,this.sharedMemory=s}var h="input is invalid type",r="object"==typeof window,s=r?window:{};s.JS_SHA256_NO_WINDOW&&(r=!1);var e=!r&&"object"==typeof self,n=!s.JS_SHA256_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node;n?s=global:e&&(s=self);var o=!s.JS_SHA256_NO_COMMON_JS&&"object"==typeof module&&module.exports,a="function"==typeof define&&define.amd,f=!s.JS_SHA256_NO_ARRAY_BUFFER&&"undefined"!=typeof ArrayBuffer,u="0123456789abcdef".split(""),c=[-2147483648,8388608,32768,128],y=[24,16,8,0],p=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],l=["hex","array","digest","arrayBuffer"],d=[];!s.JS_SHA256_NO_NODE_JS&&Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),!f||!s.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW&&ArrayBuffer.isView||(ArrayBuffer.isView=function(t){return"object"==typeof t&&t.buffer&&t.buffer.constructor===ArrayBuffer});var A=function(i,h){return function(r){return new t(h,!0).update(r)[i]()}},w=function(i){var h=A("hex",i);n&&(h=b(h,i)),h.create=function(){return new t(i)},h.update=function(t){return h.create().update(t)};for(var r=0;r>>2]|=t[n]<>>2]|=s<>>2]|=(192|s>>>6)<>>2]|=(128|63&s)<=57344?(a[e>>>2]|=(224|s>>>12)<>>2]|=(128|s>>>6&63)<>>2]|=(128|63&s)<>>2]|=(240|s>>>18)<>>2]|=(128|s>>>12&63)<>>2]|=(128|s>>>6&63)<>>2]|=(128|63&s)<=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,i=this.lastByteIndex;t[16]=this.block,t[i>>>2]|=c[3&i],this.block=t[16],i>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,i,h,r,s,e,n,o,a,f=this.h0,u=this.h1,c=this.h2,y=this.h3,l=this.h4,d=this.h5,A=this.h6,w=this.h7,b=this.blocks;for(t=16;t<64;++t)i=((s=b[t-15])>>>7|s<<25)^(s>>>18|s<<14)^s>>>3,h=((s=b[t-2])>>>17|s<<15)^(s>>>19|s<<13)^s>>>10,b[t]=b[t-16]+i+b[t-7]+h<<0;for(a=u&c,t=0;t<64;t+=4)this.first?(this.is224?(e=300032,w=(s=b[0]-1413257819)-150054599<<0,y=s+24177077<<0):(e=704751109,w=(s=b[0]-210244248)-1521486534<<0,y=s+143694565<<0),this.first=!1):(i=(f>>>2|f<<30)^(f>>>13|f<<19)^(f>>>22|f<<10),r=(e=f&u)^f&c^a,w=y+(s=w+(h=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7))+(l&d^~l&A)+p[t]+b[t])<<0,y=s+(i+r)<<0),i=(y>>>2|y<<30)^(y>>>13|y<<19)^(y>>>22|y<<10),r=(n=y&f)^y&u^e,A=c+(s=A+(h=(w>>>6|w<<26)^(w>>>11|w<<21)^(w>>>25|w<<7))+(w&l^~w&d)+p[t+1]+b[t+1])<<0,i=((c=s+(i+r)<<0)>>>2|c<<30)^(c>>>13|c<<19)^(c>>>22|c<<10),r=(o=c&y)^c&f^n,d=u+(s=d+(h=(A>>>6|A<<26)^(A>>>11|A<<21)^(A>>>25|A<<7))+(A&w^~A&l)+p[t+2]+b[t+2])<<0,i=((u=s+(i+r)<<0)>>>2|u<<30)^(u>>>13|u<<19)^(u>>>22|u<<10),r=(a=u&c)^u&y^o,l=f+(s=l+(h=(d>>>6|d<<26)^(d>>>11|d<<21)^(d>>>25|d<<7))+(d&A^~d&w)+p[t+3]+b[t+3])<<0,f=s+(i+r)<<0,this.chromeBugWorkAround=!0;this.h0=this.h0+f<<0,this.h1=this.h1+u<<0,this.h2=this.h2+c<<0,this.h3=this.h3+y<<0,this.h4=this.h4+l<<0,this.h5=this.h5+d<<0,this.h6=this.h6+A<<0,this.h7=this.h7+w<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,i=this.h1,h=this.h2,r=this.h3,s=this.h4,e=this.h5,n=this.h6,o=this.h7,a=u[t>>>28&15]+u[t>>>24&15]+u[t>>>20&15]+u[t>>>16&15]+u[t>>>12&15]+u[t>>>8&15]+u[t>>>4&15]+u[15&t]+u[i>>>28&15]+u[i>>>24&15]+u[i>>>20&15]+u[i>>>16&15]+u[i>>>12&15]+u[i>>>8&15]+u[i>>>4&15]+u[15&i]+u[h>>>28&15]+u[h>>>24&15]+u[h>>>20&15]+u[h>>>16&15]+u[h>>>12&15]+u[h>>>8&15]+u[h>>>4&15]+u[15&h]+u[r>>>28&15]+u[r>>>24&15]+u[r>>>20&15]+u[r>>>16&15]+u[r>>>12&15]+u[r>>>8&15]+u[r>>>4&15]+u[15&r]+u[s>>>28&15]+u[s>>>24&15]+u[s>>>20&15]+u[s>>>16&15]+u[s>>>12&15]+u[s>>>8&15]+u[s>>>4&15]+u[15&s]+u[e>>>28&15]+u[e>>>24&15]+u[e>>>20&15]+u[e>>>16&15]+u[e>>>12&15]+u[e>>>8&15]+u[e>>>4&15]+u[15&e]+u[n>>>28&15]+u[n>>>24&15]+u[n>>>20&15]+u[n>>>16&15]+u[n>>>12&15]+u[n>>>8&15]+u[n>>>4&15]+u[15&n];return this.is224||(a+=u[o>>>28&15]+u[o>>>24&15]+u[o>>>20&15]+u[o>>>16&15]+u[o>>>12&15]+u[o>>>8&15]+u[o>>>4&15]+u[15&o]),a},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,i=this.h1,h=this.h2,r=this.h3,s=this.h4,e=this.h5,n=this.h6,o=this.h7,a=[t>>>24&255,t>>>16&255,t>>>8&255,255&t,i>>>24&255,i>>>16&255,i>>>8&255,255&i,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r,s>>>24&255,s>>>16&255,s>>>8&255,255&s,e>>>24&255,e>>>16&255,e>>>8&255,255&e,n>>>24&255,n>>>16&255,n>>>8&255,255&n];return this.is224||a.push(o>>>24&255,o>>>16&255,o>>>8&255,255&o),a},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(this.is224?28:32),i=new DataView(t);return i.setUint32(0,this.h0),i.setUint32(4,this.h1),i.setUint32(8,this.h2),i.setUint32(12,this.h3),i.setUint32(16,this.h4),i.setUint32(20,this.h5),i.setUint32(24,this.h6),this.is224||i.setUint32(28,this.h7),t},(i.prototype=new t).finalize=function(){if(t.prototype.finalize.call(this),this.inner){this.inner=!1;var i=this.array();t.call(this,this.is224,this.sharedMemory),this.update(this.oKeyPad),this.update(i),t.prototype.finalize.call(this)}};var B=w();B.sha256=B,B.sha224=w(!0),B.sha256.hmac=v(),B.sha224.hmac=v(!0),o?module.exports=B:(s.sha256=B.sha256,s.sha224=B.sha224,a&&define(function(){return B}))}(); -------------------------------------------------------------------------------- /src/www/js/vendor/timeago.full.min.js: -------------------------------------------------------------------------------- 1 | !function(s,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((s=s||self).timeago={})}(this,function(s){"use strict";var a=["second","minute","hour","day","week","month","year"];function n(s,n){if(0===n)return["just now","right now"];var e=a[Math.floor(n/2)];return 1=m[t]&&t=m[e]&&e0;)e[n]=arguments[n+1];var r=this.$i18n;return r._t.apply(r,[t,r.locale,r._getMessages(),this].concat(e))},t.prototype.$tc=function(t,e){for(var n=[],r=arguments.length-2;r-- >0;)n[r]=arguments[r+2];var a=this.$i18n;return a._tc.apply(a,[t,a.locale,a._getMessages(),this,e].concat(n))},t.prototype.$te=function(t,e){var n=this.$i18n;return n._te(t,n.locale,n._getMessages(),e)},t.prototype.$d=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this.$i18n).d.apply(e,[t].concat(n))},t.prototype.$n=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this.$i18n).n.apply(e,[t].concat(n))}}(k),k.mixin(function(t){function e(){this!==this.$root&&this.$options.__INTLIFY_META__&&this.$el&&this.$el.setAttribute("data-intlify",this.$options.__INTLIFY_META__)}return void 0===t&&(t=!1),t?{mounted:e}:{beforeCreate:function(){var t=this.$options;if(t.i18n=t.i18n||(t.__i18nBridge||t.__i18n?{}:null),t.i18n){if(t.i18n instanceof nt){if(t.__i18nBridge||t.__i18n)try{var e=t.i18n&&t.i18n.messages?t.i18n.messages:{};(t.__i18nBridge||t.__i18n).forEach(function(t){e=g(e,JSON.parse(t))}),Object.keys(e).forEach(function(n){t.i18n.mergeLocaleMessage(n,e[n])})}catch(t){}this._i18n=t.i18n,this._i18nWatcher=this._i18n.watchI18nData()}else if(l(t.i18n)){var n=this.$root&&this.$root.$i18n&&this.$root.$i18n instanceof nt?this.$root.$i18n:null;if(n&&(t.i18n.root=this.$root,t.i18n.formatter=n.formatter,t.i18n.fallbackLocale=n.fallbackLocale,t.i18n.formatFallbackMessages=n.formatFallbackMessages,t.i18n.silentTranslationWarn=n.silentTranslationWarn,t.i18n.silentFallbackWarn=n.silentFallbackWarn,t.i18n.pluralizationRules=n.pluralizationRules,t.i18n.preserveDirectiveContent=n.preserveDirectiveContent),t.__i18nBridge||t.__i18n)try{var r=t.i18n&&t.i18n.messages?t.i18n.messages:{};(t.__i18nBridge||t.__i18n).forEach(function(t){r=g(r,JSON.parse(t))}),t.i18n.messages=r}catch(t){}var a=t.i18n.sharedMessages;a&&l(a)&&(t.i18n.messages=g(t.i18n.messages,a)),this._i18n=new nt(t.i18n),this._i18nWatcher=this._i18n.watchI18nData(),(void 0===t.i18n.sync||t.i18n.sync)&&(this._localeWatcher=this.$i18n.watchLocale()),n&&n.onComponentInstanceCreated(this._i18n)}}else this.$root&&this.$root.$i18n&&this.$root.$i18n instanceof nt?this._i18n=this.$root.$i18n:t.parent&&t.parent.$i18n&&t.parent.$i18n instanceof nt&&(this._i18n=t.parent.$i18n)},beforeMount:function(){var t=this.$options;t.i18n=t.i18n||(t.__i18nBridge||t.__i18n?{}:null),t.i18n?t.i18n instanceof nt?(this._i18n.subscribeDataChanging(this),this._subscribing=!0):l(t.i18n)&&(this._i18n.subscribeDataChanging(this),this._subscribing=!0):this.$root&&this.$root.$i18n&&this.$root.$i18n instanceof nt?(this._i18n.subscribeDataChanging(this),this._subscribing=!0):t.parent&&t.parent.$i18n&&t.parent.$i18n instanceof nt&&(this._i18n.subscribeDataChanging(this),this._subscribing=!0)},mounted:e,beforeDestroy:function(){if(this._i18n){var t=this;this.$nextTick(function(){t._subscribing&&(t._i18n.unsubscribeDataChanging(t),delete t._subscribing),t._i18nWatcher&&(t._i18nWatcher(),t._i18n.destroyVM(),delete t._i18nWatcher),t._localeWatcher&&(t._localeWatcher(),delete t._localeWatcher)})}}}}(e.bridge)),k.directive("t",{bind:$,update:M,unbind:T}),k.component(d.name,d),k.component(w.name,w),k.config.optionMergeStrategies.i18n=function(t,e){return void 0===e?t:e}}var O=function(){this._caches=Object.create(null)};O.prototype.interpolate=function(t,e){if(!e)return[t];var n=this._caches[t];return n||(n=function(t){var e=[],n=0,r="";for(;n0)h--,u=V,f[j]();else{if(h=0,void 0===n)return!1;if(!1===(n=q(n)))return!1;f[N]()}};null!==u;)if("\\"!==(e=t[++c])||!p()){if(a=G(e),(i=(s=U[u])[a]||s.else||B)===B)return;if(u=i[0],(o=f[i[1]])&&(r=void 0===(r=i[2])?e:r,!1===o()))return;if(u===A)return l}}(t))&&(this._cache[t]=e),e||[]},J.prototype.getPathValue=function(t,e){if(!a(t))return null;var n=this.parsePath(e);if(0===n.length)return null;for(var r=n.length,i=t,o=0;o/,X=/(?:@(?:\.[a-zA-Z]+)?:(?:[\w\-_|./]+|\([\w\-_:|./]+\)))/g,K=/^@(?:\.([a-zA-Z]+))?:/,Q=/[()]/g,tt={upper:function(t){return t.toLocaleUpperCase()},lower:function(t){return t.toLocaleLowerCase()},capitalize:function(t){return""+t.charAt(0).toLocaleUpperCase()+t.substr(1)}},et=new O,nt=function(t){var e=this;void 0===t&&(t={}),!k&&"undefined"!=typeof window&&window.Vue&&L(window.Vue);var n=t.locale||"en-US",r=!1!==t.fallbackLocale&&(t.fallbackLocale||"en-US"),a=t.messages||{},i=t.dateTimeFormats||t.datetimeFormats||{},o=t.numberFormats||{};this._vm=null,this._formatter=t.formatter||et,this._modifiers=t.modifiers||{},this._missing=t.missing||null,this._root=t.root||null,this._sync=void 0===t.sync||!!t.sync,this._fallbackRoot=void 0===t.fallbackRoot||!!t.fallbackRoot,this._fallbackRootWithEmptyString=void 0===t.fallbackRootWithEmptyString||!!t.fallbackRootWithEmptyString,this._formatFallbackMessages=void 0!==t.formatFallbackMessages&&!!t.formatFallbackMessages,this._silentTranslationWarn=void 0!==t.silentTranslationWarn&&t.silentTranslationWarn,this._silentFallbackWarn=void 0!==t.silentFallbackWarn&&!!t.silentFallbackWarn,this._dateTimeFormatters={},this._numberFormatters={},this._path=new J,this._dataListeners=new Set,this._componentInstanceCreatedListener=t.componentInstanceCreatedListener||null,this._preserveDirectiveContent=void 0!==t.preserveDirectiveContent&&!!t.preserveDirectiveContent,this.pluralizationRules=t.pluralizationRules||{},this._warnHtmlInMessage=t.warnHtmlInMessage||"off",this._postTranslation=t.postTranslation||null,this._escapeParameterHtml=t.escapeParameterHtml||!1,"__VUE_I18N_BRIDGE__"in t&&(this.__VUE_I18N_BRIDGE__=t.__VUE_I18N_BRIDGE__),this.getChoiceIndex=function(t,n){var r=Object.getPrototypeOf(e);if(r&&r.getChoiceIndex)return r.getChoiceIndex.call(e,t,n);var a,i;return e.locale in e.pluralizationRules?e.pluralizationRules[e.locale].apply(e,[t,n]):(a=t,i=n,a=Math.abs(a),2===i?a?a>1?1:0:1:a?Math.min(a,2):0)},this._exist=function(t,n){return!(!t||!n)&&(!c(e._path.getPathValue(t,n))||!!t[n])},"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||Object.keys(a).forEach(function(t){e._checkLocaleMessage(t,e._warnHtmlInMessage,a[t])}),this._initVM({locale:n,fallbackLocale:r,messages:a,dateTimeFormats:i,numberFormats:o})},rt={vm:{configurable:!0},messages:{configurable:!0},dateTimeFormats:{configurable:!0},numberFormats:{configurable:!0},availableLocales:{configurable:!0},locale:{configurable:!0},fallbackLocale:{configurable:!0},formatFallbackMessages:{configurable:!0},missing:{configurable:!0},formatter:{configurable:!0},silentTranslationWarn:{configurable:!0},silentFallbackWarn:{configurable:!0},preserveDirectiveContent:{configurable:!0},warnHtmlInMessage:{configurable:!0},postTranslation:{configurable:!0},sync:{configurable:!0}};return nt.prototype._checkLocaleMessage=function(t,e,a){var o=function(t,e,a,s){if(l(a))Object.keys(a).forEach(function(n){var r=a[n];l(r)?(s.push(n),s.push("."),o(t,e,r,s),s.pop(),s.pop()):(s.push(n),o(t,e,r,s),s.pop())});else if(r(a))a.forEach(function(n,r){l(n)?(s.push("["+r+"]"),s.push("."),o(t,e,n,s),s.pop(),s.pop()):(s.push("["+r+"]"),o(t,e,n,s),s.pop())});else if(i(a)){if(Y.test(a)){var c="Detected HTML in message '"+a+"' of keypath '"+s.join("")+"' at '"+e+"'. Consider component interpolation with '' to avoid XSS. See https://bit.ly/2ZqJzkp";"warn"===t?n(c):"error"===t&&function(t,e){"undefined"!=typeof console&&(console.error("[vue-i18n] "+t),e&&console.error(e.stack))}(c)}}};o(e,t,a,[])},nt.prototype._initVM=function(t){var e=k.config.silent;k.config.silent=!0,this._vm=new k({data:t,__VUE18N__INSTANCE__:!0}),k.config.silent=e},nt.prototype.destroyVM=function(){this._vm.$destroy()},nt.prototype.subscribeDataChanging=function(t){this._dataListeners.add(t)},nt.prototype.unsubscribeDataChanging=function(t){!function(t,e){if(t.delete(e));}(this._dataListeners,t)},nt.prototype.watchI18nData=function(){var t=this;return this._vm.$watch("$data",function(){for(var e,n,r=(e=t._dataListeners,n=[],e.forEach(function(t){return n.push(t)}),n),a=r.length;a--;)k.nextTick(function(){r[a]&&r[a].$forceUpdate()})},{deep:!0})},nt.prototype.watchLocale=function(t){if(t){if(!this.__VUE_I18N_BRIDGE__)return null;var e=this,n=this._vm;return this.vm.$watch("locale",function(r){n.$set(n,"locale",r),e.__VUE_I18N_BRIDGE__&&t&&(t.locale.value=r),n.$forceUpdate()},{immediate:!0})}if(!this._sync||!this._root)return null;var r=this._vm;return this._root.$i18n.vm.$watch("locale",function(t){r.$set(r,"locale",t),r.$forceUpdate()},{immediate:!0})},nt.prototype.onComponentInstanceCreated=function(t){this._componentInstanceCreatedListener&&this._componentInstanceCreatedListener(t,this)},rt.vm.get=function(){return this._vm},rt.messages.get=function(){return f(this._getMessages())},rt.dateTimeFormats.get=function(){return f(this._getDateTimeFormats())},rt.numberFormats.get=function(){return f(this._getNumberFormats())},rt.availableLocales.get=function(){return Object.keys(this.messages).sort()},rt.locale.get=function(){return this._vm.locale},rt.locale.set=function(t){this._vm.$set(this._vm,"locale",t)},rt.fallbackLocale.get=function(){return this._vm.fallbackLocale},rt.fallbackLocale.set=function(t){this._localeChainCache={},this._vm.$set(this._vm,"fallbackLocale",t)},rt.formatFallbackMessages.get=function(){return this._formatFallbackMessages},rt.formatFallbackMessages.set=function(t){this._formatFallbackMessages=t},rt.missing.get=function(){return this._missing},rt.missing.set=function(t){this._missing=t},rt.formatter.get=function(){return this._formatter},rt.formatter.set=function(t){this._formatter=t},rt.silentTranslationWarn.get=function(){return this._silentTranslationWarn},rt.silentTranslationWarn.set=function(t){this._silentTranslationWarn=t},rt.silentFallbackWarn.get=function(){return this._silentFallbackWarn},rt.silentFallbackWarn.set=function(t){this._silentFallbackWarn=t},rt.preserveDirectiveContent.get=function(){return this._preserveDirectiveContent},rt.preserveDirectiveContent.set=function(t){this._preserveDirectiveContent=t},rt.warnHtmlInMessage.get=function(){return this._warnHtmlInMessage},rt.warnHtmlInMessage.set=function(t){var e=this,n=this._warnHtmlInMessage;if(this._warnHtmlInMessage=t,n!==t&&("warn"===t||"error"===t)){var r=this._getMessages();Object.keys(r).forEach(function(t){e._checkLocaleMessage(t,e._warnHtmlInMessage,r[t])})}},rt.postTranslation.get=function(){return this._postTranslation},rt.postTranslation.set=function(t){this._postTranslation=t},rt.sync.get=function(){return this._sync},rt.sync.set=function(t){this._sync=t},nt.prototype._getMessages=function(){return this._vm.messages},nt.prototype._getDateTimeFormats=function(){return this._vm.dateTimeFormats},nt.prototype._getNumberFormats=function(){return this._vm.numberFormats},nt.prototype._warnDefault=function(t,e,n,r,a,o){if(!c(n))return n;if(this._missing){var s=this._missing.apply(null,[t,e,r,a]);if(i(s))return s}if(this._formatFallbackMessages){var l=h.apply(void 0,a);return this._render(e,o,l.params,e)}return e},nt.prototype._isFallbackRoot=function(t){return(this._fallbackRootWithEmptyString?!t:c(t))&&!c(this._root)&&this._fallbackRoot},nt.prototype._isSilentFallbackWarn=function(t){return this._silentFallbackWarn instanceof RegExp?this._silentFallbackWarn.test(t):this._silentFallbackWarn},nt.prototype._isSilentFallback=function(t,e){return this._isSilentFallbackWarn(e)&&(this._isFallbackRoot()||t!==this.fallbackLocale)},nt.prototype._isSilentTranslationWarn=function(t){return this._silentTranslationWarn instanceof RegExp?this._silentTranslationWarn.test(t):this._silentTranslationWarn},nt.prototype._interpolate=function(t,e,n,a,o,s,h){if(!e)return null;var f,p=this._path.getPathValue(e,n);if(r(p)||l(p))return p;if(c(p)){if(!l(e))return null;if(!i(f=e[n])&&!u(f))return null}else{if(!i(p)&&!u(p))return null;f=p}return i(f)&&(f.indexOf("@:")>=0||f.indexOf("@.")>=0)&&(f=this._link(t,e,f,a,"raw",s,h)),this._render(f,o,s,n)},nt.prototype._link=function(t,e,n,a,i,o,s){var l=n,c=l.match(X);for(var u in c)if(c.hasOwnProperty(u)){var h=c[u],f=h.match(K),_=f[0],m=f[1],g=h.replace(_,"").replace(Q,"");if(p(s,g))return l;s.push(g);var v=this._interpolate(t,e,g,a,"raw"===i?"string":i,"raw"===i?void 0:o,s);if(this._isFallbackRoot(v)){if(!this._root)throw Error("unexpected error");var d=this._root.$i18n;v=d._translate(d._getMessages(),d.locale,d.fallbackLocale,g,a,i,o)}v=this._warnDefault(t,g,v,a,r(o)?o:[o],i),this._modifiers.hasOwnProperty(m)?v=this._modifiers[m](v):tt.hasOwnProperty(m)&&(v=tt[m](v)),s.pop(),l=v?l.replace(h,v):l}return l},nt.prototype._createMessageContext=function(t,e,n,i){var o=this,s=r(t)?t:[],l=a(t)?t:{},c=this._getMessages(),u=this.locale;return{list:function(t){return s[t]},named:function(t){return l[t]},values:t,formatter:e,path:n,messages:c,locale:u,linked:function(t){return o._interpolate(u,c[u]||{},t,null,i,void 0,[t])}}},nt.prototype._render=function(t,e,n,r){if(u(t))return t(this._createMessageContext(n,this._formatter||et,r,e));var a=this._formatter.interpolate(t,n,r);return a||(a=et.interpolate(t,n,r)),"string"!==e||i(a)?a:a.join("")},nt.prototype._appendItemToChain=function(t,e,n){var r=!1;return p(t,e)||(r=!0,e&&(r="!"!==e[e.length-1],e=e.replace(/!/g,""),t.push(e),n&&n[e]&&(r=n[e]))),r},nt.prototype._appendLocaleToChain=function(t,e,n){var r,a=e.split("-");do{var i=a.join("-");r=this._appendItemToChain(t,i,n),a.splice(-1,1)}while(a.length&&!0===r);return r},nt.prototype._appendBlockToChain=function(t,e,n){for(var r=!0,a=0;a0;)i[o]=arguments[o+4];if(!t)return"";var s,l=h.apply(void 0,i);this._escapeParameterHtml&&(l.params=(null!=(s=l.params)&&Object.keys(s).forEach(function(t){"string"==typeof s[t]&&(s[t]=s[t].replace(//g,">").replace(/"/g,""").replace(/'/g,"'"))}),s));var c=l.locale||e,u=this._translate(n,c,this.fallbackLocale,t,r,"string",l.params);if(this._isFallbackRoot(u)){if(!this._root)throw Error("unexpected error");return(a=this._root).$t.apply(a,[t].concat(i))}return u=this._warnDefault(c,t,u,r,i,"string"),this._postTranslation&&null!=u&&(u=this._postTranslation(u,t)),u},nt.prototype.t=function(t){for(var e,n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];return(e=this)._t.apply(e,[t,this.locale,this._getMessages(),null].concat(n))},nt.prototype._i=function(t,e,n,r,a){var i=this._translate(n,e,this.fallbackLocale,t,r,"raw",a);if(this._isFallbackRoot(i)){if(!this._root)throw Error("unexpected error");return this._root.$i18n.i(t,e,a)}return this._warnDefault(e,t,i,r,[a],"raw")},nt.prototype.i=function(t,e,n){return t?(i(e)||(e=this.locale),this._i(t,e,this._getMessages(),null,n)):""},nt.prototype._tc=function(t,e,n,r,a){for(var i,o=[],s=arguments.length-5;s-- >0;)o[s]=arguments[s+5];if(!t)return"";void 0===a&&(a=1);var l={count:a,n:a},c=h.apply(void 0,o);return c.params=Object.assign(l,c.params),o=null===c.locale?[c.params]:[c.locale,c.params],this.fetchChoice((i=this)._t.apply(i,[t,e,n,r].concat(o)),a)},nt.prototype.fetchChoice=function(t,e){if(!t||!i(t))return null;var n=t.split("|");return n[e=this.getChoiceIndex(e,n.length)]?n[e].trim():t},nt.prototype.tc=function(t,e){for(var n,r=[],a=arguments.length-2;a-- >0;)r[a]=arguments[a+2];return(n=this)._tc.apply(n,[t,this.locale,this._getMessages(),null,e].concat(r))},nt.prototype._te=function(t,e,n){for(var r=[],a=arguments.length-3;a-- >0;)r[a]=arguments[a+3];var i=h.apply(void 0,r).locale||e;return this._exist(n[i],t)},nt.prototype.te=function(t,e){return this._te(t,this.locale,this._getMessages(),e)},nt.prototype.getLocaleMessage=function(t){return f(this._vm.messages[t]||{})},nt.prototype.setLocaleMessage=function(t,e){"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||this._checkLocaleMessage(t,this._warnHtmlInMessage,e),this._vm.$set(this._vm.messages,t,e)},nt.prototype.mergeLocaleMessage=function(t,e){"warn"!==this._warnHtmlInMessage&&"error"!==this._warnHtmlInMessage||this._checkLocaleMessage(t,this._warnHtmlInMessage,e),this._vm.$set(this._vm.messages,t,g(void 0!==this._vm.messages[t]&&Object.keys(this._vm.messages[t]).length?Object.assign({},this._vm.messages[t]):{},e))},nt.prototype.getDateTimeFormat=function(t){return f(this._vm.dateTimeFormats[t]||{})},nt.prototype.setDateTimeFormat=function(t,e){this._vm.$set(this._vm.dateTimeFormats,t,e),this._clearDateTimeFormat(t,e)},nt.prototype.mergeDateTimeFormat=function(t,e){this._vm.$set(this._vm.dateTimeFormats,t,g(this._vm.dateTimeFormats[t]||{},e)),this._clearDateTimeFormat(t,e)},nt.prototype._clearDateTimeFormat=function(t,e){for(var n in e){var r=t+"__"+n;this._dateTimeFormatters.hasOwnProperty(r)&&delete this._dateTimeFormatters[r]}},nt.prototype._localizeDateTime=function(t,e,n,r,a,i){for(var o=e,s=r[o],l=this._getLocaleChain(e,n),u=0;u0;)n[r]=arguments[r+1];var o=this.locale,s=null,l=null;return 1===n.length?(i(n[0])?s=n[0]:a(n[0])&&(n[0].locale&&(o=n[0].locale),n[0].key&&(s=n[0].key)),l=Object.keys(n[0]).reduce(function(t,r){var a;return p(e,r)?Object.assign({},t,((a={})[r]=n[0][r],a)):t},null)):2===n.length&&(i(n[0])&&(s=n[0]),i(n[1])&&(o=n[1])),this._d(t,o,s,l)},nt.prototype.getNumberFormat=function(t){return f(this._vm.numberFormats[t]||{})},nt.prototype.setNumberFormat=function(t,e){this._vm.$set(this._vm.numberFormats,t,e),this._clearNumberFormat(t,e)},nt.prototype.mergeNumberFormat=function(t,e){this._vm.$set(this._vm.numberFormats,t,g(this._vm.numberFormats[t]||{},e)),this._clearNumberFormat(t,e)},nt.prototype._clearNumberFormat=function(t,e){for(var n in e){var r=t+"__"+n;this._numberFormatters.hasOwnProperty(r)&&delete this._numberFormatters[r]}},nt.prototype._getNumberFormatter=function(t,e,n,r,a,i){for(var o=e,s=r[o],l=this._getLocaleChain(e,n),u=0;u0;)n[r]=arguments[r+1];var o=this.locale,s=null,l=null;return 1===n.length?i(n[0])?s=n[0]:a(n[0])&&(n[0].locale&&(o=n[0].locale),n[0].key&&(s=n[0].key),l=Object.keys(n[0]).reduce(function(e,r){var a;return p(t,r)?Object.assign({},e,((a={})[r]=n[0][r],a)):e},null)):2===n.length&&(i(n[0])&&(s=n[0]),i(n[1])&&(o=n[1])),this._n(e,o,s,l)},nt.prototype._ntp=function(t,e,n,r){if(!nt.availabilities.numberFormat)return[];if(!n)return(r?new Intl.NumberFormat(e,r):new Intl.NumberFormat(e)).formatToParts(t);var a=this._getNumberFormatter(t,e,this.fallbackLocale,this._getNumberFormats(),n,r),i=a&&a.formatToParts(t);if(this._isFallbackRoot(i)){if(!this._root)throw Error("unexpected error");return this._root.$i18n._ntp(t,e,n,r)}return i||[]},Object.defineProperties(nt.prototype,rt),Object.defineProperty(nt,"availabilities",{get:function(){if(!Z){var t="undefined"!=typeof Intl;Z={dateTimeFormat:t&&void 0!==Intl.DateTimeFormat,numberFormat:t&&void 0!==Intl.NumberFormat}}return Z}}),nt.install=L,nt.version="8.28.2",nt},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueI18n=e(); 7 | -------------------------------------------------------------------------------- /src/www/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AmneziaWG", 3 | "display": "standalone", 4 | "background_color": "#fff", 5 | "icons": [ 6 | { 7 | "src": "img/favicon.ico", 8 | "type": "image/x-icon" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/www/src/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .p-0 { 6 | padding: 0; 7 | } 8 | --------------------------------------------------------------------------------