Merge branch 'master' into email-settings-UI
This commit is contained in:
		
						commit
						571e34eeb1
					
				| 
						 | 
					@ -25,3 +25,6 @@ docker-compose*
 | 
				
			||||||
db
 | 
					db
 | 
				
			||||||
assets
 | 
					assets
 | 
				
			||||||
wireguard-ui
 | 
					wireguard-ui
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Examples
 | 
				
			||||||
 | 
					examples
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,3 +21,7 @@ rice-box.go
 | 
				
			||||||
# IDEs
 | 
					# IDEs
 | 
				
			||||||
.vscode
 | 
					.vscode
 | 
				
			||||||
.idea
 | 
					.idea
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Examples
 | 
				
			||||||
 | 
					examples/docker-compose/config
 | 
				
			||||||
 | 
					examples/docker-compose/db
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										26
									
								
								README.md
								
								
								
								
							
							
						
						
									
										26
									
								
								README.md
								
								
								
								
							| 
						 | 
					@ -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                            |
 | 
				
			||||||
| 
						 | 
					@ -53,13 +43,15 @@ Note:
 | 
				
			||||||
| `WGUI_USERNAME`             | The username for the login page. Used for db initialization only                                                                                             | `admin`                            |
 | 
					| `WGUI_USERNAME`             | The username for the login page. Used for db initialization only                                                                                             | `admin`                            |
 | 
				
			||||||
