├── .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 |
--------------------------------------------------------------------------------