Merge branch 'master' into try-add-apikey
This commit is contained in:
		
						commit
						c8111e4b2c
					
				|  | @ -25,3 +25,6 @@ docker-compose* | ||||||
| db | db | ||||||
| assets | assets | ||||||
| wireguard-ui | wireguard-ui | ||||||
|  | 
 | ||||||
|  | # Examples | ||||||
|  | examples | ||||||
|  |  | ||||||
|  | @ -40,20 +40,12 @@ jobs: | ||||||
|         node-version: '14' |         node-version: '14' | ||||||
|         registry-url: 'https://registry.npmjs.org' |         registry-url: 'https://registry.npmjs.org' | ||||||
| 
 | 
 | ||||||
|     # prepare assets for go rice |     # prepare assets | ||||||
|     - name: Prepare assets |     - name: Prepare assets | ||||||
|       run: | |       run: | | ||||||
|         chmod +x ./prepare_assets.sh |         chmod +x ./prepare_assets.sh | ||||||
|         ./prepare_assets.sh |         ./prepare_assets.sh | ||||||
| 
 | 
 | ||||||
|     # get go rice tool |  | ||||||
|     - name: Get go rice tool |  | ||||||
|       run: go get github.com/GeertJohan/go.rice/rice |  | ||||||
| 
 |  | ||||||
|     # run go rice embed |  | ||||||
|     - name: Run go rice embed |  | ||||||
|       run: ${HOME}/go/bin/rice embed-go |  | ||||||
| 
 |  | ||||||
|     # build and make the releases |     # build and make the releases | ||||||
|     - name: Build and make the releases |     - name: Build and make the releases | ||||||
|       uses: wangyoucao577/go-release-action@master |       uses: wangyoucao577/go-release-action@master | ||||||
|  | @ -62,6 +54,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 }} | ||||||
|  |  | ||||||
|  | @ -16,8 +16,11 @@ wireguard-ui | ||||||
| vendor/ | vendor/ | ||||||
| assets/ | assets/ | ||||||
| node_modules/ | node_modules/ | ||||||
| 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" | ||||||
|  | @ -42,10 +43,6 @@ RUN mkdir -p assets/plugins && \ | ||||||
|     /build/node_modules/jquery-tags-input/ \ |     /build/node_modules/jquery-tags-input/ \ | ||||||
|     assets/plugins/ |     assets/plugins/ | ||||||
| 
 | 
 | ||||||
| # Get go modules and build tool |  | ||||||
| RUN go mod download && \ |  | ||||||
|     go get github.com/GeertJohan/go.rice/rice |  | ||||||
| 
 |  | ||||||
| # Add sources | # Add sources | ||||||
| COPY . /build | COPY . /build | ||||||
| 
 | 
 | ||||||
|  | @ -53,8 +50,7 @@ COPY . /build | ||||||
| RUN cp -r /build/custom/ assets/ | RUN cp -r /build/custom/ assets/ | ||||||
| 
 | 
 | ||||||
| # Build | # Build | ||||||
| RUN rice embed-go && \ | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X main.gitCommit=${COMMIT}" -a -o wg-ui . | ||||||
|     CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -o wg-ui . |  | ||||||
| 
 | 
 | ||||||