| `WGUI_PASSWORD`             | The password for the user on the login page. Will be hashed automatically. Used for db initialization only                                                   | `admin`                            |
 | 
					| `WGUI_PASSWORD`             | 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_FAVICON_FILE_PATH`    | The file path used as website favicon                                                                                                                        | Embedded WireGuard logo            |
 | 
					| `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_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`                     |
 | 
				
			||||||
| 
						 | 
					@ -202,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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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).
 | 
				
			||||||
| 
						 | 
					@ -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"
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
								
								
								
								
							
							
						
						
									
										3
									
								
								go.mod
								
								
								
								
							| 
						 | 
					@ -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
								
								
								
								
							
							
						
						
									
										24
									
								
								go.sum
								
								
								
								
							| 
						 | 
					@ -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=
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,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())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,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"})
 | 
				
			||||||
| 
						 | 
					@ -102,6 +117,40 @@ func Login(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUsers handler return a JSON list of all users
 | 
				
			||||||
 | 
					func GetUsers(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							usersList, err := db.GetUsers()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
 | 
				
			||||||
 | 
									false, fmt.Sprintf("Cannot get user list: %v", err),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return c.JSON(http.StatusOK, usersList)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUser handler returns a JSON object of single user
 | 
				
			||||||
 | 
					func GetUser(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							username := c.Param("username")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !isAdmin(c) && (username != currentUser(c)) {
 | 
				
			||||||
 | 
								return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							userData, err := db.GetUserByName(username)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return c.JSON(http.StatusOK, userData)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Logout to log a user out
 | 
					// 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 {
 | 
				
			||||||
| 
						 | 
					@ -113,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)
 | 
				
			||||||
| 
						 | 
					@ -138,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()})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -150,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 {
 | 
				
			||||||
| 
						 | 
					@ -158,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"})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -179,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,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -541,7 +693,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,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
| 
						 | 
					@ -609,7 +761,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,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -697,7 +849,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,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
| 
						 | 
					@ -706,7 +858,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,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
| 
						 | 
					@ -718,7 +870,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,
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
| 
						 | 
					@ -764,7 +916,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":    "",
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
| 
						 | 
					@ -878,6 +1030,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
 | 
				
			||||||
			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"})
 | 
								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)
 | 
				
			||||||
| 
						 | 
					@ -885,7 +1043,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{
 | 
				
			||||||
| 
						 | 
					@ -893,15 +1051,35 @@ 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
 | 
					// AboutPage handler
 | 
				
			||||||
func AboutPage() echo.HandlerFunc {
 | 
					func AboutPage() echo.HandlerFunc {
 | 
				
			||||||
	return func(c echo.Context) error {
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
		return c.Render(http.StatusOK, "about.html", map[string]interface{}{
 | 
							return c.Render(http.StatusOK, "about.html", map[string]interface{}{
 | 
				
			||||||
			"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c)},
 | 
								"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c), Admin: isAdmin(c)},
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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":    "",
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,15 +14,24 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
 | 
				
			||||||
		if !isValidSession(c) {
 | 
							if !isValidSession(c) {
 | 
				
			||||||
			nextURL := c.Request().URL
 | 
								nextURL := c.Request().URL
 | 
				
			||||||
			if nextURL != nil && c.Request().Method == http.MethodGet {
 | 
								if nextURL != nil && c.Request().Method == http.MethodGet {
 | 
				
			||||||
				return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath + "/login?next=%s", c.Request().URL))
 | 
									return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL))
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				return c.Redirect(http.StatusTemporaryRedirect, util.BasePath + "/login")
 | 
									return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return next(c)
 | 
							return next(c)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
 | 
				
			||||||
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
 | 
							if !isAdmin(c) {
 | 
				
			||||||
 | 
								return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return next(c)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func isValidSession(c echo.Context) bool {
 | 
					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
								
								
								
								
							
							
						
						
									
										34
									
								
								main.go
								
								
								
								
							| 
						 | 
					@ -80,6 +80,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)
 | 
				
			||||||
| 
						 | 
					@ -95,6 +97,7 @@ func init() {
 | 
				
			||||||
		//fmt.Println("Session secret\t:", util.SessionSecret)
 | 
							//fmt.Println("Session secret\t:", util.SessionSecret)
 | 
				
			||||||
		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() {
 | 
				
			||||||
| 
						 | 
					@ -108,6 +111,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
 | 
				
			||||||
| 
						 | 
					@ -157,10 +161,16 @@ 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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession)
 | 
				
			||||||
	app.GET(util.BasePath+"/about", handler.AboutPage())
 | 
						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.GET(util.BasePath+"/favicon", handler.Favicon())
 | 
				
			||||||
| 
						 | 
					@ -170,13 +180,14 @@ func main() {
 | 
				
			||||||
	app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.POST(util.BasePath+"/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+"/email-settings", handler.EmailSettings(db), handler.ValidSession)
 | 
					  app.GET(util.BasePath+"/email-settings", handler.EmailSettings(db), handler.ValidSession, handler.NeedsAdmin)
 | 
				
			||||||
	app.POST(util.BasePath+"/email-settings", handler.EmailSettingsSubmit(db), handler.ValidSession)
 | 
						app.POST(util.BasePath+"/email-settings", handler.EmailSettingsSubmit(db), handler.ValidSession, handler.NeedsAdmin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession)
 | 
						app.GET(util.BasePath+"/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)
 | 
				
			||||||
| 
						 | 
					@ -215,8 +226,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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,6 +88,15 @@ 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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
| 
						 | 
					@ -114,14 +123,33 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
 | 
				
			||||||
	templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString))
 | 
						templates["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["email_settings.html"] = template.Must(template.New("email_settings").Funcs(funcs).Parse(tmplBaseString + tmplEmailSettingsString))
 | 
						templates["email_settings.html"] = template.Must(template.New("email_settings").Funcs(funcs).Parse(tmplBaseString + tmplEmailSettingsString))
 | 
				
			||||||
 | 
						templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString))
 | 
				
			||||||
	templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString))
 | 
						templates["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))
 | 
						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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,7 @@ func (o *JsonDB) Init() error {
 | 
				
			||||||
	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 emailSettingPath string = path.Join(serverPath, "email_settings.json")
 | 
						var emailSettingPath string = path.Join(serverPath, "email_settings.json")
 | 
				
			||||||
 | 
						var hashesPath string = path.Join(serverPath, "hashes.json")
 | 
				
			||||||
	var userPath string = path.Join(serverPath, "users.json")
 | 
						var userPath string = path.Join(serverPath, "users.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// create directories if they do not exist
 | 
						// create directories if they do not exist
 | 
				
			||||||
| 
						 | 
					@ -55,6 +56,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) {
 | 
				
			||||||
| 
						 | 
					@ -98,12 +102,20 @@ 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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// email settings
 | 
						// email settings
 | 
				
			||||||
	// TODO move ENV_VAR variables and default values to config.go
 | 
						// TODO move ENV_VAR variables and default values to config.go
 | 
				
			||||||
	if _, err := os.Stat(emailSettingPath); os.IsNotExist(err) {
 | 
						if _, err := os.Stat(emailSettingPath); os.IsNotExist(err) {
 | 
				
			||||||
| 
						 | 
					@ -126,9 +138,11 @@ func (o *JsonDB) Init() error {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 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)
 | 
				
			||||||
| 
						 | 
					@ -138,7 +152,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
 | 
				
			||||||
| 
						 | 
					@ -150,9 +164,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
 | 
				
			||||||
| 
						 | 
					@ -236,15 +285,12 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
 | 
				
			||||||
		server, _ := o.GetServer()
 | 
							server, _ := o.GetServer()
 | 
				
			||||||
		globalSettings, _ := o.GetGlobalSettings()
 | 
							globalSettings, _ := o.GetGlobalSettings()
 | 
				
			||||||
		client := client
 | 
							client := client
 | 
				
			||||||
		if !qrCodeSettings.IncludeDNS{
 | 
							if !qrCodeSettings.IncludeDNS {
 | 
				
			||||||
			globalSettings.DNSServers = []string{}
 | 
								globalSettings.DNSServers = []string{}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		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 {
 | 
				
			||||||
| 
						 | 
					@ -288,3 +334,16 @@ func (o *JsonDB) GetEmailSettings() (model.EmailSetting, error) {
 | 
				
			||||||
func (o *JsonDB) SaveEmailSettings(emailSettings model.EmailSetting) error {
 | 
					func (o *JsonDB) SaveEmailSettings(emailSettings model.EmailSetting) error {
 | 
				
			||||||
	return o.conn.Write("server", "email_settings", emailSettings)
 | 
						return o.conn.Write("server", "email_settings", emailSettings)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o *JsonDB) GetPath() string {
 | 
				
			||||||
 | 
						return o.dbPath
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) {
 | 
				
			||||||
 | 
						hashes := model.ClientServerHashes{}
 | 
				
			||||||
 | 
						return hashes, o.conn.Read("server", "hashes", &hashes)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error {
 | 
				
			||||||
 | 
						return o.conn.Write("server", "hashes", hashes)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
	GetEmailSettings() (model.EmailSetting, error)
 | 
						GetEmailSettings() (model.EmailSetting, error)
 | 
				
			||||||
	GetServer() (model.Server, error)
 | 
						GetServer() (model.Server, error)
 | 
				
			||||||
| 
						 | 
					@ -24,4 +26,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)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,12 @@ About
 | 
				
			||||||
                            <label for="version" class="control-label">Current version</label>
 | 
					                            <label for="version" class="control-label">Current version</label>
 | 
				
			||||||
                            <input type="text" class="form-control" id="version" value="{{ .appVersion }}" readonly>
 | 
					                            <input type="text" class="form-control" id="version" value="{{ .appVersion }}" readonly>
 | 
				
			||||||
                        </div>
 | 
					                        </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">
 | 
					                        <div class="form-group">
 | 
				
			||||||
                            <label for="currentReleaseDate" class="control-label">Current version release date</label>
 | 
					                            <label for="currentReleaseDate" class="control-label">Current version release date</label>
 | 
				
			||||||
                            <input type="text" class="form-control" id="currentReleaseDate" readonly>
 | 
					                            <input type="text" class="form-control" id="currentReleaseDate" readonly>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,6 +56,13 @@
 | 
				
			||||||
                        </button>
 | 
					                        </button>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					                <select name="status-selector" id="status-selector" class="form-control selectpicker show-tick" style="margin-left: 10px">
 | 
				
			||||||
 | 
					                    <option value="All">All</option>
 | 
				
			||||||
 | 
					                    <option value="Enabled">Enabled</option>
 | 
				
			||||||
 | 
					                    <option value="Disabled">Disabled</option>
 | 
				
			||||||
 | 
					                    <option value="Connected">Connected</option>
 | 
				
			||||||
 | 
					                    <option value="Disconnected">Disconnected</option>
 | 
				
			||||||
 | 
					                </select>
 | 
				
			||||||
            </form>
 | 
					            </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- Right navbar links -->
 | 
					            <!-- Right navbar links -->
 | 
				
			||||||
| 
						 | 
					@ -63,7 +70,7 @@
 | 
				
			||||||
                <button style="margin-left: 0.5em;" type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal"
 | 
					                <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}}
 | 
				
			||||||
| 
						 | 
					@ -90,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}}
 | 
				
			||||||
| 
						 | 
					@ -109,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>
 | 
				
			||||||
| 
						 | 
					@ -117,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}}">
 | 
				
			||||||
| 
						 | 
					@ -134,6 +151,14 @@
 | 
				
			||||||
                            </p>
 | 
					                            </p>
 | 
				
			||||||
                            </a>
 | 
					                            </a>
 | 
				
			||||||
                        </li>
 | 
					                        </li>
 | 
				
			||||||
 | 
					                            <a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}">
 | 
				
			||||||
 | 
					                            <i class="nav-icon fas fa-cog"></i>
 | 
				
			||||||
 | 
					                            <p>
 | 
				
			||||||
 | 
					                                Users Settings
 | 
				
			||||||
 | 
					                            </p>
 | 
				
			||||||
 | 
					                            </a>
 | 
				
			||||||
 | 
					                        </li>
 | 
				
			||||||
 | 
					                        {{end}}
 | 
				
			||||||
                        <li class="nav-header">UTILITIES</li>
 | 
					                        <li class="nav-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}}">
 | 
				
			||||||
| 
						 | 
					@ -347,6 +372,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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,17 +70,8 @@ Wireguard Clients
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <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">
 | 
				
			||||||
                <a href="" download="" id="qr_code_a">
 | 
					 | 
				
			||||||
                <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" />
 | 
				
			||||||
                </a>
 | 
					                <!-- do not include FwMark in any client configs: it is INVALID. -->
 | 
				
			||||||
                <div class="form-group">
 | 
					 | 
				
			||||||
                    <div class="icheck-primary d-inline">
 | 
					 | 
				
			||||||
                        <input type="checkbox" id="qr_include_fwmark" onchange="regenerateQRCode()">
 | 
					 | 
				
			||||||
                        <label for="qr_include_fwmark">
 | 
					 | 
				
			||||||
                            Include FwMark
 | 
					 | 
				
			||||||
                        </label>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <!-- /.modal-content -->
 | 
					        <!-- /.modal-content -->
 | 
				
			||||||
| 
						 | 
					@ -263,6 +254,7 @@ Wireguard Clients
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // hide all clients and display only the ones that meet the search criteria (name, email, IP)
 | 
					        // hide all clients and display only the ones that meet the search criteria (name, email, IP)
 | 
				
			||||||
        $('#search-input').keyup(function () {
 | 
					        $('#search-input').keyup(function () {
 | 
				
			||||||
 | 
					            $("#status-selector").val("All");
 | 
				
			||||||
            var query = $(this).val();
 | 
					            var query = $(this).val();
 | 
				
			||||||
            $('.col-lg-4').hide();
 | 
					            $('.col-lg-4').hide();
 | 
				
			||||||
            $(".info-box-text").each(function() {
 | 
					            $(".info-box-text").each(function() {
 | 
				
			||||||
| 
						 | 
					@ -274,6 +266,70 @@ Wireguard Clients
 | 
				
			||||||
            $(".badge-secondary").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);
 | 
				
			||||||
| 
						 | 
					@ -425,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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -101,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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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">×</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">×</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}}
 | 
				
			||||||
| 
						 | 
					@ -24,12 +24,13 @@ 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"
 | 
				
			||||||
| 
						 | 
					@ -39,8 +40,9 @@ const (
 | 
				
			||||||
	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"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										73
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										73
									
								
								util/util.go
								
								
								
								
							| 
						 | 
					@ -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 +
 | 
				
			||||||
| 
						 | 
					@ -221,10 +220,12 @@ func GetPublicIP() (model.Interface, error) {
 | 
				
			||||||
	ip, err := consensus.ExternalIP()
 | 
						ip, err := consensus.ExternalIP()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		publicInterface.IPAddress = "N/A"
 | 
							publicInterface.IPAddress = "N/A"
 | 
				
			||||||
	}
 | 
						} else {
 | 
				
			||||||
		publicInterface.IPAddress = ip.String()
 | 
							publicInterface.IPAddress = ip.String()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return publicInterface, err
 | 
						// error handling happend above, no need to pass it through
 | 
				
			||||||
 | 
						return publicInterface, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetIPFromCIDR get ip from CIDR
 | 
					// GetIPFromCIDR get ip from CIDR
 | 
				
			||||||
| 
						 | 
					@ -381,7 +382,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | 
					// 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
 | 
				
			||||||
| 
						 | 
					@ -416,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)
 | 
				
			||||||
| 
						 | 
					@ -462,3 +464,56 @@ 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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue