Merge branch 'master' into email-settings-UI

This commit is contained in:
Arminas 2023-03-15 22:58:07 +02:00 committed by GitHub
commit 571e34eeb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1186 additions and 177 deletions

View file

@ -25,3 +25,6 @@ docker-compose*
db
assets
wireguard-ui
# Examples
examples

View file

@ -62,6 +62,7 @@ jobs:
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz"
pre_command: export CGO_ENABLED=0
binary_name: "wireguard-ui"
build_flags: -v
ldflags: -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}" -X main.gitCommit=${{ github.sha }} -X main.gitRef=${{ github.ref }}

4
.gitignore vendored
View file

@ -21,3 +21,7 @@ rice-box.go
# IDEs
.vscode
.idea
# Examples
examples/docker-compose/config
examples/docker-compose/db

View file

@ -4,6 +4,7 @@ LABEL maintainer="Khanh Ngo <k@ndk.name"
ARG TARGETOS=linux
ARG TARGETARCH=amd64
ARG COMMIT=
ARG BUILD_DEPENDENCIES="npm \
yarn"
@ -54,7 +55,7 @@ RUN cp -r /build/custom/ assets/
# Build
RUN rice embed-go && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -o wg-ui .
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X main.gitCommit=${COMMIT}" -a -o wg-ui .
# Release stage
FROM alpine:3.16

View file

