Merge branch 'master' into nat-support

This commit is contained in:
Khanh Ngo 2023-03-15 21:41:05 +01:00 committed by GitHub
commit afd8791910
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1417 additions and 186 deletions

View file

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

View file

@ -62,6 +62,7 @@ jobs:
goos: ${{ matrix.goos }} goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }} goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz" goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz"
pre_command: export CGO_ENABLED=0
binary_name: "wireguard-ui" binary_name: "wireguard-ui"
build_flags: -v 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 }} 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 # IDEs
.vscode .vscode
.idea .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 TARGETOS=linux
ARG TARGETARCH=amd64 ARG TARGETARCH=amd64
ARG COMMIT=
ARG BUILD_DEPENDENCIES="npm \ ARG BUILD_DEPENDENCIES="npm \
yarn" yarn"
@ -54,7 +55,7 @@ RUN cp -r /build/custom/ assets/
# Build # Build
RUN rice embed-go && \ 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 # Release stage
FROM alpine:3.16 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 ### Using docker compose
You can take a look at this example The [examples/docker-compose](examples/docker-compose) folder contains example docker-compose files.
of [docker-compose.yml](https://github.com/ngoduykhanh/wireguard-ui/blob/master/docker-compose.yaml). Please adjust Choose the example which fits you the most, adjust the configuration for your needs, then run it like below:
volume mount points to work with your setup. Then run it like below:
``` ```
docker-compose up 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 ## Environment Variables
| Variable | Description | Default | | Variable | Description | Default |
@ -54,11 +44,14 @@ Note:
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. 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_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_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_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_MTU` | The default MTU used in global settings | `1450` |
| `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` | | `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_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 | | `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_ADDRESS` | The sender email address | N/A |
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` | | `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
@ -68,7 +61,7 @@ Note:
| `SMTP_USERNAME` | The SMTP username | N/A | | `SMTP_USERNAME` | The SMTP username | N/A |
| `SMTP_PASSWORD` | The SMTP user password | N/A | | `SMTP_PASSWORD` | The SMTP user password | N/A |
| `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` | | `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` |
| `SMTP_ENCRYPTION` | the encryption method. Possible values: `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` | | `SMTP_ENCRYPTION` | the encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` |
### Defaults for server configuration ### Defaults for server configuration
@ -201,7 +194,13 @@ feature work.
Go to the project root directory and run the following command: Go to the project root directory and run the following command:
```sh ```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 ### Build binary file

BIN
custom/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -58,6 +58,7 @@ function renderClientList(data) {
</div> </div>
<hr> <hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> <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-envelope"></i> ${obj.Client.email}</span>
<span class="info-box-text"><i class="fas fa-clock"></i> <span class="info-box-text"><i class="fas fa-clock"></i>
${prettyDateTime(obj.Client.created_at)}</span> ${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) { function prettyDateTime(timeStr) {
const dt = new Date(timeStr); const dt = new Date(timeStr);
const offsetMs = dt.getTimezoneOffset() * 60 * 1000; const offsetMs = dt.getTimezoneOffset() * 60 * 1000;

View file

@ -33,6 +33,8 @@ func authType(authType string) mail.AuthType {
func encryptionType(encryptionType string) mail.Encryption { func encryptionType(encryptionType string) mail.Encryption {
switch strings.ToUpper(encryptionType) { switch strings.ToUpper(encryptionType) {
case "NONE":
return mail.EncryptionNone
case "SSL": case "SSL":
return mail.EncryptionSSL return mail.EncryptionSSL
case "SSLTLS": case "SSLTLS":

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/sendgrid/sendgrid-go v3.10.0+incompatible
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
github.com/xhit/go-simple-mail/v2 v2.10.0 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 v0.0.20200121 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 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/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 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4= 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= 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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-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-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-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-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-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-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-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-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-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-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-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= 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-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-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-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-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-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-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-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-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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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-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-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-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-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-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-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-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.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.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.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.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-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-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-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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -32,6 +33,15 @@ func Health() echo.HandlerFunc {
} }
} }
func Favicon() echo.HandlerFunc {
return func(c echo.Context) error {
if favicon, ok := os.LookupEnv(util.FaviconFilePathEnvVar); ok {
return c.File(favicon)
}
return c.Redirect(http.StatusFound, util.BasePath+"/static/custom/img/favicon.ico")
}
}
// LoginPage handler // LoginPage handler
func LoginPage() echo.HandlerFunc { func LoginPage() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -42,39 +52,54 @@ func LoginPage() echo.HandlerFunc {
// Login for signing in handler // Login for signing in handler
func Login(db store.IStore) echo.HandlerFunc { func Login(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
user := new(model.User) data := make(map[string]interface{})
c.Bind(user) 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 { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) 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 var passwordCorrect bool
if dbuser.PasswordHash != "" { if dbuser.PasswordHash != "" {
match, err := util.VerifyHash(dbuser.PasswordHash, user.Password) match, err := util.VerifyHash(dbuser.PasswordHash, password)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"})
} }
passwordCorrect = match passwordCorrect = match
} else { } else {
passwordCorrect = subtle.ConstantTimeCompare([]byte(user.Password), []byte(dbuser.Password)) == 1 passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1
} }
if userCorrect && passwordCorrect { if userCorrect && passwordCorrect {
// TODO: refresh the token // 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, _ := session.Get("session", c)
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: util.BasePath, Path: util.BasePath,
MaxAge: 86400, MaxAge: ageMax,
HttpOnly: true, HttpOnly: true,
} }
// set session_token // set session_token
tokenUID := xid.New().String() 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.Values["session_token"] = tokenUID
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
@ -82,7 +107,7 @@ func Login(db store.IStore) echo.HandlerFunc {
cookie := new(http.Cookie) cookie := new(http.Cookie)
cookie.Name = "session_token" cookie.Name = "session_token"
cookie.Value = tokenUID cookie.Value = tokenUID
cookie.Expires = time.Now().Add(24 * time.Hour) cookie.Expires = expiration
c.SetCookie(cookie) c.SetCookie(cookie)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"})
@ -92,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 // Logout to log a user out
func Logout() echo.HandlerFunc { func Logout() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -103,21 +162,23 @@ func Logout() echo.HandlerFunc {
// LoadProfile to load user information // LoadProfile to load user information
func LoadProfile(db store.IStore) echo.HandlerFunc { func LoadProfile(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { 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{}{ return c.Render(http.StatusOK, "profile.html", map[string]interface{}{
"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c)}, "baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"userInfo": userInfo,
}) })
} }
} }
// UpdateProfile to update user information // UsersSettings handler
func UpdateProfile(db store.IStore) echo.HandlerFunc { 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 { return func(c echo.Context) error {
data := make(map[string]interface{}) data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data) err := json.NewDecoder(c.Request().Body).Decode(&data)
@ -128,8 +189,18 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
username := data["username"].(string) username := data["username"].(string)
password := data["password"].(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 { if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()})
} }
@ -140,6 +211,13 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
user.Username = username 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 != "" { if password != "" {
hash, err := util.HashPassword(password) hash, err := util.HashPassword(password)
if err != nil { if err != nil {
@ -148,12 +226,96 @@ func UpdateProfile(db store.IStore) echo.HandlerFunc {
user.PasswordHash = hash 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 { if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) 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"})
} }
} }
@ -169,7 +331,7 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ 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, "clientDataList": clientDataList,
}) })
} }
@ -522,7 +684,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "server.html", map[string]interface{}{ 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, "serverInterface": server.Interface,
"serverKeyPair": server.KeyPair, "serverKeyPair": server.KeyPair,
}) })
@ -590,7 +752,7 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ 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, "globalSettings": globalSettings,
}) })
} }
@ -607,6 +769,8 @@ func Status(db store.IStore) echo.HandlerFunc {
LastHandshakeTime time.Time LastHandshakeTime time.Time
LastHandshakeRel time.Duration LastHandshakeRel time.Duration
Connected bool Connected bool
AllocatedIP string
Endpoint string
} }
type DeviceVM struct { type DeviceVM struct {
@ -618,7 +782,7 @@ func Status(db store.IStore) echo.HandlerFunc {
wgClient, err := wgctrl.New() wgClient, err := wgctrl.New()
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ 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(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -627,7 +791,7 @@ func Status(db store.IStore) echo.HandlerFunc {
devices, err := wgClient.Devices() devices, err := wgClient.Devices()
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ 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(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -639,7 +803,7 @@ func Status(db store.IStore) echo.HandlerFunc {
clients, err := db.GetClients(false) clients, err := db.GetClients(false)
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ 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(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -654,12 +818,21 @@ func Status(db store.IStore) echo.HandlerFunc {
for i := range devices { for i := range devices {
devVm := DeviceVM{Name: devices[i].Name} devVm := DeviceVM{Name: devices[i].Name}
for j := range devices[i].Peers { for j := range devices[i].Peers {
var allocatedIPs string
for _, ip := range devices[i].Peers[j].AllowedIPs {
if len(allocatedIPs) > 0 {
allocatedIPs += "</br>"
}
allocatedIPs += ip.String()
}
pVm := PeerVM{ pVm := PeerVM{
PublicKey: devices[i].Peers[j].PublicKey.String(), PublicKey: devices[i].Peers[j].PublicKey.String(),
ReceivedBytes: devices[i].Peers[j].ReceiveBytes, ReceivedBytes: devices[i].Peers[j].ReceiveBytes,
TransmitBytes: devices[i].Peers[j].TransmitBytes, TransmitBytes: devices[i].Peers[j].TransmitBytes,
LastHandshakeTime: devices[i].Peers[j].LastHandshakeTime, LastHandshakeTime: devices[i].Peers[j].LastHandshakeTime,
LastHandshakeRel: time.Since(devices[i].Peers[j].LastHandshakeTime), LastHandshakeRel: time.Since(devices[i].Peers[j].LastHandshakeTime),
AllocatedIP: allocatedIPs,
Endpoint: devices[i].Peers[j].Endpoint.String(),
} }
pVm.Connected = pVm.LastHandshakeRel.Minutes() < 3. pVm.Connected = pVm.LastHandshakeRel.Minutes() < 3.
@ -676,7 +849,7 @@ func Status(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "status.html", map[string]interface{}{ 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, "devices": devicesVm,
"error": "", "error": "",
}) })
@ -790,6 +963,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) 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() settings, err := db.GetGlobalSettings()
if err != nil { if err != nil {
log.Error("Cannot get global settings: ", err) log.Error("Cannot get global settings: ", err)
@ -797,7 +976,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
} }
// Write config file // Write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings)
if err != nil { if err != nil {
log.Error("Cannot apply server config: ", err) log.Error("Cannot apply server config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
@ -805,6 +984,36 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
}) })
} }
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"}) 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)},
})
}
}

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{}{ 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, "hosts": hosts,
"error": "", "error": "",
}) })

View file

@ -23,6 +23,15 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
} }
} }
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 { func isValidSession(c echo.Context) bool {
if util.DisableLogin { if util.DisableLogin {
return true return true
@ -46,10 +55,29 @@ func currentUser(c echo.Context) string {
return username 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 // clearSession to remove current session
func clearSession(c echo.Context) { func clearSession(c echo.Context) {
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Values["username"] = "" sess.Values["username"] = ""
sess.Values["admin"] = false
sess.Values["session_token"] = "" sess.Values["session_token"] = ""
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
} }

34
main.go
View file

@ -61,7 +61,7 @@ func init() {
flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username") flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username")
flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword), "SMTP Password") flag.StringVar(&flagSmtpPassword, "smtp-password", util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword), "SMTP Password")
flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.") flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.")
flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : SSL, SSLTLS, TLS or STARTTLS (by default)") flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)")
flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.") flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.")
flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey), "Your sendgrid api key.") flag.StringVar(&flagSendgridApiKey, "sendgrid-api-key", util.LookupEnvOrString("SENDGRID_API_KEY", flagSendgridApiKey), "Your sendgrid api key.")
flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.") flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
@ -88,6 +88,8 @@ func init() {
util.WgConfTemplate = flagWgConfTemplate util.WgConfTemplate = flagWgConfTemplate
util.BasePath = util.ParseBasePath(flagBasePath) util.BasePath = util.ParseBasePath(flagBasePath)
// print only if log level is INFO or lower
if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
// print app information // print app information
fmt.Println("Wireguard UI") fmt.Println("Wireguard UI")
fmt.Println("App Version\t:", appVersion) fmt.Println("App Version\t:", appVersion)
@ -104,6 +106,7 @@ func init() {
fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) fmt.Println("Custom wg.conf\t:", util.WgConfTemplate)
fmt.Println("Base path\t:", util.BasePath+"/") fmt.Println("Base path\t:", util.BasePath+"/")
} }
}
func main() { func main() {
db, err := jsondb.New("./db") db, err := jsondb.New("./db")
@ -116,6 +119,7 @@ func main() {
// set app extra data // set app extra data
extraData := make(map[string]string) extraData := make(map[string]string)
extraData["appVersion"] = appVersion extraData["appVersion"] = appVersion
extraData["gitCommit"] = gitCommit
extraData["basePath"] = util.BasePath extraData["basePath"] = util.BasePath
// create rice box for embedded template // create rice box for embedded template
@ -137,7 +141,12 @@ func main() {
app.POST(util.BasePath+"/login", handler.Login(db)) app.POST(util.BasePath+"/login", handler.Login(db))
app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession)
app.GET(util.BasePath+"/profile", handler.LoadProfile(db), 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)
} }
var sendmail emailer.Emailer var sendmail emailer.Emailer
@ -147,18 +156,22 @@ func main() {
sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
} }
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+"/_health", handler.Health())
app.GET(util.BasePath+"/favicon", handler.Favicon())
app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) 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.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+"/download", handler.DownloadClient(db), handler.ValidSession)
app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(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) 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) 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) 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)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession) 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/clients", handler.GetClients(db), handler.ValidSession)
app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
@ -197,8 +210,13 @@ func initServerConfig(db store.IStore, tmplBox *rice.Box) {
log.Fatalf("Cannot get client config: ", err) log.Fatalf("Cannot get client config: ", err)
} }
users, err := db.GetUsers()
if err != nil {
log.Fatalf("Cannot get user config: ", err)
}
// write config file // write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings)
if err != nil { if err != nil {
log.Fatalf("Cannot create server config: ", err) log.Fatalf("Cannot create server config: ", err)
} }

View file

@ -10,4 +10,11 @@ type Interface struct {
type BaseData struct { type BaseData struct {
Active string Active string
CurrentUser 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"` DNSServers []string `json:"dns_servers"`
MTU int `json:"mtu,string"` MTU int `json:"mtu,string"`
PersistentKeepalive int `json:"persistent_keepalive,string"` PersistentKeepalive int `json:"persistent_keepalive,string"`
ForwardMark string `json:"forward_mark"` FirewallMark string `json:"firewall_mark"`
ConfigFilePath string `json:"config_file_path"` ConfigFilePath string `json:"config_file_path"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }

View file

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

View file

@ -83,6 +83,11 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
log.Fatal(err) log.Fatal(err)
} }
tmplUsersSettingsString, err := tmplBox.String("users_settings.html")
if err != nil {
log.Fatal(err)
}
tmplStatusString, err := tmplBox.String("status.html") tmplStatusString, err := tmplBox.String("status.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -93,6 +98,11 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
log.Fatal(err) log.Fatal(err)
} }
aboutPageString, err := tmplBox.String("about.html")
if err != nil {
log.Fatal(err)
}
// create template list // create template list
funcs := template.FuncMap{ funcs := template.FuncMap{
"StringsJoin": strings.Join, "StringsJoin": strings.Join,
@ -103,13 +113,33 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString))
templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) 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["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString))
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["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["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.Pre(middleware.RemoveTrailingSlash())
e.Use(middleware.Logger()) e.Use(middleware.LoggerWithConfig(logConfig))
e.HideBanner = true e.HideBanner = true
e.HidePort = lvl > log.INFO // hide the port output if the log level is higher than INFO
e.Validator = NewValidator() e.Validator = NewValidator()
e.Renderer = &TemplateRegistry{ e.Renderer = &TemplateRegistry{
templates: templates, templates: templates,

View file

@ -42,7 +42,9 @@ func (o *JsonDB) Init() error {
var serverInterfacePath string = path.Join(serverPath, "interfaces.json") var serverInterfacePath string = path.Join(serverPath, "interfaces.json")
var serverKeyPairPath string = path.Join(serverPath, "keypair.json") var serverKeyPairPath string = path.Join(serverPath, "keypair.json")
var globalSettingPath string = path.Join(serverPath, "global_settings.json") var globalSettingPath string = path.Join(serverPath, "global_settings.json")
var hashesPath string = path.Join(serverPath, "hashes.json")
var userPath string = path.Join(serverPath, "users.json") var userPath string = path.Join(serverPath, "users.json")
// create directories if they do not exist // create directories if they do not exist
if _, err := os.Stat(clientPath); os.IsNotExist(err) { if _, err := os.Stat(clientPath); os.IsNotExist(err) {
os.MkdirAll(clientPath, os.ModePerm) os.MkdirAll(clientPath, os.ModePerm)
@ -53,6 +55,9 @@ func (o *JsonDB) Init() error {
if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
} }
if _, err := os.Stat(userPath); os.IsNotExist(err) {
os.MkdirAll(userPath, os.ModePerm)
}
// server's interface // server's interface
if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
@ -96,16 +101,26 @@ func (o *JsonDB) Init() error {
globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS}) globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS})
globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU)
globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive) 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.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath)
globalSetting.UpdatedAt = time.Now().UTC() globalSetting.UpdatedAt = time.Now().UTC()
o.conn.Write("server", "global_settings", globalSetting) 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)
}
// user info // 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 := new(model.User)
user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
user.Admin = util.DefaultIsAdmin
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
if user.PasswordHash == "" { if user.PasswordHash == "" {
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
@ -115,7 +130,7 @@ func (o *JsonDB) Init() error {
} }
user.PasswordHash = hash user.PasswordHash = hash
} }
o.conn.Write("server", "users", user) o.conn.Write("users", user.Username, user)
} }
return nil return nil
@ -127,9 +142,44 @@ func (o *JsonDB) GetUser() (model.User, error) {
return user, o.conn.Read("server", "users", &user) 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 { 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 // GetGlobalSettings func to query global settings from the database
@ -219,9 +269,6 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
if !qrCodeSettings.IncludeMTU { if !qrCodeSettings.IncludeMTU {
globalSettings.MTU = 0 globalSettings.MTU = 0
} }
if !qrCodeSettings.IncludeFwMark {
globalSettings.ForwardMark = ""
}
png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
if err == nil { if err == nil {
@ -255,3 +302,16 @@ func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
return o.conn.Write("server", "global_settings", globalSettings) return o.conn.Write("server", "global_settings", globalSettings)
} }
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 { type IStore interface {
Init() error Init() error
GetUser() (model.User, error) GetUsers() ([]model.User, error)
GetUserByName(username string) (model.User, error)
SaveUser(user model.User) error SaveUser(user model.User) error
DeleteUser(username string) error
GetGlobalSettings() (model.GlobalSetting, error) GetGlobalSettings() (model.GlobalSetting, error)
GetServer() (model.Server, error) GetServer() (model.Server, error)
GetClients(hasQRCode bool) ([]model.ClientData, error) GetClients(hasQRCode bool) ([]model.ClientData, error)
@ -22,4 +24,7 @@ type IStore interface {
DeleteWakeOnHostLanHost(macAddress string) error DeleteWakeOnHostLanHost(macAddress string) error
SaveWakeOnLanHost(host model.WakeOnLanHost) error SaveWakeOnLanHost(host model.WakeOnLanHost) error
DeleteWakeOnHost(host model.WakeOnLanHost) error DeleteWakeOnHost(host model.WakeOnLanHost) error
GetPath() string
SaveHashes(hashes model.ClientServerHashes) error
GetHashes() (model.ClientServerHashes, error)
} }

145
templates/about.html Normal file
View file

@ -0,0 +1,145 @@
{{ define "title"}}
About
{{ end }}
{{ define "top_css"}}
{{ end }}
{{ define "username"}}
{{ .username }}
{{ end }}
{{ define "page_title"}}
About
{{ end }}
{{ define "page_content"}}
<section class="content">
<div class="container-fluid">
<!-- <h5 class="mt-4 mb-2">Global Settings</h5> -->
<div class="row">
<!-- left column -->
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">About Wireguard-UI</h3>
</div>
<!-- /.card-header -->
<div class="card-body">
<div class="form-group">
<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>
</div>
<div class="form-group">
<label for="latestRelease" class="control-label">Latest release</label>
<input type="text" class="form-control" id="latestRelease" readonly>
</div>
<div class="form-group">
<label for="latestReleaseDate" class="control-label">Latest release date</label>
<input type="text" class="form-control" id="latestReleaseDate" readonly>
</div>
<div class="form-group">
<label for="author" class="control-label">Author</label>
<div id="author">
<a id="authorLink">
<img id="authorImage"
style="width: 50px; height: 50px; border-radius: 50%; border: 1px solid #000;">
</a>
</div>
</div>
<div class="form-group">
<label for="contributors" class="control-label">Contributors</label>
<div id="contributors"></div>
</div>
<strong>Copyright &copy;
<script>document.write(new Date().getFullYear())</script>
<a href="https://github.com/ngoduykhanh/wireguard-ui">Wireguard UI</a>.
</strong> All rights reserved.
</div>
</div>
<!-- /.card -->
</div>
</div>
<!-- /.row -->
</div>
</section>
{{ end }}
{{ define "bottom_js"}}
<script>
$(document).ready(function () {
$.ajax({
cache: false,
method: 'GET',
url: 'https://api.github.com/repos/ngoduykhanh/wireguard-ui/releases/tags/' + $("#version").val(),
dataType: 'json',
contentType: "application/json",
success: function (data) {
$("#currentReleaseDate").attr("value", data.published_at.split("T")[0]);
},
error: function (jqXHR, exception) {
$("#currentReleaseDate").attr("value", "Could not find this version on GitHub.com");
}
});
$.ajax({
cache: false,
method: 'GET',
url: 'https://api.github.com/repos/ngoduykhanh/wireguard-ui/releases/latest',
dataType: 'json',
contentType: "application/json",
success: function (data) {
$("#latestRelease").attr("value", data.tag_name);
$("#latestReleaseDate").attr("value", data.published_at.split("T")[0]);
$("#author").attr("value", data.author.login);
$("#authorImage").attr("src", data.author.avatar_url);
$("#authorImage").after("<b> " + data.author.login + "</b>");
$("#authorLink").attr("href", data.author.html_url);
},
error: function (jqXHR, exception) {
$("#latestRelease").attr("value", "Could not connect to GitHub.com");
$("#latestReleaseDate").attr("value", "Could not connect to GitHub.com");
$("#author").attr("value", "Could not connect to GitHub.com");
}
});
$.ajax({
cache: false,
method: 'GET',
url: 'https://api.github.com/repos/ngoduykhanh/wireguard-ui/contributors',
dataType: 'json',
contentType: "application/json",
success: function (data) {
data.forEach(contributor => $("#contributors").append("<a href=\"" + contributor.html_url + "\" title=\"" + contributor.login + "\">" +
"<img src=\"" + contributor.avatar_url + "\" style=\"width: 50px; height: 50px; border-radius: 50%; border: 1px solid #000; margin: 5px;\"/></a>"));
},
error: function (jqXHR, exception) {
$("#contributors").html("<p>Could not connect to GitHub.com</p>");
}
});
});
$(document).ajaxStop(function () {
if (Date.parse($("#currentReleaseDate").val()) < Date.parse($("#latestReleaseDate").val())) {
$("#currentReleaseDate").after("<p style=\"color:red\">Current version is out of date</p>")
}
});
</script>
{{ end }}

View file

@ -8,6 +8,8 @@
<title>{{template "title" .}}</title> <title>{{template "title" .}}</title>
<!-- Tell the browser to be responsive to screen width --> <!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Favicon -->
<link rel="icon" href="{{.basePath}}/favicon">
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css"> <link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css">
@ -44,24 +46,31 @@
</ul> </ul>
<!-- SEARCH FORM --> <!-- SEARCH FORM -->
<!-- <form class="form-inline ml-3">--> <form class="form-inline ml-3" style="display: none" id="search-form">
<!-- <div class="input-group input-group-sm">--> <div class="input-group input-group-sm">
<!-- <input class="form-control form-control-navbar" type="search" placeholder="Search"--> <input class="form-control form-control-navbar" placeholder="Search"
<!-- aria-label="Search">--> aria-label="Search" id="search-input">
<!-- <div class="input-group-append">--> <div class="input-group-append">
<!-- <button class="btn btn-navbar" type="submit">--> <button class="btn-navbar" type="submit" disabled>
<!-- <i class="fas fa-search"></i>--> <i class="fas fa-search"></i>
<!-- </button>--> </button>
<!-- </div>--> </div>
<!-- </div>--> </div>
<!-- </form>--> <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 --> <!-- Right navbar links -->
<div class="navbar-nav ml-auto"> <div class="navbar-nav ml-auto">
<button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" <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 data-target="#modal_new_client"><i class="nav-icon fas fa-plus"></i> New
Client</button> 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 data-target="#modal_apply_config"><i class="nav-icon fas fa-check"></i> Apply
Config</button> Config</button>
{{if .baseData.CurrentUser}} {{if .baseData.CurrentUser}}
@ -88,7 +97,13 @@
</div> </div>
<div class="info"> <div class="info">
{{if .baseData.CurrentUser}} {{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}} {{else}}
<a href="#" class="d-block">Administrator</a> <a href="#" class="d-block">Administrator</a>
{{end}} {{end}}
@ -107,6 +122,8 @@
</p> </p>
</a> </a>
</li> </li>
{{if .baseData.Admin}}
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}"> <a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
<i class="nav-icon fas fa-server"></i> <i class="nav-icon fas fa-server"></i>
@ -115,6 +132,8 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-header">SETTINGS</li> <li class="nav-header">SETTINGS</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}"> <a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
@ -124,6 +143,16 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-item">
<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-header">UTILITIES</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}">
@ -141,6 +170,15 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-header">ABOUT</li>
<li class="nav-item">
<a href="{{.basePath}}/about" class="nav-link {{if eq .baseData.Active "about" }}active{{end}}">
<i class="nav-icon fas fa-solid fa-id-card"></i>
<p>
About
</p>
</a>
</li>
</ul> </ul>
</nav> </nav>
<!-- /.sidebar-menu --> <!-- /.sidebar-menu -->
@ -281,7 +319,7 @@
<!-- /.content --> <!-- /.content -->
</div> </div>
<!-- /.content-wrapper --> <!-- /.content-wrapper -->
<!--
<footer class="main-footer"> <footer class="main-footer">
<div class="float-right d-none d-sm-block"> <div class="float-right d-none d-sm-block">
<b>Version</b> {{ .appVersion }} <b>Version</b> {{ .appVersion }}
@ -289,7 +327,7 @@
<strong>Copyright &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://github.com/ngoduykhanh/wireguard-ui">Wireguard UI</a>.</strong> All rights <strong>Copyright &copy; <script>document.write(new Date().getFullYear())</script> <a href="https://github.com/ngoduykhanh/wireguard-ui">Wireguard UI</a>.</strong> All rights
reserved. reserved.
</footer> </footer>
-->
<!-- Control Sidebar --> <!-- Control Sidebar -->
<aside class="control-sidebar control-sidebar-dark"> <aside class="control-sidebar control-sidebar-dark">
<!-- Control sidebar content goes here --> <!-- Control sidebar content goes here -->
@ -320,6 +358,32 @@
$('[data-toggle="tooltip"]').tooltip() $('[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 // populateClient function for render new client info
// on the client page. // on the client page.
function populateClient(client_id) { function populateClient(client_id) {

View file

@ -71,14 +71,7 @@ Wireguard Clients
<div class="modal-body"> <div class="modal-body">
<input type="hidden" id="qr_client_id" name="qr_client_id"> <input type="hidden" id="qr_client_id" name="qr_client_id">
<img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" /> <img id="qr_code" class="w-100" style="image-rendering: pixelated;" src="" alt="QR code" />
<div class="form-group"> <!-- do not include FwMark in any client configs: it is INVALID. -->
<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>
</div> </div>
</div> </div>
<!-- /.modal-content --> <!-- /.modal-content -->
@ -250,6 +243,93 @@ Wireguard Clients
populateClientList(); populateClientList();
}) })
// show search bar and override :contains to be case-insensitive
$(document).ready(function () {
$("#search-form").show();
jQuery.expr[':'].contains = function(a, i, m) {
return jQuery(a).text().toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0;
};
})
// 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() {
if($(this).children('i.fa-user').length > 0 || $(this).children('i.fa-envelope').length > 0)
{
$(this).filter(':contains("' + query + '")').parent().parent().parent().show();
}
})
$(".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 modal event
$("#modal_pause_client").on('show.bs.modal', function (event) { $("#modal_pause_client").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget); const button = $(event.relatedTarget);
@ -391,6 +471,7 @@ Wireguard Clients
function regenerateQRCode() { function regenerateQRCode() {
const client_id = $("#qr_client_id").val(); const client_id = $("#qr_client_id").val();
const QRCodeImg = $("#qr_code"); const QRCodeImg = $("#qr_code");
const QRCodeA = $("#qr_code_a");
let include_fwmark = false; let include_fwmark = false;
if ($("#qr_include_fwmark").is(':checked')){ if ($("#qr_include_fwmark").is(':checked')){
include_fwmark = true; include_fwmark = true;
@ -400,9 +481,7 @@ Wireguard Clients
cache: false, cache: false,
method: 'GET', method: 'GET',
url: '{{.basePath}}/api/client/' + client_id, url: '{{.basePath}}/api/client/' + client_id,
data: { data: JSON.stringify(data),
qrCodeIncludeFwMark: include_fwmark
},
dataType: 'json', dataType: 'json',
contentType: "application/json", contentType: "application/json",
success: function (resp) { success: function (resp) {
@ -410,6 +489,8 @@ Wireguard Clients
$(".modal-title").text("Scan QR Code for " + client.name + " profile"); $(".modal-title").text("Scan QR Code for " + client.name + " profile");
QRCodeImg.attr('src', resp.QRCode).show(); QRCodeImg.attr('src', resp.QRCode).show();
QRCodeA.attr('download', resp.Client.name);
QRCodeA.attr('href', resp.QRCode).show();
}, },
error: function (jqXHR, exception) { error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);

View file

@ -56,10 +56,10 @@ Global Settings
value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}"> value="{{if .globalSettings.PersistentKeepalive }}{{ .globalSettings.PersistentKeepalive }}{{end}}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="forward_mark">Forward Mark</label> <label for="firewall_mark">Firewall Mark</label>
<input type="text" class="form-control" id="forward_mark" <input type="text" class="form-control" id="firewall_mark"
name="forward_mark" placeholder="Forward Mark" name="firewall_mark" placeholder="Firewall Mark"
value="{{ .globalSettings.ForwardMark }}"> value="{{ .globalSettings.FirewallMark }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_file_path">Wireguard Config File Path</label> <label for="config_file_path">Wireguard Config File Path</label>
@ -91,7 +91,7 @@ Global Settings
<dt>2. DNS Servers</dt> <dt>2. DNS Servers</dt>
<dd>The DNS servers will be set to client config.</dd> <dd>The DNS servers will be set to client config.</dd>
<dt>3. MTU</dt> <dt>3. MTU</dt>
<dd>The MTU will be set to server and client config. By default it is <code>1420</code>. You might want <dd>The MTU will be set to server and client config. By default it is <code>1450</code>. You might want
to adjust the MTU size if your connection (e.g PPPoE, 3G, satellite network, etc) has a low MTU.</dd> to adjust the MTU size if your connection (e.g PPPoE, 3G, satellite network, etc) has a low MTU.</dd>
<dd>Leave blank to omit this setting in the configs.</dd> <dd>Leave blank to omit this setting in the configs.</dd>
<dt>4. Persistent Keepalive</dt> <dt>4. Persistent Keepalive</dt>
@ -100,8 +100,8 @@ Global Settings
until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code> until they reach out to other peers themselves. Adding <code>PersistentKeepalive</code>
can ensure that the connection remains open.</dd> can ensure that the connection remains open.</dd>
<dd>Leave blank to omit this setting in the Client config.</dd> <dd>Leave blank to omit this setting in the Client config.</dd>
<dt>5. Forward Mark</dt> <dt>5. Firewall Mark</dt>
<dd>Set an <code>fwmark</code> on all packets going out of WireGuard's UDP socket. Default value: <code>0xca6c</code></dd> <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> <dt>6. Wireguard Config File Path</dt>
<dd>The path of your Wireguard server config file. Please make sure the parent directory <dd>The path of your Wireguard server config file. Please make sure the parent directory
exists and is writable.</dd> exists and is writable.</dd>
@ -149,9 +149,9 @@ Global Settings
const dns_servers = $("#dns_servers").val().split(","); const dns_servers = $("#dns_servers").val().split(",");
const mtu = $("#mtu").val(); const mtu = $("#mtu").val();
const persistent_keepalive = $("#persistent_keepalive").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 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({ $.ajax({
cache: false, cache: false,
@ -222,7 +222,7 @@ Global Settings
config_file_path: { config_file_path: {
required: true required: true
}, },
forward_mark: { firewall_mark: {
required: false required: false
} }
}, },

View file

@ -7,6 +7,8 @@
<title>WireGuard UI</title> <title>WireGuard UI</title>
<!-- Tell the browser to be responsive to screen width --> <!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Favicon -->
<link rel="icon" href="{{.basePath}}/favicon">
<!-- Font Awesome --> <!-- Font Awesome -->
<link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css"> <link rel="stylesheet" href="{{.basePath}}/static/plugins/fontawesome-free/css/all.min.css">
@ -99,7 +101,11 @@
$("#btn_login").click(function () { $("#btn_login").click(function () {
const username = $("#username").val(); const username = $("#username").val();
const password = $("#password").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({ $.ajax({
cache: false, cache: false,

View file

@ -31,7 +31,7 @@ Profile
<div class="form-group"> <div class="form-group">
<label for="username" class="control-label">Username</label> <label for="username" class="control-label">Username</label>
<input type="text" class="form-control" name="username" id="username" <input type="text" class="form-control" name="username" id="username"
value="{{ .userInfo.Username }}"> value="">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">Password</label> <label for="password" class="control-label">Password</label>
@ -55,19 +55,45 @@ Profile
{{ define "bottom_js"}} {{ define "bottom_js"}}
<script> <script>
{
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']);
}
});
});
function updateUserInfo() { function updateUserInfo() {
const username = $("#username").val(); const username = $("#username").val();
const password = $("#password").val(); const password = $("#password").val();
const data = {"username": username, "password": password}; const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin};
$.ajax({ $.ajax({
cache: false, cache: false,
method: 'POST', method: 'POST',
url: '{{.basePath}}/profile', url: '{{.basePath}}/update-user',
dataType: 'json', dataType: 'json',
contentType: "application/json", contentType: "application/json",
data: JSON.stringify(data), data: JSON.stringify(data),
success: function (data) { success: function (data) {
toastr.success("Updated admin user information successfully"); toastr.success("Updated user information successfully");
location.reload();
}, },
error: function (jqXHR, exception) { error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText); const responseJson = jQuery.parseJSON(jqXHR.responseText);

View file

@ -41,6 +41,8 @@ Connected Peers
<th scope="col">#</th> <th scope="col">#</th>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col">Email</th> <th scope="col">Email</th>
<th scope="col">Allocated IPs</th>
<th scope="col">Endpoint</th>
<th scope="col">Public Key</th> <th scope="col">Public Key</th>
<th scope="col">Received</th> <th scope="col">Received</th>
<th scope="col">Transmitted</th> <th scope="col">Transmitted</th>
@ -54,6 +56,8 @@ Connected Peers
<th scope="row">{{ $idx }}</th> <th scope="row">{{ $idx }}</th>
<td>{{ $peer.Name }}</td> <td>{{ $peer.Name }}</td>
<td>{{ $peer.Email }}</td> <td>{{ $peer.Email }}</td>
<td>{{ $peer.AllocatedIP }}</td>
<td>{{ $peer.Endpoint }}</td>
<td>{{ $peer.PublicKey }}</td> <td>{{ $peer.PublicKey }}</td>
<td title="{{ $peer.ReceivedBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.ReceivedBytes }}))</script></td> <td title="{{ $peer.ReceivedBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.ReceivedBytes }}))</script></td>
<td title="{{ $peer.TransmitBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.TransmitBytes }}))</script></td> <td title="{{ $peer.TransmitBytes }} Bytes"><script>document.write(bytesToHumanReadable({{ $peer.TransmitBytes }}))</script></td>

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,22 +24,25 @@ var (
const ( const (
DefaultUsername = "admin" DefaultUsername = "admin"
DefaultPassword = "admin" DefaultPassword = "admin"
DefaultIsAdmin = true
DefaultServerAddress = "10.252.1.0/24" DefaultServerAddress = "10.252.1.0/24"
DefaultServerPort = 51820 DefaultServerPort = 51820
DefaultDNS = "1.1.1.1" DefaultDNS = "1.1.1.1"
DefaultMTU = 1450 DefaultMTU = 1450
DefaultPersistentKeepalive = 15 DefaultPersistentKeepalive = 15
DefaultForwardMark = "0xca6c" DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultConfigFilePath = "/etc/wireguard/wg0.conf" DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
UsernameEnvVar = "WGUI_USERNAME" UsernameEnvVar = "WGUI_USERNAME"
PasswordEnvVar = "WGUI_PASSWORD" PasswordEnvVar = "WGUI_PASSWORD"
PasswordHashEnvVar = "WGUI_PASSWORD_HASH" PasswordHashEnvVar = "WGUI_PASSWORD_HASH"
FaviconFilePathEnvVar = "WGUI_FAVICON_FILE_PATH"
EndpointAddressEnvVar = "WGUI_ENDPOINT_ADDRESS" EndpointAddressEnvVar = "WGUI_ENDPOINT_ADDRESS"
DNSEnvVar = "WGUI_DNS" DNSEnvVar = "WGUI_DNS"
MTUEnvVar = "WGUI_MTU" MTUEnvVar = "WGUI_MTU"
PersistentKeepaliveEnvVar = "WGUI_PERSISTENT_KEEPALIVE" PersistentKeepaliveEnvVar = "WGUI_PERSISTENT_KEEPALIVE"
ForwardMarkEnvVar = "WGUI_FORWARD_MARK" FirewallMarkEnvVar = "WGUI_FIREWALL_MARK"
ConfigFilePathEnvVar = "WGUI_CONFIG_FILE_PATH" ConfigFilePathEnvVar = "WGUI_CONFIG_FILE_PATH"
LogLevel = "WGUI_LOG_LEVEL"
ServerAddressesEnvVar = "WGUI_SERVER_INTERFACE_ADDRESSES" ServerAddressesEnvVar = "WGUI_SERVER_INTERFACE_ADDRESSES"
ServerListenPortEnvVar = "WGUI_SERVER_LISTEN_PORT" ServerListenPortEnvVar = "WGUI_SERVER_LISTEN_PORT"
ServerPostUpScriptEnvVar = "WGUI_SERVER_POST_UP_SCRIPT" ServerPostUpScriptEnvVar = "WGUI_SERVER_POST_UP_SCRIPT"

View file

@ -4,9 +4,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/ngoduykhanh/wireguard-ui/store"
"golang.org/x/mod/sumdb/dirhash"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"text/template" "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) 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 // build the config as string
strConfig := "[Interface]\n" + strConfig := "[Interface]\n" +
clientAddress + clientAddress +
clientPrivateKey + clientPrivateKey +
clientDNS + clientDNS +
clientMTU + clientMTU +
forwardMark +
"\n[Peer]\n" + "\n[Peer]\n" +
peerPublicKey + peerPublicKey +
peerPresharedKey + peerPresharedKey +
@ -383,7 +382,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
} }
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf // 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 var tmplWireguardConf string
// if set, read wg.conf template from WgConfTemplate // if set, read wg.conf template from WgConfTemplate
@ -418,6 +417,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
"serverConfig": serverConfig, "serverConfig": serverConfig,
"clientDataList": clientDataList, "clientDataList": clientDataList,
"globalSettings": globalSettings, "globalSettings": globalSettings,
"usersList": usersList,
} }
err = t.Execute(f, config) err = t.Execute(f, config)
@ -464,3 +464,55 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string {
} }
return defaultVal 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)
}