| # Release stage | # Release stage | ||||||
| FROM alpine:3.16 | FROM alpine:3.16 | ||||||
|  |  | ||||||
							
								
								
									
										34
									
								
								README.md
								
								
								
								
							
							
						
						
									
										34
									
								
								README.md
								
								
								
								
							|  | @ -35,15 +35,6 @@ 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,14 +44,16 @@ 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`          | | ||||||
| | `WG_CONF_TEMPLATE`          | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/alikhanich/wireguard-ui/blob/master/templates/wg.conf) | N/A                                | | | `WGUI_LOG_LEVEL`            | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`                                                                              | `INFO`                             | | ||||||
|  | | `WG_CONF_TEMPLATE`          | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A                                | | ||||||
| | `EMAIL_FROM_ADDRESS`        | The sender email address                                                                                                                                     | N/A                                | | | `EMAIL_FROM_ADDRESS`        | The sender email address                                                                                                                                     | N/A                                | | ||||||
| | `EMAIL_FROM_NAME`           | The sender name                                                                                                                                              | `WireGuard UI`                     | | | `EMAIL_FROM_NAME`           | The sender name                                                                                                                                              | `WireGuard UI`                     | | ||||||
| | `SENDGRID_API_KEY`          | The SendGrid api key                                                                                                                                         | N/A                                | | | `SENDGRID_API_KEY`          | The SendGrid api key                                                                                                                                         | N/A                                | | ||||||
|  | @ -202,7 +195,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 | ||||||
|  | @ -213,18 +212,9 @@ Prepare the assets directory | ||||||
| ./prepare_assets.sh | ./prepare_assets.sh | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Then you can embed resources by generating Go source code | Then build your executable | ||||||
| 
 |  | ||||||