@ -27,23 +27,13 @@ Download the binary file from the release page and run it directly on the host m
### Using docker compose
You can take a look at this example
of [docker-compose.yml](https://github.com/ngoduykhanh/wireguard-ui/blob/master/docker-compose.yaml). Please adjust
volume mount points to work with your setup. Then run it like below:
The [examples/docker-compose](examples/docker-compose) folder contains example docker-compose files.
Choose the example which fits you the most, adjust the configuration for your needs, then run it like below:
```
docker-compose up
```
Note:
- There is a Status page that needs docker to be able to access the network of the host in order to read the
wireguard interface stats. See the `cap_add` and `network_mode` options on the docker-compose.yaml
- Similarly, the `WGUI_MANAGE_START` and `WGUI_MANAGE_RESTART` settings need the same access, in order to restart the
wireguard interface.
- Because the `network_mode` is set to `host`, we don't need to specify the exposed ports. The app will listen on
port `5000` by default.
## Environment Variables
| Variable | Description | Default |
@ -53,13 +43,15 @@ Note:
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to | Resolved to your public ip address |
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings | Resolved to your public ip address |
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
| `WGUI_MTU` | The default MTU used in global settings | `1450` |
| `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` |
| `WGUI_FORWARD_MARK` | The default WireGuard forward mark | `0xca6c` |
| `WGUI_FIREWALL_MARK` | The default WireGuard firewall mark | `0xca6c` (51820) |
| `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` |
| `WGUI_LOG_LEVEL` | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` |
| `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A |
| `EMAIL_FROM_ADDRESS` | The sender email address | N/A |
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
@ -202,7 +194,13 @@ feature work.
Go to the project root directory and run the following command:
```sh
docker build -t wireguard-ui .
docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) -t wireguard-ui .
```
or
```sh
docker compose build --build-arg=COMMIT=$(git rev-parse --short HEAD)
```
### Build binary file

View file

@ -58,6 +58,7 @@ function renderClientList(data) {
</div>
<hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span>
<span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span>
<span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span>
<span class="info-box-text"><i class="fas fa-clock"></i>
${prettyDateTime(obj.Client.created_at)}</span>
@ -78,6 +79,34 @@ function renderClientList(data) {
});
}
function renderUserList(data) {
$.each(data, function(index, obj) {
let clientStatusHtml = '>'
// render user html content
let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="user_${obj.username}">
<div class="info-box">
<div class="info-box-content">
<div class="btn-group">
<button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#modal_edit_user" data-username="${obj.username}">Edit</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
data-target="#modal_remove_user" data-username="${obj.username}">Delete</button>
</div>
<hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.username}</span>
<span class="info-box-text"><i class="fas fa-terminal"></i> ${obj.admin? 'Administrator':'Manager'}</span>
</div>
</div>
</div>`
// add the user html elements to the list
$('#users-list').append(html);
});
}
function prettyDateTime(timeStr) {
const dt = new Date(timeStr);
const offsetMs = dt.getTimezoneOffset() * 60 * 1000;

View file

@ -0,0 +1,30 @@
## Prerequisites
### Kernel Module
Depending on if the Wireguard kernel module is available on your system you have more or less choices which example to use.
You can check if the kernel modules are available via the following command:
```shell
modprobe wireguard
```
If the command exits successfully and doesn't print an error the kernel modules are available.
If it does error, you either have to install them manually (or activate if deactivated) or use an userspace implementation.
For an example of an userspace implementation, see _borigtun_.
### Credentials
Username and password for all examples is `admin` by default.
For security reasons it's highly recommended to change them before the first startup.
## Examples
- **[system](system.yml)**
If you have Wireguard already installed on your system and only want to run the UI in docker this might fit the most.
- **[linuxserver](linuxserver.yml)**
If you have the Wireguard kernel modules installed (included in the mainline kernel since version 5.6) but want it running inside of docker, this might fit the most.
- **[boringtun](boringtun.yml)**
If Wireguard kernel modules are not available, you can switch to an userspace implementation like [boringtun](https://github.com/cloudflare/boringtun).

View file

@ -0,0 +1,43 @@
version: "3"
services:
boringtun:
image: ghcr.io/ntkme/boringtun:edge
command:
- wg0
container_name: boringtun
# use the network of the 'wireguard-ui' service. this enables to show active clients in the status page
network_mode: service:wireguard-ui
cap_add:
- NET_ADMIN
volumes:
- /dev/net/tun:/dev/net/tun
- ./config:/etc/wireguard
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
cap_add:
- NET_ADMIN
environment:
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./db:/app/db
- ./config:/etc/wireguard
ports:
# port for wireguard-ui
- "5000:5000"
# port of the wireguard server. this must be set here as the `boringtun` container joins the network of this container and hasn't its own network over which it could publish the ports
- "51820:51820/udp"

View file

@ -0,0 +1,42 @@
version: "3"
services:
wireguard:
image: linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
volumes:
- ./config:/config
ports:
# port for wireguard-ui. this must be set here as the `wireguard-ui` container joins the network of this container and hasn't its own network over which it could publish the ports
- "5000:5000"
# port of the wireguard server
- "51820:51820/udp"
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
depends_on:
- wireguard
cap_add:
- NET_ADMIN
# use the network of the 'wireguard' service. this enables to show active clients in the status page
network_mode: service:wireguard
environment:
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=true
- WGUI_MANAGE_RESTART=true
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./db:/app/db
- ./config:/etc/wireguard

View file

@ -0,0 +1,27 @@
version: "3"
services:
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
cap_add:
- NET_ADMIN
# required to show active clients. with this set, you don't need to expose the ui port (5000) anymore
network_mode: host
environment:
- SENDGRID_API_KEY
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- SESSION_SECRET
- WGUI_USERNAME=admin
- WGUI_PASSWORD=admin
- WG_CONF_TEMPLATE
- WGUI_MANAGE_START=false
- WGUI_MANAGE_RESTART=false
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./db:/app/db
- /etc/wireguard:/etc/wireguard

3
go.mod
View file

@ -19,7 +19,8 @@ require (
github.com/sendgrid/sendgrid-go v3.10.0+incompatible
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
github.com/xhit/go-simple-mail/v2 v2.10.0
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/mod v0.7.0
//golang.zx2c4.com/wireguard v0.0.20200121 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect

24
go.sum
View file

@ -157,6 +157,7 @@ github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -164,13 +165,18 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -181,11 +187,13 @@ golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -215,19 +223,27 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -52,39 +52,54 @@ func LoginPage() echo.HandlerFunc {
// Login for signing in handler
func Login(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
user := new(model.User)
c.Bind(user)
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
dbuser, err := db.GetUser()
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
username := data["username"].(string)
password := data["password"].(string)
rememberMe := data["rememberMe"].(bool)
dbuser, err := db.GetUserByName(username)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"})
}
userCorrect := subtle.ConstantTimeCompare([]byte(user.Username), []byte(dbuser.Username)) == 1
userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1
var passwordCorrect bool
if dbuser.PasswordHash != "" {
match, err := util.VerifyHash(dbuser.PasswordHash, user.Password)
match, err := util.VerifyHash(dbuser.PasswordHash, password)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"})
}
passwordCorrect = match
} else {
passwordCorrect = subtle.ConstantTimeCompare([]byte(user.Password), []byte(dbuser.Password)) == 1
passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1
}
if userCorrect && passwordCorrect {
// TODO: refresh the token
ageMax := 0
expiration := time.Now().Add(24 * time.Hour)
if rememberMe {
ageMax = 86400
expiration.Add(144 * time.Hour)
}
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: util.BasePath,
MaxAge: 86400,
MaxAge: ageMax,
HttpOnly: true,
}
// set session_token
tokenUID := xid.New().String()
sess.Values["username"] = user.Username
sess.Values["username"] = dbuser.Username
sess.Values["admin"] = dbuser.Admin
sess.Values["session_token"] = tokenUID
sess.Save(c.Request(), c.Response())
@ -92,7 +107,7 @@ func Login(db store.IStore) echo.HandlerFunc {
cookie := new(http.Cookie)
cookie.Name = "session_token"
cookie.Value = tokenUID
cookie.Expires = time.Now().Add(24 * time.Hour)
cookie.Expires = expiration
c.SetCookie(cookie)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"})
@ -102,6 +117,40 @@ func Login(db store.IStore) echo.HandlerFunc {
}
}
// GetUsers handler return a JSON list of all users
func GetUsers(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
usersList, err := db.GetUsers()
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false, fmt.Sprintf("Cannot get user list: %v", err),
})
}
return c.JSON(http.StatusOK, usersList)
}
}
// GetUser handler returns a JSON object of single user
func GetUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
username := c.Param("username")
if !isAdmin(c) && (username != currentUser(c)) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
}
userData, err := db.GetUserByName(username)
if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"})
}
return c.JSON(http.StatusOK, userData)
}
}
// Logout to log a user out
func Logout() echo.HandlerFunc {
return func(c echo.Context) error {
@ -113,21 +162,23 @@ func Logout() echo.HandlerFunc {
// LoadProfile to load user information
func LoadProfile(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
userInfo, err := db.GetUser()
if err != nil {
log.Error("Cannot get user information: ", err)
}
return c.Render(http.StatusOK, "profile.html", map[string]interface{}{
"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c)},
"userInfo": userInfo,
"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)},
})
}
}
// UpdateProfile to update user information
func UpdateProfile(db store.IStore) echo.HandlerFunc {
// UsersSettings handler
func UsersSettings(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{
"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)},
})
}
}
// UpdateUser to update user information
func UpdateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
@ -138,8 +189,18 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
username := data["username"].(string)
password := data["password"].(string)
previousUsername := data["previous_username"].(string)
admin := data["admin"].(bool)
user, err := db.GetUser()
if !isAdmin(c) && (previousUsername != currentUser(c)) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
}
if !isAdmin(c) {
admin = false
}
user, err := db.GetUserByName(previousUsername)
if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()})
}
@ -150,6 +211,13 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
user.Username = username
}
if username != previousUsername {
_, err := db.GetUserByName(username)
if err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"})
}
}
if password != "" {
hash, err := util.HashPassword(password)
if err != nil {
@ -158,12 +226,96 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
user.PasswordHash = hash
}
if previousUsername != currentUser(c) {
user.Admin = admin
}
if err := db.DeleteUser(previousUsername); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
log.Infof("Updated admin user information successfully")
log.Infof("Updated user information successfully")
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated admin user information successfully"})
if previousUsername == currentUser(c) {
setUser(c, user.Username, user.Admin)
}
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"})
}
}
// CreateUser to create new user
func CreateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
var user model.User
username := data["username"].(string)
password := data["password"].(string)
admin := data["admin"].(bool)
if username == "" {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
} else {
user.Username = username
}
{
_, err := db.GetUserByName(username)
if err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"})
}
}
hash, err := util.HashPassword(password)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
user.PasswordHash = hash
user.Admin = admin
if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
log.Infof("Created user successfully")
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Created user successfully"})
}
}
// RemoveUser handler
func RemoveUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
username := data["username"].(string)
if username == currentUser(c) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"})
}
// delete user from database
if err := db.DeleteUser(username); err != nil {
log.Error("Cannot delete user: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete user from database"})
}
log.Infof("Removed user: %s", username)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "User removed"})
}
}
@ -179,7 +331,7 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc {
}
return c.Render(http.StatusOK, "clients.html", map[string]interface{}{
"baseData": model.BaseData{Active: "", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"clientDataList": clientDataList,
})
}
@ -541,7 +693,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc {
}
return c.Render(http.StatusOK, "server.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"serverInterface": server.Interface,
"serverKeyPair": server.KeyPair,
})
@ -609,7 +761,7 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc {
}
return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{
"baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"globalSettings": globalSettings,
})
}
@ -697,7 +849,7 @@ func Status(db store.IStore) echo.HandlerFunc {
wgClient, err := wgctrl.New()
if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"error": err.Error(),
"devices": nil,
})
@ -706,7 +858,7 @@ func Status(db store.IStore) echo.HandlerFunc {
devices, err := wgClient.Devices()
if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"error": err.Error(),
"devices": nil,
})
@ -718,7 +870,7 @@ func Status(db store.IStore) echo.HandlerFunc {
clients, err := db.GetClients(false)
if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"error": err.Error(),
"devices": nil,
})
@ -764,7 +916,7 @@ func Status(db store.IStore) echo.HandlerFunc {
}
return c.Render(http.StatusOK, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"devices": devicesVm,
"error": "",
})
@ -878,6 +1030,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"})
}
users, err := db.GetUsers()
if err != nil {
log.Error("Cannot get users config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"})
}
settings, err := db.GetGlobalSettings()
if err != nil {
log.Error("Cannot get global settings: ", err)
@ -885,23 +1043,43 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
}
// Write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings)
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings)
if err != nil {
log.Error("Cannot apply server config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false, fmt.Sprintf("Cannot apply server config: %v", err),
})
}
err = util.UpdateHashes(db)
if err != nil {
log.Error("Cannot update hashes: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false, fmt.Sprintf("Cannot update hashes: %v", err),
})
}
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Applied server config successfully"})
}
}
// GetHashesChanges handler returns if database hashes have changed
func GetHashesChanges(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
if util.HashesChanged(db) {
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Hashes changed"})
} else {
return c.JSON(http.StatusOK, jsonHTTPResponse{false, "Hashes not changed"})
}
}
}
// AboutPage handler
func AboutPage() echo.HandlerFunc {
return func(c echo.Context) error {
return c.Render(http.StatusOK, "about.html", map[string]interface{}{
"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c), Admin: isAdmin(c)},
})
}
}

View file

@ -37,7 +37,7 @@ func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc {
}
err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c)},
"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"hosts": hosts,
"error": "",
})

View file

@ -14,15 +14,24 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
if !isValidSession(c) {
nextURL := c.Request().URL
if nextURL != nil && c.Request().Method == http.MethodGet {
return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath + "/login?next=%s", c.Request().URL))
return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL))
} else {
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath + "/login")
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login")
}
}
return next(c)
}
}
func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !isAdmin(c) {
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
}
return next(c)
}
}
func isValidSession(c echo.Context) bool {
if util.DisableLogin {
return true
@ -46,10 +55,29 @@ func currentUser(c echo.Context) string {
return username
}
// isAdmin to get user type: admin or manager
func isAdmin(c echo.Context) bool {
if util.DisableLogin {
return true
}
sess, _ := session.Get("session", c)
admin := fmt.Sprintf("%t", sess.Values["admin"])
return admin == "true"
}
func setUser(c echo.Context, username string, admin bool) {
sess, _ := session.Get("session", c)
sess.Values["username"] = username
sess.Values["admin"] = admin
sess.Save(c.Request(), c.Response())
}
// clearSession to remove current session
func clearSession(c echo.Context) {
sess, _ := session.Get("session", c)
sess.Values["username"] = ""
sess.Values["admin"] = false
sess.Values["session_token"] = ""
sess.Save(c.Request(), c.Response())
}

64
main.go
View file

@ -80,21 +80,24 @@ func init() {
util.WgConfTemplate = flagWgConfTemplate
util.BasePath = util.ParseBasePath(flagBasePath)
// print app information
fmt.Println("Wireguard UI")
fmt.Println("App Version\t:", appVersion)
fmt.Println("Git Commit\t:", gitCommit)
fmt.Println("Git Ref\t\t:", gitRef)
fmt.Println("Build Time\t:", buildTime)
fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui")
fmt.Println("Authentication\t:", !util.DisableLogin)
fmt.Println("Bind address\t:", util.BindAddress)
//fmt.Println("Sendgrid key\t:", util.SendgridApiKey)
fmt.Println("Email from\t:", util.EmailFrom)
fmt.Println("Email from name\t:", util.EmailFromName)
//fmt.Println("Session secret\t:", util.SessionSecret)
fmt.Println("Custom wg.conf\t:", util.WgConfTemplate)
fmt.Println("Base path\t:", util.BasePath+"/")
// print only if log level is INFO or lower
if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
// print app information
fmt.Println("Wireguard UI")
fmt.Println("App Version\t:", appVersion)
fmt.Println("Git Commit\t:", gitCommit)
fmt.Println("Git Ref\t\t:", gitRef)
fmt.Println("Build Time\t:", buildTime)
fmt.Println("Git Repo\t:", "https://github.com/ngoduykhanh/wireguard-ui")
fmt.Println("Authentication\t:", !util.DisableLogin)
fmt.Println("Bind address\t:", util.BindAddress)
//fmt.Println("Sendgrid key\t:", util.SendgridApiKey)
fmt.Println("Email from\t:", util.EmailFrom)
fmt.Println("Email from name\t:", util.EmailFromName)
//fmt.Println("Session secret\t:", util.SessionSecret)
fmt.Println("Custom wg.conf\t:", util.WgConfTemplate)
fmt.Println("Base path\t:", util.BasePath+"/")
}
}
func main() {
@ -108,6 +111,7 @@ func main() {
// set app extra data
extraData := make(map[string]string)
extraData["appVersion"] = appVersion
extraData["gitCommit"] = gitCommit
extraData["basePath"] = util.BasePath
// create rice box for embedded template
@ -157,10 +161,16 @@ func main() {
app.POST(util.BasePath+"/login", handler.Login(db))
app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession)
app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession)
app.POST(util.BasePath+"/profile", handler.UpdateProfile(db), handler.ValidSession)
app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession)
app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession)
}
app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession)
app.GET(util.BasePath+"/about", handler.AboutPage())
app.GET(util.BasePath+"/_health", handler.Health())
app.GET(util.BasePath+"/favicon", handler.Favicon())
@ -170,13 +180,14 @@ func main() {
app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession)
app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession)
app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/email-settings", handler.EmailSettings(db), handler.ValidSession)
app.POST(util.BasePath+"/email-settings", handler.EmailSettingsSubmit(db), handler.ValidSession)
app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/email-settings", handler.EmailSettings(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/email-settings", handler.EmailSettingsSubmit(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession)
app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession)
app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
@ -215,8 +226,13 @@ func initServerConfig(db store.IStore, tmplBox *rice.Box) {
log.Fatalf("Cannot get client config: ", err)
}
users, err := db.GetUsers()
if err != nil {
log.Fatalf("Cannot get user config: ", err)
}
// write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings)
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings)
if err != nil {
log.Fatalf("Cannot create server config: ", err)
}

View file

@ -10,4 +10,11 @@ type Interface struct {
type BaseData struct {
Active string
CurrentUser string
Admin bool
}
// ClientServerHashes struct, to save hashes to detect changes
type ClientServerHashes struct {
Client string `json:"client"`
Server string `json:"server"`
}

View file

@ -10,7 +10,7 @@ type GlobalSetting struct {
DNSServers []string `json:"dns_servers"`
MTU int `json:"mtu,string"`
PersistentKeepalive int `json:"persistent_keepalive,string"`
ForwardMark string `json:"forward_mark"`
FirewallMark string `json:"firewall_mark"`
ConfigFilePath string `json:"config_file_path"`
UpdatedAt time.Time `json:"updated_at"`
}

View file

@ -6,4 +6,5 @@ type User struct {
Password string `json:"password"`
// PasswordHash takes precedence over Password.
PasswordHash string `json:"password_hash"`
Admin bool `json:"admin"`
}

View file

@ -87,6 +87,15 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
if err != nil {
log.Fatal(err)
}
tmplUsersSettingsString, err := tmplBox.String("users_settings.html")
if err != nil {
log.Fatal(err)
}
if err != nil {
log.Fatal(err)
}
tmplStatusString, err := tmplBox.String("status.html")
if err != nil {
@ -114,14 +123,33 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString))
templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString))
templates["email_settings.html"] = template.Must(template.New("email_settings").Funcs(funcs).Parse(tmplBaseString + tmplEmailSettingsString))
templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString))
templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString))
templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString))
templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString))
e.Logger.SetLevel(log.DEBUG)
lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
if err != nil {
log.Fatal(err)
}
logConfig := middleware.DefaultLoggerConfig
logConfig.Skipper = func(c echo.Context) bool {
resp := c.Response()
if resp.Status >= 500 && lvl > log.ERROR { // do not log if response is 5XX but log level is higher than ERROR
return true
} else if resp.Status >= 400 && lvl > log.WARN { // do not log if response is 4XX but log level is higher than WARN
return true
} else if lvl > log.DEBUG { // do not log if log level is higher than DEBUG
return true
}
return false
}
e.Logger.SetLevel(lvl)
e.Pre(middleware.RemoveTrailingSlash())
e.Use(middleware.Logger())
e.Use(middleware.LoggerWithConfig(logConfig))
e.HideBanner = true
e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
e.Validator = NewValidator()
e.Renderer = &TemplateRegistry{
templates: templates,

View file

@ -43,6 +43,7 @@ func (o *JsonDB) Init() error {
var serverKeyPairPath string = path.Join(serverPath, "keypair.json")
var globalSettingPath string = path.Join(serverPath, "global_settings.json")
var emailSettingPath string = path.Join(serverPath, "email_settings.json")
var hashesPath string = path.Join(serverPath, "hashes.json")
var userPath string = path.Join(serverPath, "users.json")
// create directories if they do not exist
@ -55,6 +56,9 @@ func (o *JsonDB) Init() error {
if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
}
if _, err := os.Stat(userPath); os.IsNotExist(err) {
os.MkdirAll(userPath, os.ModePerm)
}
// server's interface
if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
@ -98,11 +102,19 @@ func (o *JsonDB) Init() error {
globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS})
globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU)
globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive)
globalSetting.ForwardMark = util.LookupEnvOrString(util.ForwardMarkEnvVar, util.DefaultForwardMark)
globalSetting.FirewallMark = util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark)
globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath)
globalSetting.UpdatedAt = time.Now().UTC()
o.conn.Write("server", "global_settings", globalSetting)
}
// hashes
if _, err := os.Stat(hashesPath); os.IsNotExist(err) {
clientServerHashes := new(model.ClientServerHashes)
clientServerHashes.Client = "none"
clientServerHashes.Server = "none"
o.conn.Write("server", "hashes", clientServerHashes)
}
// email settings
// TODO move ENV_VAR variables and default values to config.go
@ -126,9 +138,11 @@ func (o *JsonDB) Init() error {
}
// user info
if _, err := os.Stat(userPath); os.IsNotExist(err) {
results, err := o.conn.ReadAll("users")
if err != nil || len(results) < 1 {
user := new(model.User)
user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
user.Admin = util.DefaultIsAdmin
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
if user.PasswordHash == "" {
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
@ -138,7 +152,7 @@ func (o *JsonDB) Init() error {
}
user.PasswordHash = hash
}
o.conn.Write("server", "users", user)
o.conn.Write("users", user.Username, user)
}
return nil
@ -150,9 +164,44 @@ func (o *JsonDB) GetUser() (model.User, error) {
return user, o.conn.Read("server", "users", &user)
}
// SaveUser func to user info to the database
// GetUsers func to get all users from the database
func (o *JsonDB) GetUsers() ([]model.User, error) {
var users []model.User
results, err := o.conn.ReadAll("users")
if err != nil {
return users, err
}
for _, i := range results {
user := model.User{}
if err := json.Unmarshal([]byte(i), &user); err != nil {
return users, fmt.Errorf("cannot decode user json structure: %v", err)
}
users = append(users, user)
}
return users, err
}
// GetUserByName func to get single user from the database
func (o *JsonDB) GetUserByName(username string) (model.User, error) {
user := model.User{}
if err := o.conn.Read("users", username, &user); err != nil {
return user, err
}
return user, nil
}
// SaveUser func to save user in the database
func (o *JsonDB) SaveUser(user model.User) error {
return o.conn.Write("server", "users", user)
return o.conn.Write("users", user.Username, user)
}
// DeleteUser func to remove user from the database
func (o *JsonDB) DeleteUser(username string) error {
return o.conn.Delete("users", username)
}
// GetGlobalSettings func to query global settings from the database
@ -236,15 +285,12 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
server, _ := o.GetServer()
globalSettings, _ := o.GetGlobalSettings()
client := client
if !qrCodeSettings.IncludeDNS{
if !qrCodeSettings.IncludeDNS {
globalSettings.DNSServers = []string{}
}
if !qrCodeSettings.IncludeMTU {
globalSettings.MTU = 0
}
if !qrCodeSettings.IncludeFwMark {
globalSettings.ForwardMark = ""
}
png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
if err == nil {
@ -288,3 +334,16 @@ func (o *JsonDB) GetEmailSettings() (model.EmailSetting, error) {
func (o *JsonDB) SaveEmailSettings(emailSettings model.EmailSetting) error {
return o.conn.Write("server", "email_settings", emailSettings)
}
func (o *JsonDB) GetPath() string {
return o.dbPath
}
func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) {
hashes := model.ClientServerHashes{}
return hashes, o.conn.Read("server", "hashes", &hashes)
}
func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error {
return o.conn.Write("server", "hashes", hashes)
}

View file

@ -6,8 +6,10 @@ import (
type IStore interface {
Init() error
GetUser() (model.User, error)
GetUsers() ([]model.User, error)
GetUserByName(username string) (model.User, error)
SaveUser(user model.User) error
DeleteUser(username string) error
GetGlobalSettings() (model.GlobalSetting, error)
GetEmailSettings() (model.EmailSetting, error)
GetServer() (model.Server, error)
@ -24,4 +26,7 @@ type IStore interface {
DeleteWakeOnHostLanHost(macAddress string) error
SaveWakeOnLanHost(host model.WakeOnLanHost) error
DeleteWakeOnHost(host model.WakeOnLanHost) error
GetPath() string
SaveHashes(hashes model.ClientServerHashes) error
GetHashes() (model.ClientServerHashes, error)
}

View file

@ -30,6 +30,12 @@ About
<label for="version" class="control-label">Current version</label>
<input type="text" class="form-control" id="version" value="{{ .appVersion }}" readonly>
</div>
{{ if .gitCommit }}
<div class="form-group">
<label for="version" class="control-label">git commit hash</label>
<input type="text" class="form-control" id="version" value="{{ .gitCommit }}" readonly>
</div>
{{ end }}
<div class="form-group">
<label for="currentReleaseDate" class="control-label">Current version release date</label>
<input type="text" class="form-control" id="currentReleaseDate" readonly>

View file

@ -56,6 +56,13 @@
</button>
</div>
</div>
<select name="status-selector" id="status-selector" class="form-control selectpicker show-tick" style="margin-left: 10px">
<option value="All">All</option>
<option value="Enabled">Enabled</option>
<option value="Disabled">Disabled</option>
<option value="Connected">Connected</option>
<option value="Disconnected">Disconnected</option>
</select>
</form>
<!-- Right navbar links -->
@ -63,7 +70,7 @@
<button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New
Client</button>
<button style="margin-left: 0.5em;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
<button id="apply-config-button" style="margin-left: 0.5em; display: none;" type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply
Config</button>
{{if .baseData.CurrentUser}}
@ -90,7 +97,13 @@
</div>
<div class="info">
{{if .baseData.CurrentUser}}
<a href="{{.basePath}}/profile" class="d-block">{{.baseData.CurrentUser}}</a>
{{if .baseData.Admin}}
<a href="{{.basePath}}/profile" class="d-block">Administrator: {{.baseData.CurrentUser}}</a>
{{else}}
<a href="{{.basePath}}/profile" class="d-block">Manager: {{.baseData.CurrentUser}}</a>
{{end}}
{{else}}
<a href="#" class="d-block">Administrator</a>
{{end}}
@ -109,6 +122,8 @@
</p>
</a>
</li>
{{if .baseData.Admin}}
<li class="nav-item">
<a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
<i class="nav-icon fas fa-server"></i>
@ -117,6 +132,8 @@
</p>
</a>
</li>
<li class="nav-header">SETTINGS</li>
<li class="nav-item">
<a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
@ -134,6 +151,14 @@
</p>
</a>
</li>
<a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}">
<i class="nav-icon fas fa-cog"></i>
<p>
Users Settings
</p>
</a>
</li>
{{end}}
<li class="nav-header">UTILITIES</li>
<li class="nav-item">
<a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}">
@ -347,6 +372,32 @@
$('[data-toggle="tooltip"]').tooltip()
})
$(document).ready(function () {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/test-hash',
dataType: 'json',
contentType: "application/json",
success: function(data) {
if (data.status) {
$("#apply-config-button").show()
}
else
{
$("#apply-config-button").hide()
}
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
// populateClient function for render new client info
// on the client page.
function populateClient(client_id) {

View file

@ -70,17 +70,8 @@ Wireguard Clients
</div>
<div class="modal-body">
<input type="hidden" id="qr_client_id" name="qr_client_id">
<a href="" download="" id="qr_code_a">
<img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
</a>
<div class="form-group">
<div class="icheck-primary d-inline">
<input type="checkbox" id="qr_include_fwmark" onchange="regenerateQRCode()">
<label for="qr_include_fwmark">
Include FwMark
</label>
</div>
</div>
<img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
<!-- do not include FwMark in any client configs: it is INVALID. -->
</div>
</div>
<!-- /.modal-content -->
@ -263,6 +254,7 @@ Wireguard Clients
// hide all clients and display only the ones that meet the search criteria (name, email, IP)
$('#search-input').keyup(function () {
$("#status-selector").val("All");
var query = $(this).val();
$('.col-lg-4').hide();
$(".info-box-text").each(function() {
@ -274,6 +266,70 @@ Wireguard Clients
$(".badge-secondary").filter(':contains("' + query + '")').parent().parent().parent().show();
})
$("#status-selector").on('change', function () {
$('#search-input').val("");
switch ($("#status-selector").val()) {
case "All":
$('.col-lg-4').show();
break;
case "Enabled":
$('.col-lg-4').hide();
$('[id^="paused_"]').each(function () {
if ($(this).css("visibility") === "hidden") {
$(this).parent().parent().show();
}
});
break;
case "Disabled":
$('.col-lg-4').hide();
$('[id^="paused_"]').each(function () {
if ($(this).css("visibility") !== "hidden") {
$(this).parent().parent().show();
}
});
break;
case "Connected":
$('.col-lg-4').hide();
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/status',
success: function (data) {
const returnedHTML = $(data).find(".table-success").get();
var returnedString = "";
returnedHTML.forEach(entry => returnedString += entry.outerHTML);
$(".fa-key").each(function () {
if (returnedString.indexOf($(this).parent().text().trim()) != -1) {
$(this).closest('.col-lg-4').show();
}
})
}
});
break;
case "Disconnected":
$('.col-lg-4').show();
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/status',
success: function (data) {
const returnedHTML = $(data).find(".table-success").get();
var returnedString = "";
returnedHTML.forEach(entry => returnedString += entry.outerHTML);
$(".fa-key").each(function () {
if (returnedString.indexOf($(this).parent().text().trim()) != -1) {
$(this).closest('.col-lg-4').hide();
}
})
}
});
break;
default:
$('.col-lg-4').show();
break;
}
});
// modal_pause_client modal event
$("#modal_pause_client").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget);
@ -425,9 +481,7 @@ Wireguard Clients
cache: false,
method: 'GET',
url: '{{.basePath}}/api/client/' + client_id,
data: {
qrCodeIncludeFwMark: include_fwmark
},
data: JSON.stringify(data),
dataType: 'json',
contentType: "application/json",
success: function (resp) {

View file

@ -56,10 +56,10 @@ Global Settings
value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}">
</div>
<div class="form-group">
<label for="forward_mark">Forward Mark</label>
<input type="text" class="form-control" id="forward_mark"
name="forward_mark" placeholder="Forward Mark"
value="{{ .globalSettings.ForwardMark }}">
<label for="firewall_mark">Firewall Mark</label>
<input type="text" class="form-control" id="firewall_mark"
name="firewall_mark" placeholder="Firewall Mark"
value="{{ .globalSettings.FirewallMark }}">
</div>
<div class="form-group">
<label for="config_file_path">Wireguard Config File Path</label>
@ -100,8 +100,8 @@ Global Settings
until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code>
can ensure that the connection remains open.</dd>
<dd>Leave blank to omit this setting in the Client config.</dd>
<dt>5. Forward Mark</dt>
<dd>Set an <code>fwmark</code> on all packets going out of WireGuard's UDP socket. Default value: <code>0xca6c</code></dd>
<dt>5. Firewall Mark</dt>
<dd>Add a matching <code>fwmark</code> on all packets going out of a WireGuard non-default-route tunnel. Default value: <code>0xca6c</code></dd>
<dt>6. Wireguard Config File Path</dt>
<dd>The path of your Wireguard server config file. Please make sure the parent directory
exists and is writable.</dd>
@ -149,9 +149,9 @@ Global Settings
const dns_servers = $("#dns_servers").val().split(",");
const mtu = $("#mtu").val();
const persistent_keepalive = $("#persistent_keepalive").val();
const forward_mark = $("#forward_mark").val();
const firewall_mark = $("#firewall_mark").val();
const config_file_path = $("#config_file_path").val();
const data = {"endpoint_address": endpoint_address, "dns_servers": dns_servers, "mtu": mtu, "persistent_keepalive": persistent_keepalive, "forward_mark": forward_mark, "config_file_path": config_file_path};
const data = {"endpoint_address": endpoint_address, "dns_servers": dns_servers, "mtu": mtu, "persistent_keepalive": persistent_keepalive, "firewall_mark": firewall_mark, "config_file_path": config_file_path};
$.ajax({
cache: false,
@ -222,7 +222,7 @@ Global Settings
config_file_path: {
required: true
},
forward_mark: {
firewall_mark: {
required: false
}
},

View file

@ -101,7 +101,11 @@
$("#btn_login").click(function () {
const username = $("#username").val();
const password = $("#password").val();
const data = {"username": username, "password": password}
let rememberMe = false;
if ($("#remember").is(':checked')){
rememberMe = true;
}
const data = {"username": username, "password": password, "rememberMe": rememberMe}
$.ajax({
cache: false,

View file

@ -31,7 +31,7 @@ Profile
<div class="form-group">
<label for="username" class="control-label">Username</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ .userInfo.Username }}">
value="">
</div>
<div class="form-group">
<label for="password" class="control-label">Password</label>
@ -55,56 +55,82 @@ Profile
{{ define "bottom_js"}}
<script>
function updateUserInfo() {
const username = $("#username").val();
const password = $("#password").val();
const data = {"username": username, "password": password};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/profile',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated admin user information successfully");
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
{
var previous_username;
var admin;
}
$(document).ready(function () {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}',
dataType: 'json',
contentType: "application/json",
success: function (resp) {
const user = resp;
$("#username").val(user.username);
previous_username = user.username;
admin = user.admin;
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
updateUserInfo();
}
function updateUserInfo() {
const username = $("#username").val();
const password = $("#password").val();
const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
updateUserInfo();
}
});
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
});
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
});
</script>
{{ end }}

View file

@ -0,0 +1,294 @@
{{define "title"}}
Users Settings
{{end}}
{{define "top_css"}}
{{end}}
{{define "username"}}
{{ .username }}
{{end}}
{{define "page_title"}}
Users Settings
{{end}}
{{define "page_content"}}
<section class="content">
<div class="container-fluid">
<div class="row" id="users-list">
</div>
</div>
</section>
<div class="modal fade" id="modal_edit_user">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Edit User</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form name="frm_edit_user" id="frm_edit_user">
<div class="modal-body">
<div class="form-group" style="display:none">
<input type="text" style="display:none" class="form-control" id="_previous_user_name"
name="_previous_user_name">
</div>
<div class="form-group">
<label for="_user_name" class="control-label">Name</label>
<input type="text" class="form-control" id="_user_name" name="_user_name">
</div>
<div class="form-group">
<label for="_user_password" class="control-label">Password</label>
<input type="text" class="form-control" id="_user_password" name="_user_password" value=""
placeholder="Leave empty to keep the password unchanged">
</div>
<div class="form-group">
<div class="icheck-primary d-inline">
<input type="checkbox" id="_admin">
<label for="_admin">
Admin
</label>
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Save</button>
</div>
</form>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade" id="modal_remove_user">
<div class="modal-dialog">
<div class="modal-content bg-danger">
<div class="modal-header">
<h4 class="modal-title">Remove</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-dark" id="remove_user_confirm">Apply</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{{end}}
{{define "bottom_js"}}
<script>
function populateUsersList() {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/getusers',
dataType: 'json',
contentType: "application/json",
success: function (data) {
renderUserList(data);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
</script>
<script>
// load user list
$(document).ready(function () {
populateUsersList();
let newUserHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;">' +
'<button style="" id="btn_new_user" type="button" class="btn btn-outline-primary btn-sm" ' +
'data-toggle="modal" data-target="#modal_edit_user" data-username="">' +
'<i class="nav-icon fas fa-plus"></i> New User</button></div>';
$('h1').parents(".row").append(newUserHtml);
})
// modal_remove_user modal event
$("#modal_remove_user").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget);
const user_name = button.data('username');
const modal = $(this);
modal.find('.modal-body').text("You are about to remove user " + user_name);
modal.find('#remove_user_confirm').val(user_name);
})
// remove_user_confirm button event
$(document).ready(function () {
$("#remove_user_confirm").click(function () {
const user_name = $(this).val();
const data = {"username": user_name};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/remove-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
$("#modal_remove_user").modal('hide');
toastr.success('Removed user successfully');
const divElement = document.getElementById('user_' + user_name);
divElement.style.display = "none";
location.reload()
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
});
// Edit user modal event
$(document).ready(function () {
$("#modal_edit_user").on('show.bs.modal', function (event) {
let modal = $(this);
const button = $(event.relatedTarget);
const user_name = button.data('username');
// update user modal data
if (user_name !== "") {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/user/' + user_name,
dataType: 'json',
contentType: "application/json",
success: function (resp) {
const user = resp;
modal.find(".modal-title").text("Edit user " + user.username);
modal.find("#_user_name").val(user.username);
modal.find("#_previous_user_name").val(user.username);
modal.find("#_user_password").val("");
modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged")
modal.find("#_admin").prop("checked", user.admin);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
} else {
modal.find(".modal-title").text("Add new user");
modal.find("#_user_name").val("");
modal.find("#_previous_user_name").val("");
modal.find("#_user_password").val("");
modal.find("#_user_password").prop("placeholder", "")
modal.find("#_admin").prop("checked", false);
}
});
});
function updateUserInfo() {
const username = $("#_user_name").val();
const previous_username = $("#_previous_user_name").val();
const password = $("#_user_password").val();
let admin = false;
if ($("#_admin").is(':checked')) {
admin = true;
}
const data = {
"username": username,
"password": password,
"previous_username": previous_username,
"admin": admin
};
if (previous_username !== "") {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
} else {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/create-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Created user successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function (form) {
updateUserInfo();
}
});
// Edit user form validation
$("#frm_edit_user").validate({
rules: {
_user_name: {
required: true
},
_user_password: {
required: function () {
return $("#_previous_user_name").val() === "";
}
},
},
messages: {
_user_name: {
required: "Please enter a username"
},
_user_password: {
required: "Please input a password"
},
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
//
});
</script>
{{end}}

View file

@ -24,12 +24,13 @@ var (
const (
DefaultUsername = "admin"
DefaultPassword = "admin"
DefaultIsAdmin = true
DefaultServerAddress = "10.252.1.0/24"
DefaultServerPort = 51820
DefaultDNS = "1.1.1.1"
DefaultMTU = 1450
DefaultPersistentKeepalive = 15
DefaultForwardMark = "0xca6c"
DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
UsernameEnvVar = "WGUI_USERNAME"
PasswordEnvVar = "WGUI_PASSWORD"
@ -39,8 +40,9 @@ const (
DNSEnvVar = "WGUI_DNS"
MTUEnvVar = "WGUI_MTU"
PersistentKeepaliveEnvVar = "WGUI_PERSISTENT_KEEPALIVE"
ForwardMarkEnvVar = "WGUI_FORWARD_MARK"
FirewallMarkEnvVar = "WGUI_FIREWALL_MARK"
ConfigFilePathEnvVar = "WGUI_CONFIG_FILE_PATH"
LogLevel = "WGUI_LOG_LEVEL"
ServerAddressesEnvVar = "WGUI_SERVER_INTERFACE_ADDRESSES"
ServerListenPortEnvVar = "WGUI_SERVER_LISTEN_PORT"
ServerPostUpScriptEnvVar = "WGUI_SERVER_POST_UP_SCRIPT"

View file

@ -4,9 +4,14 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/ngoduykhanh/wireguard-ui/store"
"golang.org/x/mod/sumdb/dirhash"
"io"
"io/ioutil"
"net"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"text/template"
@ -60,18 +65,12 @@ func BuildClientConfig(client model.Client, server model.Server, setting model.G
peerPersistentKeepalive = fmt.Sprintf("PersistentKeepalive = %d\n", setting.PersistentKeepalive)
}
forwardMark := ""
if setting.ForwardMark != "" {
forwardMark = fmt.Sprintf("FwMark = %s\n", setting.ForwardMark)
}
// build the config as string
strConfig := "[Interface]\n" +
clientAddress +
clientPrivateKey +
clientDNS +
clientMTU +
forwardMark +
"\n[Peer]\n" +
peerPublicKey +
peerPresharedKey +
@ -221,10 +220,12 @@ func GetPublicIP() (model.Interface, error) {
ip, err := consensus.ExternalIP()
if err != nil {
publicInterface.IPAddress = "N/A"
} else {
publicInterface.IPAddress = ip.String()
}
publicInterface.IPAddress = ip.String()
return publicInterface, err
// error handling happend above, no need to pass it through
return publicInterface, nil
}
// GetIPFromCIDR get ip from CIDR
@ -381,7 +382,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
}
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error {
func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error {
var tmplWireguardConf string
// if set, read wg.conf template from WgConfTemplate
@ -416,6 +417,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
"serverConfig": serverConfig,
"clientDataList": clientDataList,
"globalSettings": globalSettings,
"usersList": usersList,
}
err = t.Execute(f, config)
@ -462,3 +464,56 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string {
}
return defaultVal
}
func ParseLogLevel(lvl string) (log.Lvl, error) {
switch strings.ToLower(lvl) {
case "debug":
return log.DEBUG, nil
case "info":
return log.INFO, nil
case "warn":
return log.WARN, nil
case "error":
return log.ERROR, nil
case "off":
return log.OFF, nil
default:
return log.DEBUG, fmt.Errorf("not a valid log level: %s", lvl)
}
}
// GetCurrentHash returns current hashes
func GetCurrentHash(db store.IStore) (string, string) {
hashClients, _ := dirhash.HashDir(path.Join(db.GetPath(), "clients"), "prefix", dirhash.Hash1)
files := append([]string(nil), "prefix/global_settings.json", "prefix/interfaces.json", "prefix/keypair.json")
osOpen := func(name string) (io.ReadCloser, error) {
return os.Open(filepath.Join(path.Join(db.GetPath(), "server"), strings.TrimPrefix(name, "prefix")))
}
hashServer, _ := dirhash.Hash1(files, osOpen)
return hashClients, hashServer
}
func HashesChanged(db store.IStore) bool {
old, _ := db.GetHashes()
oldClient := old.Client
oldServer := old.Server
newClient, newServer := GetCurrentHash(db)
if oldClient != newClient {
fmt.Println("Hash for client differs")
return true
}
if oldServer != newServer {
fmt.Println("Hash for server differs")
return true
}
return false
}
func UpdateHashes(db store.IStore) error {
var clientServerHashes model.ClientServerHashes
clientServerHashes.Client, clientServerHashes.Server = GetCurrentHash(db)
return db.SaveHashes(clientServerHashes)
}