| ```sh |  | ||||||
| rice embed-go |  | ||||||
| go build -o wireguard-ui |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Or, append resources to executable as zip file |  | ||||||
| 
 |  | ||||||
| ```sh | ```sh | ||||||
| go build -o wireguard-ui | go build -o wireguard-ui | ||||||
| rice append --exec wireguard-ui |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## License | ## License | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
							
								
								
									
										6
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										6
									
								
								go.mod
								
								
								
								
							|  | @ -1,9 +1,8 @@ | ||||||
| module github.com/alikhanich/wireguard-ui | module github.com/alikhanich/wireguard-ui | ||||||
| 
 | 
 | ||||||
| go 1.14 | go 1.16 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	github.com/GeertJohan/go.rice v1.0.2 |  | ||||||
| 	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd | 	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd | ||||||
| 	github.com/go-playground/universal-translator v0.17.0 // indirect | 	github.com/go-playground/universal-translator v0.17.0 // indirect | ||||||
| 	github.com/gorilla/sessions v1.2.0 | 	github.com/gorilla/sessions v1.2.0 | ||||||
|  | @ -19,7 +18,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 | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										33
									
								
								go.sum
								
								
								
								
							|  | @ -1,8 +1,4 @@ | ||||||
| github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= |  | ||||||
| github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk= |  | ||||||
| github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4= |  | ||||||
| github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= | ||||||
| github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= |  | ||||||
| github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||||||
| github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | ||||||
| github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= | ||||||
|  | @ -12,8 +8,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r | ||||||
| github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= | github.com/casbin/casbin/v2 v2.0.0/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= | ||||||
| github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= | ||||||
| github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | github.com/coreos/bbolt v1.3.1-coreos.6.0.20180223184059-4f5275f4ebbf/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||||
| github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= |  | ||||||
| github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= |  | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
|  | @ -52,7 +46,6 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z | ||||||
| github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= | github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= | ||||||
| github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= | github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= | ||||||
| github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | github.com/jessevdk/go-flags v0.0.0-20150816100521-1acbbaff2f34/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | ||||||
| github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= |  | ||||||
| github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= | ||||||
| github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= | ||||||
| github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= | github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= | ||||||
|  | @ -109,7 +102,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ | ||||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||||
| github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | ||||||
| github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= |  | ||||||
| github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= | ||||||
| github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | @ -157,6 +149,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 +157,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 +179,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 +215,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= | ||||||
|  | @ -236,7 +244,6 @@ golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmo | ||||||
| golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= | ||||||
| golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= | ||||||
| gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |  | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= | ||||||
| gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= | ||||||
|  |  | ||||||
|  | @ -5,13 +5,13 @@ import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/fs" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	rice "github.com/GeertJohan/go.rice" |  | ||||||
| 	"github.com/gorilla/sessions" | 	"github.com/gorilla/sessions" | ||||||
| 	"github.com/labstack/echo-contrib/session" | 	"github.com/labstack/echo-contrib/session" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
|  | @ -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, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | @ -532,7 +684,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "server.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "server.html", map[string]interface{}{ | ||||||
| 			"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)}, | 			"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"serverInterface": server.Interface, | 			"serverInterface": server.Interface, | ||||||
| 			"serverKeyPair":   server.KeyPair, | 			"serverKeyPair":   server.KeyPair, | ||||||
| 		}) | 		}) | ||||||
|  | @ -600,7 +752,7 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ | ||||||
| 			"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)}, | 			"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"globalSettings": globalSettings, | 			"globalSettings": globalSettings, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | @ -630,7 +782,7 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 		wgClient, err := wgctrl.New() | 		wgClient, err := wgctrl.New() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 				"error":    err.Error(), | 				"error":    err.Error(), | ||||||
| 				"devices":  nil, | 				"devices":  nil, | ||||||
| 			}) | 			}) | ||||||
|  | @ -639,7 +791,7 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 		devices, err := wgClient.Devices() | 		devices, err := wgClient.Devices() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||||
| 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 				"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 				"error":    err.Error(), | 				"error":    err.Error(), | ||||||
| 				"devices":  nil, | 				"devices":  nil, | ||||||
| 			}) | 			}) | ||||||
|  | @ -651,7 +803,7 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 			clients, err := db.GetClients(false) | 			clients, err := db.GetClients(false) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | 				return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ | ||||||
| 					"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 					"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 					"error":    err.Error(), | 					"error":    err.Error(), | ||||||
| 					"devices":  nil, | 					"devices":  nil, | ||||||
| 				}) | 				}) | ||||||
|  | @ -697,7 +849,7 @@ func Status(db store.IStore) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return c.Render(http.StatusOK, "status.html", map[string]interface{}{ | 		return c.Render(http.StatusOK, "status.html", map[string]interface{}{ | ||||||
| 			"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)}, | 			"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, | ||||||
| 			"devices":  devicesVm, | 			"devices":  devicesVm, | ||||||
| 			"error":    "", | 			"error":    "", | ||||||
| 		}) | 		}) | ||||||
|  | @ -796,7 +948,7 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ApplyServerConfig handler to write config file and restart Wireguard server
 | // ApplyServerConfig handler to write config file and restart Wireguard server
 | ||||||
| func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	return func(c echo.Context) error { | ||||||
| 
 | 
 | ||||||
| 		server, err := db.GetServer() | 		server, err := db.GetServer() | ||||||
|  | @ -811,6 +963,12 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | ||||||
| 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		users, err := db.GetUsers() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Cannot get users config: ", err) | ||||||
|  | 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		settings, err := db.GetGlobalSettings() | 		settings, err := db.GetGlobalSettings() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("Cannot get global settings: ", err) | 			log.Error("Cannot get global settings: ", err) | ||||||
|  | @ -818,7 +976,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Write config file
 | 		// Write config file
 | ||||||
| 		err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings) | 		err = util.WriteWireGuardServerConfig(tmplDir, 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{ | ||||||
|  | @ -826,16 +984,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":    "", | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
|  | @ -23,6 +23,15 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc { | ||||||
|  | 	return func(c echo.Context) error { | ||||||
|  | 		if !isAdmin(c) { | ||||||
|  | 			return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/") | ||||||
|  | 		} | ||||||
|  | 		return next(c) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func isValidSession(c echo.Context) bool { | func isValidSession(c echo.Context) bool { | ||||||
| 	if util.DisableLogin { | 	if util.DisableLogin { | ||||||
| 		return true | 		return true | ||||||
|  | @ -53,10 +62,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()) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										63
									
								
								main.go
								
								
								
								
							
							
						
						
									
										63
									
								
								main.go
								
								
								
								
							|  | @ -1,15 +1,16 @@ | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"embed" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| 	"github.com/labstack/gommon/log" | 	"github.com/labstack/gommon/log" | ||||||
| 	"github.com/alikhanich/wireguard-ui/store" | 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||||
|  | 	"io/fs" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"time" | 	"time" | ||||||
| 
 |  | ||||||
| 	rice "github.com/GeertJohan/go.rice" | 	rice "github.com/GeertJohan/go.rice" | ||||||
| 	"github.com/alikhanich/wireguard-ui/emailer" | 	"github.com/alikhanich/wireguard-ui/emailer" | ||||||
| 	"github.com/alikhanich/wireguard-ui/handler" | 	"github.com/alikhanich/wireguard-ui/handler" | ||||||
|  | @ -52,6 +53,16 @@ const ( | ||||||
| ` | ` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // embed the "templates" directory
 | ||||||
|  | //
 | ||||||
|  | //go:embed templates/*
 | ||||||
|  | var embeddedTemplates embed.FS | ||||||
|  | 
 | ||||||
|  | // embed the "assets" directory
 | ||||||
|  | //
 | ||||||
|  | //go:embed assets/*
 | ||||||
|  | var embeddedAssets embed.FS | ||||||
|  | 
 | ||||||
| func init() { | func init() { | ||||||
| 
 | 
 | ||||||
| 	// command-line flags and env variables
 | 	// command-line flags and env variables
 | ||||||
|  | @ -119,19 +130,18 @@ 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
 | 	// strip the "templates/" prefix from the embedded directory so files can be read by their direct name (e.g.
 | ||||||
| 	tmplBox := rice.MustFindBox("templates") | 	// "base.html" instead of "templates/base.html")
 | ||||||
| 
 | 	tmplDir, _ := fs.Sub(fs.FS(embeddedTemplates), "templates") | ||||||
| 	// rice file server for assets. "assets" is the folder where the files come from.
 |  | ||||||
| 	assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox()) |  | ||||||
| 
 | 
 | ||||||
| 	// create the wireguard config on start, if it doesn't exist
 | 	// create the wireguard config on start, if it doesn't exist
 | ||||||
| 	initServerConfig(db, tmplBox) | 	initServerConfig(db, tmplDir) | ||||||
| 
 | 
 | ||||||
| 	// register routes
 | 	// register routes
 | ||||||
| 	app := router.New(tmplBox, extraData, util.SessionSecret) | 	app := router.New(tmplDir, extraData, util.SessionSecret) | ||||||
| 
 | 
 | ||||||
| 	app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession) | 	app.GET(util.BasePath, handler.WireGuardClients(db), handler.ValidSession) | ||||||
| 
 | 
 | ||||||
|  | @ -140,7 +150,12 @@ func main() { | ||||||
| 		app.POST(util.BasePath+"/login", handler.Login(db)) | 		app.POST(util.BasePath+"/login", handler.Login(db)) | ||||||
| 		app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) | 		app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) | ||||||
| 		app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) | 		app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) | ||||||
| 		app.POST(util.BasePath+"/profile", handler.UpdateProfile(db), handler.ValidSession) | 		app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
|  | 		app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession) | ||||||
|  | 		app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
|  | 		app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
|  | 		app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin) | ||||||
|  | 		app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var sendmail emailer.Emailer | 	var sendmail emailer.Emailer | ||||||
|  | @ -150,6 +165,7 @@ func main() { | ||||||
| 		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) | 		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	app.GET(util.BasePath+"/test-hash", handler.GetHashesChanges(db), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/about", handler.AboutPage()) | 	app.GET(util.BasePath+"/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()) | ||||||
|  | @ -159,29 +175,33 @@ 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+"/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) | ||||||
| 	app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) | 	app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) | 	app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) | ||||||
| 	app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplBox), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) | 	app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) | ||||||
| 	app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson) | 	app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) | 	app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 	app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) | 	app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson) | ||||||
| 
 | 
 | ||||||
| 	// servers other static files
 | 	// strip the "assets/" prefix from the embedded directory so files can be called directly without the "assets/"
 | ||||||
|  | 	// prefix
 | ||||||
|  | 	assetsDir, _ := fs.Sub(fs.FS(embeddedAssets), "assets") | ||||||
|  | 	assetHandler := http.FileServer(http.FS(assetsDir)) | ||||||
|  | 	// serves other static files
 | ||||||
| 	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) | 	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler))) | ||||||
| 
 | 
 | ||||||
| 	app.Logger.Fatal(app.Start(util.BindAddress)) | 	app.Logger.Fatal(app.Start(util.BindAddress)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func initServerConfig(db store.IStore, tmplBox *rice.Box) { | func initServerConfig(db store.IStore, tmplDir fs.FS) { | ||||||
| 	settings, err := db.GetGlobalSettings() | 	settings, err := db.GetGlobalSettings() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Cannot get global settings: ", err) | 		log.Fatalf("Cannot get global settings: ", err) | ||||||
|  | @ -202,8 +222,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(tmplDir, 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"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,11 +3,11 @@ package router | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"io/fs" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 
 | 
 | ||||||
| 	rice "github.com/GeertJohan/go.rice" |  | ||||||
| 	"github.com/gorilla/sessions" | 	"github.com/gorilla/sessions" | ||||||
| 	"github.com/labstack/echo-contrib/session" | 	"github.com/labstack/echo-contrib/session" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
|  | @ -66,54 +66,59 @@ func apiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| // New function
 | // New function
 | ||||||
| func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Echo { | func New(tmplDir fs.FS, extraData map[string]string, secret []byte) *echo.Echo { | ||||||
| 	e := echo.New() | 	e := echo.New() | ||||||
| 
 | 
 | ||||||
| 	store := sessions.NewCookieStore(secret) | 	store := sessions.NewCookieStore(secret) | ||||||
| 	e.Use(session.Middleware(store)) | 	e.Use(session.Middleware(store)) | ||||||
| 	e.Use(apiKeyMiddleware) | 	e.Use(apiKeyMiddleware) | ||||||
| 	// read html template file to string
 | 	// read html template file to string
 | ||||||
| 	tmplBaseString, err := tmplBox.String("base.html") | 	tmplBaseString, err := util.StringFromEmbedFile(tmplDir, "base.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplLoginString, err := tmplBox.String("login.html") | 	tmplLoginString, err := util.StringFromEmbedFile(tmplDir, "login.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplProfileString, err := tmplBox.String("profile.html") | 	tmplProfileString, err := util.StringFromEmbedFile(tmplDir, "profile.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplClientsString, err := tmplBox.String("clients.html") | 	tmplClientsString, err := util.StringFromEmbedFile(tmplDir, "clients.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplServerString, err := tmplBox.String("server.html") | 	tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplGlobalSettingsString, err := tmplBox.String("global_settings.html") | 	tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplStatusString, err := tmplBox.String("status.html") | 	tmplUsersSettingsString, err := util.StringFromEmbedFile(tmplDir, "users_settings.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmplWakeOnLanHostsString, err := tmplBox.String("wake_on_lan_hosts.html") | 	tmplStatusString, err := util.StringFromEmbedFile(tmplDir, "status.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	aboutPageString, err := tmplBox.String("about.html") | 	tmplWakeOnLanHostsString, err := util.StringFromEmbedFile(tmplDir, "wake_on_lan_hosts.html") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	aboutPageString, err := util.StringFromEmbedFile(tmplDir, "about.html") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | @ -128,14 +133,33 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec | ||||||
| 	templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) | 	templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) | ||||||
| 	templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) | 	templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) | ||||||
| 	templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) | 	templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) | ||||||
|  | 	templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString)) | ||||||
| 	templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) | 	templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) | ||||||
| 	templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString)) | 	templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString)) | ||||||
| 	templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString)) | 	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, | ||||||
|  |  | ||||||
|  | @ -42,7 +42,9 @@ func (o *JsonDB) Init() error { | ||||||
| 	var serverInterfacePath string = path.Join(serverPath, "interfaces.json") | 	var serverInterfacePath string = path.Join(serverPath, "interfaces.json") | ||||||
| 	var serverKeyPairPath string = path.Join(serverPath, "keypair.json") | 	var serverKeyPairPath string = path.Join(serverPath, "keypair.json") | ||||||
| 	var globalSettingPath string = path.Join(serverPath, "global_settings.json") | 	var globalSettingPath string = path.Join(serverPath, "global_settings.json") | ||||||
|  | 	var hashesPath string = path.Join(serverPath, "hashes.json") | ||||||
| 	var userPath string = path.Join(serverPath, "users.json") | 	var userPath string = path.Join(serverPath, "users.json") | ||||||
|  | 
 | ||||||
| 	// create directories if they do not exist
 | 	// create directories if they do not exist
 | ||||||
| 	if _, err := os.Stat(clientPath); os.IsNotExist(err) { | 	if _, err := os.Stat(clientPath); os.IsNotExist(err) { | ||||||
| 		os.MkdirAll(clientPath, os.ModePerm) | 		os.MkdirAll(clientPath, os.ModePerm) | ||||||
|  | @ -53,6 +55,9 @@ func (o *JsonDB) Init() error { | ||||||
| 	if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { | 	if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { | ||||||
| 		os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) | 		os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) | ||||||
| 	} | 	} | ||||||
|  | 	if _, err := os.Stat(userPath); os.IsNotExist(err) { | ||||||
|  | 		os.MkdirAll(userPath, os.ModePerm) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// server's interface
 | 	// server's interface
 | ||||||
| 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | 	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { | ||||||
|  | @ -96,16 +101,26 @@ func (o *JsonDB) Init() error { | ||||||
| 		globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS}) | 		globalSetting.DNSServers = util.LookupEnvOrStrings(util.DNSEnvVar, []string{util.DefaultDNS}) | ||||||
| 		globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) | 		globalSetting.MTU = util.LookupEnvOrInt(util.MTUEnvVar, util.DefaultMTU) | ||||||
| 		globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive) | 		globalSetting.PersistentKeepalive = util.LookupEnvOrInt(util.PersistentKeepaliveEnvVar, util.DefaultPersistentKeepalive) | ||||||
| 		globalSetting.ForwardMark = util.LookupEnvOrString(util.ForwardMarkEnvVar, util.DefaultForwardMark) | 		globalSetting.FirewallMark = util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark) | ||||||
| 		globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) | 		globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) | ||||||
| 		globalSetting.UpdatedAt = time.Now().UTC() | 		globalSetting.UpdatedAt = time.Now().UTC() | ||||||
| 		o.conn.Write("server", "global_settings", globalSetting) | 		o.conn.Write("server", "global_settings", globalSetting) | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|  | 	// hashes
 | ||||||
|  | 	if _, err := os.Stat(hashesPath); os.IsNotExist(err) { | ||||||
|  | 		clientServerHashes := new(model.ClientServerHashes) | ||||||
|  | 		clientServerHashes.Client = "none" | ||||||
|  | 		clientServerHashes.Server = "none" | ||||||
|  | 		o.conn.Write("server", "hashes", clientServerHashes) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// user info
 | 	// user info
 | ||||||
| 	if _, err := os.Stat(userPath); os.IsNotExist(err) { | 	results, err := o.conn.ReadAll("users") | ||||||
|  | 	if err != nil || len(results) < 1 { | ||||||
| 		user := new(model.User) | 		user := new(model.User) | ||||||
| 		user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) | 		user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) | ||||||
|  | 		user.Admin = util.DefaultIsAdmin | ||||||
| 		user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") | 		user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") | ||||||
| 		if user.PasswordHash == "" { | 		if user.PasswordHash == "" { | ||||||
| 			plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) | 			plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) | ||||||
|  | @ -115,7 +130,7 @@ func (o *JsonDB) Init() error { | ||||||
| 			} | 			} | ||||||
| 			user.PasswordHash = hash | 			user.PasswordHash = hash | ||||||
| 		} | 		} | ||||||
| 		o.conn.Write("server", "users", user) | 		o.conn.Write("users", user.Username, user) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
|  | @ -127,9 +142,44 @@ func (o *JsonDB) GetUser() (model.User, error) { | ||||||
| 	return user, o.conn.Read("server", "users", &user) | 	return user, o.conn.Read("server", "users", &user) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SaveUser func to user info to the database
 | // GetUsers func to get all users from the database
 | ||||||
|  | func (o *JsonDB) GetUsers() ([]model.User, error) { | ||||||
|  | 	var users []model.User | ||||||
|  | 	results, err := o.conn.ReadAll("users") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return users, err | ||||||
|  | 	} | ||||||
|  | 	for _, i := range results { | ||||||
|  | 		user := model.User{} | ||||||
|  | 
 | ||||||
|  | 		if err := json.Unmarshal([]byte(i), &user); err != nil { | ||||||
|  | 			return users, fmt.Errorf("cannot decode user json structure: %v", err) | ||||||
|  | 		} | ||||||
|  | 		users = append(users, user) | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 	return users, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUserByName func to get single user from the database
 | ||||||
|  | func (o *JsonDB) GetUserByName(username string) (model.User, error) { | ||||||
|  | 	user := model.User{} | ||||||
|  | 
 | ||||||
|  | 	if err := o.conn.Read("users", username, &user); err != nil { | ||||||
|  | 		return user, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return user, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SaveUser func to save user in the database
 | ||||||
| func (o *JsonDB) SaveUser(user model.User) error { | func (o *JsonDB) SaveUser(user model.User) error { | ||||||
| 	return o.conn.Write("server", "users", user) | 	return o.conn.Write("users", user.Username, user) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DeleteUser func to remove user from the database
 | ||||||
|  | func (o *JsonDB) DeleteUser(username string) error { | ||||||
|  | 	return o.conn.Delete("users", username) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetGlobalSettings func to query global settings from the database
 | // GetGlobalSettings func to query global settings from the database
 | ||||||
|  | @ -219,9 +269,6 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti | ||||||
| 		if !qrCodeSettings.IncludeMTU { | 		if !qrCodeSettings.IncludeMTU { | ||||||
| 			globalSettings.MTU = 0 | 			globalSettings.MTU = 0 | ||||||
| 		} | 		} | ||||||
| 		if !qrCodeSettings.IncludeFwMark { |  | ||||||
| 			globalSettings.ForwardMark = "" |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) | 		png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
|  | @ -255,3 +302,16 @@ func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error { | ||||||
| func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { | func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { | ||||||
| 	return o.conn.Write("server", "global_settings", globalSettings) | 	return o.conn.Write("server", "global_settings", globalSettings) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (o *JsonDB) GetPath() string { | ||||||
|  | 	return o.dbPath | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (o *JsonDB) GetHashes() (model.ClientServerHashes, error) { | ||||||
|  | 	hashes := model.ClientServerHashes{} | ||||||
|  | 	return hashes, o.conn.Read("server", "hashes", &hashes) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (o *JsonDB) SaveHashes(hashes model.ClientServerHashes) error { | ||||||
|  | 	return o.conn.Write("server", "hashes", hashes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -6,8 +6,10 @@ import ( | ||||||
| 
 | 
 | ||||||
| type IStore interface { | type IStore interface { | ||||||
| 	Init() error | 	Init() error | ||||||
| 	GetUser() (model.User, error) | 	GetUsers() ([]model.User, error) | ||||||
|  | 	GetUserByName(username string) (model.User, error) | ||||||
| 	SaveUser(user model.User) error | 	SaveUser(user model.User) error | ||||||
|  | 	DeleteUser(username string) error | ||||||
| 	GetGlobalSettings() (model.GlobalSetting, error) | 	GetGlobalSettings() (model.GlobalSetting, error) | ||||||
| 	GetServer() (model.Server, error) | 	GetServer() (model.Server, error) | ||||||
| 	GetClients(hasQRCode bool) ([]model.ClientData, error) | 	GetClients(hasQRCode bool) ([]model.ClientData, error) | ||||||
|  | @ -22,4 +24,7 @@ type IStore interface { | ||||||
| 	DeleteWakeOnHostLanHost(macAddress string) error | 	DeleteWakeOnHostLanHost(macAddress string) error | ||||||
| 	SaveWakeOnLanHost(host model.WakeOnLanHost) error | 	SaveWakeOnLanHost(host model.WakeOnLanHost) error | ||||||
| 	DeleteWakeOnHost(host model.WakeOnLanHost) error | 	DeleteWakeOnHost(host model.WakeOnLanHost) error | ||||||
|  | 	GetPath() string | ||||||
|  | 	SaveHashes(hashes model.ClientServerHashes) error | ||||||
|  | 	GetHashes() (model.ClientServerHashes, error) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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,15 @@ | ||||||
|                         </button> |                         </button> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <div class="form-group form-group-sm"> | ||||||
|  |                     <select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;"> | ||||||
|  |                         <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> | ||||||
|  |                 </div> | ||||||
|             </form> |             </form> | ||||||
| 
 | 
 | ||||||
|             <!-- Right navbar links --> |             <!-- Right navbar links --> | ||||||
|  | @ -63,7 +72,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 +99,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 +124,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 +134,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}}"> | ||||||
|  | @ -126,6 +145,16 @@ | ||||||
|                                 </p> |                                 </p> | ||||||
|                             </a> |                             </a> | ||||||
|                         </li> |                         </li> | ||||||
|  |                         <li class="nav-item"> | ||||||
|  |                             <a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}"> | ||||||
|  |                             <i class="nav-icon fas fa-cog"></i> | ||||||
|  |                             <p> | ||||||
|  |                                 Users Settings | ||||||
|  |                             </p> | ||||||
|  |                             </a> | ||||||
|  |                         </li> | ||||||
|  |                         {{end}} | ||||||
|  | 
 | ||||||
|                         <li class="nav-header">UTILITIES</li> |                         <li class="nav-header">UTILITIES</li> | ||||||
|                         <li class="nav-item"> |                         <li class="nav-item"> | ||||||
|                             <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> |                             <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> | ||||||
|  | @ -331,6 +360,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); | ||||||
|  |  | ||||||
|  | @ -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="password" 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}} | ||||||
|  | @ -25,12 +25,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" | ||||||
|  | @ -40,8 +41,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" | ||||||
|  |  | ||||||
							
								
								
									
										89
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										89
									
								
								util/util.go
								
								
								
								
							|  | @ -4,15 +4,20 @@ import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/ngoduykhanh/wireguard-ui/store" | ||||||
|  | 	"golang.org/x/mod/sumdb/dirhash" | ||||||
|  | 	"io" | ||||||
|  | 	"io/fs" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	rice "github.com/GeertJohan/go.rice" |  | ||||||
| 	externalip "github.com/glendc/go-external-ip" | 	externalip "github.com/glendc/go-external-ip" | ||||||
| 	"github.com/labstack/gommon/log" | 	"github.com/labstack/gommon/log" | ||||||
| 	"github.com/alikhanich/wireguard-ui/model" | 	"github.com/alikhanich/wireguard-ui/model" | ||||||
|  | @ -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(tmplDir fs.FS, 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
 | ||||||
|  | @ -393,7 +394,7 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl | ||||||
| 		tmplWireguardConf = string(fileContentBytes) | 		tmplWireguardConf = string(fileContentBytes) | ||||||
| 	} else { | 	} else { | ||||||
| 		// read default wg.conf template file to string
 | 		// read default wg.conf template file to string
 | ||||||
| 		fileContent, err := tmplBox.String("wg.conf") | 		fileContent, err := StringFromEmbedFile(tmplDir, "wg.conf") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | @ -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,68 @@ func LookupEnvOrStrings(key string, defaultVal []string) []string { | ||||||
| 	} | 	} | ||||||
| 	return defaultVal | 	return defaultVal | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func StringFromEmbedFile(embed fs.FS, filename string) (string, error) { | ||||||
|  | 	file, err := embed.Open(filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	content, err := io.ReadAll(file) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return string(content), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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