Browse Source

add: setup page

- add: in-memory database provider
- create a new account (signup)
- login with username and password
- first setup page to create an account
- PASSWORD_HASH was removed from environment and files was updated/removed due to that change
pull/1330/head
tetuaoro 8 months ago
parent
commit
21a38eb3c8
  1. 36
      How_to_generate_an_bcrypt_hash.md
  2. 104
      README.md
  3. 1
      docker-compose.dev.yml
  4. 1
      docker-compose.yml
  5. 82
      src/adapters/database/inmemory.ts
  6. 5
      src/composables/useDatabase.ts
  7. 17
      src/pages/login.vue
  8. 60
      src/pages/setup.vue
  9. 15
      src/plugins/database.server.ts
  10. 55
      src/ports/database.ts
  11. 15
      src/ports/types.ts
  12. 4
      src/ports/user/interface.ts
  13. 14
      src/ports/user/model.ts
  14. 22
      src/server/api/account/new.post.ts
  15. 5
      src/server/api/lang.get.ts
  16. 15
      src/server/api/session.post.ts
  17. 12
      src/server/middleware/session.ts
  18. 16
      src/server/middleware/setup.ts
  19. 16
      src/server/utils/Database.ts
  20. 3
      src/server/utils/config.ts
  21. 46
      src/server/utils/password.ts
  22. 5
      src/server/utils/types.ts
  23. 14
      src/stores/auth.ts
  24. 17
      src/utils/api.ts
  25. 49
      src/wgpw.js
  26. 5
      src/wgpw.sh

36
How_to_generate_an_bcrypt_hash.md

@ -1,36 +0,0 @@
# wg-password
`wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `wg-easy`, enhancing security by requiring passwords.
## Features
- Generate bcrypt password hashes.
- Easily integrate with `wg-easy` to enforce password requirements.
## Usage with Docker
To generate a bcrypt password hash using docker, run the following command :
```sh
docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
```
**Important** : make sure to enclose your password in **single quotes** when you run `docker run` command :
```bash
$ echo $2b$12$coPqCsPtcF <-- not correct
b2
$ echo "$2b$12$coPqCsPtcF" <-- not correct
b2
$ echo '$2b$12$coPqCsPtcF' <-- correct
$2b$12$coPqCsPtcF
```
**Important** : Please note: don't wrap the generated hash password in single quotes when you use `docker-compose.yml`. Instead, replace each `$` symbol with two `$$` symbols. For example:
``` yaml
- PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG
```
This hash is for the password 'foobar123', obtained using the command `docker run ghcr.io/wg-easy/wg-easy wgpw foobar123` and then inserted an additional `$` before each existing `$` symbal.

104
README.md

@ -14,37 +14,37 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
## Features ## Features
* All-in-one: WireGuard + Web UI. - All-in-one: WireGuard + Web UI.
* Easy installation, simple to use. - Easy installation, simple to use.
* List, create, edit, delete, enable & disable clients. - List, create, edit, delete, enable & disable clients.
* Show a client's QR code. - Show a client's QR code.
* Download a client's configuration file. - Download a client's configuration file.
* Statistics for which clients are connected. - Statistics for which clients are connected.
* Tx/Rx charts for each connected client. - Tx/Rx charts for each connected client.
* Gravatar support. - Gravatar support.
* Automatic Light / Dark Mode - Automatic Light / Dark Mode
* Multilanguage Support - Multilanguage Support
* Traffic Stats (default off) - Traffic Stats (default off)
* One Time Links (default off) - One Time Links (default off)
* Client Expiry (default off) - Client Expiry (default off)
* Prometheus metrics support - Prometheus metrics support
## Requirements ## Requirements
* A host with a kernel that supports WireGuard (all modern kernels). - A host with a kernel that supports WireGuard (all modern kernels).
* A host with Docker installed. - A host with Docker installed.
## Versions ## Versions
We provide more then 1 docker image to get, this will help you decide which one is best for you. <br> We provide more then 1 docker image to get, this will help you decide which one is best for you. <br>
For **stable** versions instead of nightly or development please read **README** from the **production** branch! For **stable** versions instead of nightly or development please read **README** from the **production** branch!
| tag | Branch | Example | Description | | tag | Branch | Example | Description |
| - | - | - | - | | ------------- | ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `latest` | production | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possbile get bug fixes quickly when needed, deployed against `production`. | | `latest` | production | `ghcr.io/wg-easy/wg-easy:latest` or `ghcr.io/wg-easy/wg-easy` | stable as possbile get bug fixes quickly when needed, deployed against `production`. |
| `13` | production | `ghcr.io/wg-easy/wg-easy:13` | same as latest, stick to a version tag. | | `13` | production | `ghcr.io/wg-easy/wg-easy:13` | same as latest, stick to a version tag. |
| `nightly` | master | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against `master`. | | `nightly` | master | `ghcr.io/wg-easy/wg-easy:nightly` | mostly unstable gets frequent package and code updates, deployed against `master`. |
| `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into `master`. | | `development` | pull requests | `ghcr.io/wg-easy/wg-easy:development` | used for development, testing code from PRs before landing into `master`. |
## Installation ## Installation
@ -69,7 +69,6 @@ To automatically install & run wg-easy, simply run:
--name=wg-easy \ --name=wg-easy \
-e LANG=de \ -e LANG=de \
-e WG_HOST=<🚨YOUR_SERVER_IP> \ -e WG_HOST=<🚨YOUR_SERVER_IP> \
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
-e PORT=51821 \ -e PORT=51821 \
-e WG_PORT=51820 \ -e WG_PORT=51820 \
-v ~/.wg-easy:/etc/wireguard \ -v ~/.wg-easy:/etc/wireguard \
@ -84,8 +83,6 @@ To automatically install & run wg-easy, simply run:
``` ```
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname. > 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
>
> 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI. See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash.
The Web UI will now be available on `http://0.0.0.0:51821`. The Web UI will now be available on `http://0.0.0.0:51821`.
@ -105,33 +102,32 @@ Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/W
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command. These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
| Env | Default | Example | Description | | Env | Default | Example | Description |
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------| | ----------------------------- | ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT` | `51821` | `6789` | TCP port for Web UI. | | `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. | | `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. | | `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. | | `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. | | `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. | | `WG_CONFIG_PORT` | `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) |
| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) | `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. | | `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
| `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. | | `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. | | `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. | | `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. | | `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. | | `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. | | `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. | | `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. | | `WG_ENABLE_EXPIRES_TIME` | `false` | `true` | Enable expire time for clients |
| `WG_ENABLE_EXPIRES_TIME` | `false` | `true` | Enable expire time for clients | | `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). |
| `LANG` | `en` | `de` | Web UI language (Supports: en, ua, ru, tr, no, pl, fr, de, ca, es, ko, vi, nl, is, pt, chs, cht, it, th, hi). | | `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI |
| `UI_TRAFFIC_STATS` | `false` | `true` | Enable detailed RX / TX client stats in Web UI | | `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart |
| `UI_CHART_TYPE` | `0` | `1` | UI_CHART_TYPE=0 # Charts disabled, UI_CHART_TYPE=1 # Line chart, UI_CHART_TYPE=2 # Area chart, UI_CHART_TYPE=3 # Bar chart | | `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) |
| `WG_ENABLE_ONE_TIME_LINKS` | `false` | `true` | Enable display and generation of short one time download links (expire after 5 minutes) | | `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. |
| `MAX_AGE` | `0` | `1440` | The maximum age of Web UI sessions in minutes. `0` means that the session will exist until the browser is closed. | | `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name |
| `UI_ENABLE_SORT_CLIENTS` | `false` | `true` | Enable UI sort clients by name | | `ENABLE_PROMETHEUS_METRICS` | `false` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json` |
| `ENABLE_PROMETHEUS_METRICS` | `false` | `true` | Enable Prometheus metrics `http://0.0.0.0:51821/metrics` and `http://0.0.0.0:51821/metrics/json`| | `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
| `PROMETHEUS_METRICS_PASSWORD` | - | `$2y$05$Ci...` | If set, Basic Auth is required when requesting metrics. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
> If you change `WG_PORT`, make sure to also change the exposed port. > If you change `WG_PORT`, make sure to also change the exposed port.
@ -157,7 +153,7 @@ was pulled.
## Common Use Cases ## Common Use Cases
* [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole) - [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole)
* [Using WireGuard-Easy with nginx/SSL](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-nginx-SSL) - [Using WireGuard-Easy with nginx/SSL](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-nginx-SSL)
For less common or specific edge-case scenarios, please refer to the detailed information provided in the [Wiki](https://github.com/wg-easy/wg-easy/wiki). For less common or specific edge-case scenarios, please refer to the detailed information provided in the [Wiki](https://github.com/wg-easy/wg-easy/wiki).

1
docker-compose.dev.yml

@ -15,7 +15,6 @@ services:
- NET_ADMIN - NET_ADMIN
- SYS_MODULE - SYS_MODULE
environment: environment:
- PASSWORD_HASH=$$2y$$10$$Vhi2tF1i2c/ReW3LdLOru.z7LDITqBgb2wrSVw6sa.KEtbpYgSAf2 # foobar123
- WG_HOST=192.168.1.233 - WG_HOST=192.168.1.233
# folders should be generated inside container # folders should be generated inside container

1
docker-compose.yml

@ -12,7 +12,6 @@ services:
- WG_HOST=raspberrypi.local - WG_HOST=raspberrypi.local
# Optional: # Optional:
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
# - PORT=51821 # - PORT=51821
# - WG_PORT=51820 # - WG_PORT=51820
# - WG_CONFIG_PORT=92820 # - WG_CONFIG_PORT=92820

82
src/adapters/database/inmemory.ts

@ -1,14 +1,18 @@
import debug from 'debug'; import debug from 'debug';
import packageJson from '@/package.json'; import packageJson from '@/package.json';
import DatabaseProvider from '~/ports/database'; import DatabaseProvider, { DatabaseError } from '~/ports/database';
import { ChartType, Lang } from '~/ports/types'; import { ChartType, Lang } from '~/ports/types';
import { ROLE } from '~/ports/user/model'; import { ROLE } from '~/ports/user/model';
import type { SessionConfig } from 'h3'; import type { SessionConfig } from 'h3';
import type { System } from '~/ports/system/model'; import type { System } from '~/ports/system/model';
import type { User } from '~/ports/user/model'; import type { User } from '~/ports/user/model';
import type { Identity } from '~/ports/types'; import type { Identity, String } from '~/ports/types';
import {
hashPasswordWithBcrypt,
isPasswordStrong,
} from '~/server/utils/password';
const INMDP_DEBUG = debug('InMemoryDP'); const INMDP_DEBUG = debug('InMemoryDP');
@ -19,10 +23,10 @@ type InMemoryData = {
}; };
// In-Memory Database Provider // In-Memory Database Provider
export class InMemory extends DatabaseProvider { export default class InMemory extends DatabaseProvider {
protected data: InMemoryData = { users: [] }; protected data: InMemoryData = { users: [] };
override async connect() { async connect() {
INMDP_DEBUG('Connection...'); INMDP_DEBUG('Connection...');
const system: System = { const system: System = {
release: packageJson.release.version, release: packageJson.release.version,
@ -73,11 +77,11 @@ export class InMemory extends DatabaseProvider {
INMDP_DEBUG('Connection done'); INMDP_DEBUG('Connection done');
} }
override async disconnect() { async disconnect() {
this.data = { users: [] }; this.data = { users: [] };
} }
override async getSystem() { async getSystem() {
INMDP_DEBUG('Get System'); INMDP_DEBUG('Get System');
return this.data.system; return this.data.system;
} }
@ -87,53 +91,69 @@ export class InMemory extends DatabaseProvider {
this.data.system = system; this.data.system = system;
} }
override async getLang() { async getLang() {
return this.data.system?.lang || Lang.EN; return this.data.system?.lang || Lang.EN;
} }
override async getUsers() { async getUsers() {
return this.data.users; return this.data.users;
} }
override async getUser(id: Identity<User>) { async getUser(id: Identity<User>) {
INMDP_DEBUG('Get User'); INMDP_DEBUG('Get User');
if (typeof id === 'string') { if (typeof id === 'string' || typeof id === 'number') {
return this.data.users.find((user) => user.id === id); return this.data.users.find((user) => user.id === id);
} }
return this.data.users.find((user) => user.id === id.id); return this.data.users.find((user) => user.id === id.id);
} }
override async saveUser(user: User) { async newUserWithPassword(username: String, password: String) {
INMDP_DEBUG('New User');
if (username.length < 8) {
throw new DatabaseError(DatabaseError.ERROR_USERNAME_LEN);
}
if (!isPasswordStrong(password)) {
throw new DatabaseError(DatabaseError.ERROR_PASSWORD_REQ);
}
const isUserExist = this.data.users.find(
(user) => user.username === username
);
if (isUserExist) {
throw new DatabaseError(DatabaseError.ERROR_USER_EXIST);
}
const now = new Date();
const isUserEmpty = this.data.users.length == 0;
const newUser: User = {
id: `${this.data.users.length + 1}`,
password: hashPasswordWithBcrypt(password),
username,
role: isUserEmpty ? ROLE.ADMIN : ROLE.CLIENT,
enabled: true,
createdAt: now,
updatedAt: now,
};
this.data.users.push(newUser);
}
async saveUser(user: User) {
let _user = await this.getUser(user); let _user = await this.getUser(user);
if (_user) { if (_user) {
INMDP_DEBUG('Update User'); INMDP_DEBUG('Update User');
_user = user; _user = user;
} else {
INMDP_DEBUG('New User');
if (this.data.users.length == 0) {
// first user is admin
user.role = ROLE.ADMIN;
}
this.data.users.push(user);
} }
} }
override async deleteUser(id: Identity<User>) { async deleteUser(id: Identity<User>) {
const _id = typeof id === 'string' ? id : id.id; INMDP_DEBUG('Delete User');
const _id = typeof id === 'string' || typeof id === 'number' ? id : id.id;
const idx = this.data.users.findIndex((user) => user.id == _id); const idx = this.data.users.findIndex((user) => user.id == _id);
if (idx !== -1) { if (idx !== -1) {
this.data.users.splice(idx, 1); this.data.users.splice(idx, 1);
} }
} }
} }
export default function initInMemoryProvider() {
const provider = new InMemory();
provider.connect().catch((err) => {
console.error(err);
process.exit(1);
});
return provider;
}

5
src/composables/useDatabase.ts

@ -1,5 +0,0 @@
import type { InMemory } from '~/adapters/database/inmemory';
export default (): InMemory => {
return useNuxtApp().$database;
};

17
src/pages/login.vue

@ -18,6 +18,15 @@
<IconsAvatar class="w-10 h-10 m-5 text-white dark:text-white" /> <IconsAvatar class="w-10 h-10 m-5 text-white dark:text-white" />
</div> </div>
<input
v-model="username"
type="text"
name="username"
:placeholder="$t('username')"
autocomplete="username"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none"
/>
<input <input
v-model="password" v-model="password"
type="password" type="password"
@ -77,6 +86,7 @@
<script setup lang="ts"> <script setup lang="ts">
const authenticating = ref(false); const authenticating = ref(false);
const remember = ref(false); const remember = ref(false);
const username = ref<string>();
const password = ref<null | string>(null); const password = ref<null | string>(null);
const authStore = useAuthStore(); const authStore = useAuthStore();
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
@ -84,12 +94,17 @@ const globalStore = useGlobalStore();
async function login(e: Event) { async function login(e: Event) {
e.preventDefault(); e.preventDefault();
if (!username.value) return;
if (!password.value) return; if (!password.value) return;
if (authenticating.value) return; if (authenticating.value) return;
authenticating.value = true; authenticating.value = true;
try { try {
const res = await authStore.login(password.value, remember.value); const res = await authStore.login(
username.value,
password.value,
remember.value
);
if (res) { if (res) {
await navigateTo('/'); await navigateTo('/');
} }

60
src/pages/setup.vue

@ -0,0 +1,60 @@
<template>
<main>
<div>
<h1>Welcome to your first setup of wg-easy !</h1>
<p>Please first enter an admin username and a strong password.</p>
<form @submit="newAccount">
<div>
<label for="username">Username</label>
<input
id="username"
v-model="username"
type="text"
name="username"
autocomplete="username"
/>
</div>
<div>
<label for="password">New Password</label>
<input
id="password"
v-model="password"
type="password"
name="password"
autocomplete="new-password"
/>
</div>
<div>
<label for="accept">I accept the condition.</label>
<input id="accept" type="checkbox" name="accept" />
</div>
<button type="submit">Save</button>
</form>
</div>
</main>
</template>
<script setup lang="ts">
const username = ref<string>();
const password = ref<string>();
const authStore = useAuthStore();
async function newAccount(e: Event) {
e.preventDefault();
if (!username.value) return;
if (!password.value) return;
try {
const res = await authStore.signup(username.value, password.value);
if (res) {
navigateTo('/login');
}
} catch (error) {
if (error instanceof Error) {
// TODO: replace alert with actual ui error message
alert(error.message || error.toString());
}
}
}
</script>

15
src/plugins/database.server.ts

@ -1,15 +0,0 @@
/**
* Changing the Database Provider
* This design allows for easy swapping of different database implementations.
*
*/
import initInMemoryProvider from '~/adapters/database/inmemory';
export default defineNuxtPlugin(() => {
return {
provide: {
database: initInMemoryProvider(),
},
};
});

55
src/ports/database.ts

@ -1,12 +1,15 @@
import type SystemRepository from './system/interface'; import type SystemRepository from './system/interface';
import type UserRepository from './user/interface'; import type UserRepository from './user/interface';
import type { Identity, Undefined, Lang } from './types'; import type { Identity, Undefined, Lang, String } from './types';
import type { User } from './user/model'; import type { User } from './user/model';
import type { System } from './system/model'; import type { System } from './system/model';
/** /**
* Abstract class for database operations. * Abstract class for database operations.
* Provides methods to connect, disconnect, and interact with system and user data. * Provides methods to connect, disconnect, and interact with system and user data.
*
* *Note : Throw with `DatabaseError` to ensure API handling errors.*
*
*/ */
export default abstract class DatabaseProvider export default abstract class DatabaseProvider
implements SystemRepository, UserRepository implements SystemRepository, UserRepository
@ -14,33 +17,37 @@ export default abstract class DatabaseProvider
/** /**
* Connects to the database. * Connects to the database.
*/ */
connect(): Promise<void> { abstract connect(): Promise<void>;
throw new Error('Method not implemented.');
}
/** /**
* Disconnects from the database. * Disconnects from the database.
*/ */
disconnect(): Promise<void> { abstract disconnect(): Promise<void>;
throw new Error('Method not implemented.');
}
getSystem(): Promise<System | Undefined> { abstract getSystem(): Promise<System | Undefined>;
throw new Error('Method not implemented.'); abstract getLang(): Promise<Lang>;
}
getLang(): Promise<Lang> {
throw new Error('Method not implemented.');
}
getUsers(): Promise<Array<User>> { abstract getUsers(): Promise<Array<User>>;
throw new Error('Method not implemented.'); abstract getUser(_id: Identity<User>): Promise<User | Undefined>;
} abstract newUserWithPassword(
getUser(_id: Identity<User>): Promise<User | Undefined> { _username: String,
throw new Error('Method not implemented.'); _password: String
} ): Promise<void>;
saveUser(_user: User): Promise<void> { abstract saveUser(_user: User): Promise<void>;
throw new Error('Method not implemented.'); abstract deleteUser(_id: Identity<User>): Promise<void>;
} }
deleteUser(_id: Identity<User>): Promise<void> {
throw new Error('Method not implemented.'); export class DatabaseError extends Error {
static readonly ERROR_PASSWORD_REQ =
'Password does not meet the strength requirements. It must be at least 12 characters long, with at least one uppercase letter, one lowercase letter, one number, and one special character.';
static readonly ERROR_USER_EXIST = 'User already exists.';
static readonly ERROR_DATABASE_CONNECTION =
'Failed to connect to the database.';
static readonly ERROR_USERNAME_LEN =
'Username must be longer than 8 characters.';
constructor(message: string) {
super(message);
this.name = 'DatabaseError';
} }
} }

15
src/ports/types.ts

@ -6,11 +6,12 @@ export enum Lang {
FR = 'fr', FR = 'fr',
} }
export type String = string; export type String = string;
export type ID = String; export type Number = number;
export type ID = String | Number;
export type Boolean = boolean; export type Boolean = boolean;
export type Version = String; export type Version = String;
export type SessionTimeOut = number; export type SessionTimeOut = Number;
export type Port = number; export type Port = Number;
export type Address = String; export type Address = String;
export type HashPassword = String; export type HashPassword = String;
export type Command = String; export type Command = String;
@ -27,8 +28,8 @@ export type WGInterface = {
address: Address; address: Address;
}; };
export type WGConfig = { export type WGConfig = {
mtu: number; mtu: Number;
persistentKeepalive: number; persistentKeepalive: Number;
rangeAddress: Address; rangeAddress: Address;
defaultDns: Array<Address>; defaultDns: Array<Address>;
allowedIps: Array<Address>; allowedIps: Array<Address>;
@ -40,11 +41,11 @@ export enum ChartType {
Bar = 3, Bar = 3,
} }
export type TrafficStats = { export type TrafficStats = {
enabled: boolean; enabled: Boolean;
type: ChartType; type: ChartType;
}; };
export type Prometheus = { export type Prometheus = {
enabled: boolean; enabled: Boolean;
password?: HashPassword | Undefined; password?: HashPassword | Undefined;
}; };
/** /**

4
src/ports/user/interface.ts

@ -1,4 +1,4 @@
import type { Identity, Undefined } from '../types'; import type { Identity, String, Undefined } from '../types';
import type { User } from './model'; import type { User } from './model';
/** /**
@ -20,6 +20,8 @@ export default interface UserRepository {
*/ */
getUser(id: Identity<User>): Promise<User | Undefined>; getUser(id: Identity<User>): Promise<User | Undefined>;
newUserWithPassword(username: String, password: String): Promise<void>;
/** /**
* Creates or updates a user in the database. * Creates or updates a user in the database.
* @param {User} user - The user to be saved. * @param {User} user - The user to be saved.

14
src/ports/user/model.ts

@ -1,4 +1,4 @@
import type { Address, ID, Key, HashPassword, String } from '../types'; import type { Address, ID, Key, HashPassword, String, Boolean } from '../types';
export enum ROLE { export enum ROLE {
/* Full permissions to any resources (app, database...) */ /* Full permissions to any resources (app, database...) */
@ -17,12 +17,12 @@ export type User = {
role: ROLE; role: ROLE;
username: String; username: String;
password: HashPassword; password: HashPassword;
name: String; name?: String;
address: Address; address?: Address;
privateKey: Key; privateKey?: Key;
publicKey: Key; publicKey?: Key;
preSharedKey: String; preSharedKey?: String;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
enabled: boolean; enabled: Boolean;
}; };

22
src/server/api/account/new.post.ts

@ -0,0 +1,22 @@
import { DatabaseError } from '~/ports/database';
type Request = { username: string; password: string };
export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json');
try {
// TODO use zod
const { username, password } = await readBody<Request>(event);
await Database.newUserWithPassword(username, password);
return { success: true };
} catch (error) {
if (error instanceof DatabaseError) {
throw createError({
statusCode: 400,
statusMessage: error.message,
});
} else {
throw createError('Something happened !');
}
}
});

5
src/server/api/lang.get.ts

@ -1,7 +1,4 @@
import useDatabase from '~/composables/useDatabase';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json'); setHeader(event, 'Content-Type', 'application/json');
const db = useDatabase(); return await Database.getLang();
return db.getLang();
}); });

15
src/server/api/session.post.ts

@ -1,7 +1,7 @@
import type { SessionConfig } from 'h3'; import type { SessionConfig } from 'h3';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { password, remember } = await readValidatedBody( const { username, password, remember } = await readValidatedBody(
event, event,
validateZod(passwordType) validateZod(passwordType)
); );
@ -14,7 +14,17 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Invalid state', statusMessage: 'Invalid state',
}); });
} }
if (!isPasswordValid(password, PASSWORD_HASH)) {
const users = await Database.getUsers();
const user = users.find((user) => user.username == username);
if (!user)
throw createError({
statusCode: 400,
statusMessage: 'User with username does not exist',
});
const userHashPassword = user.password;
if (!isPasswordValid(password, userHashPassword)) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: 'Incorrect Password', statusMessage: 'Incorrect Password',
@ -35,6 +45,7 @@ export default defineEventHandler(async (event) => {
const data = await session.update({ const data = await session.update({
authenticated: true, authenticated: true,
userId: user.id,
}); });
SERVER_DEBUG(`New Session: ${data.id}`); SERVER_DEBUG(`New Session: ${data.id}`);

12
src/server/middleware/session.ts

@ -3,6 +3,7 @@ export default defineEventHandler(async (event) => {
if ( if (
!REQUIRES_PASSWORD || !REQUIRES_PASSWORD ||
!url.pathname.startsWith('/api/') || !url.pathname.startsWith('/api/') ||
url.pathname === '/api/account/new' ||
url.pathname === '/api/session' || url.pathname === '/api/session' ||
url.pathname === '/api/lang' || url.pathname === '/api/lang' ||
url.pathname === '/api/release' || url.pathname === '/api/release' ||
@ -18,7 +19,16 @@ export default defineEventHandler(async (event) => {
const authorization = getHeader(event, 'Authorization'); const authorization = getHeader(event, 'Authorization');
if (url.pathname.startsWith('/api/') && authorization) { if (url.pathname.startsWith('/api/') && authorization) {
if (isPasswordValid(authorization, PASSWORD_HASH)) { const users = await Database.getUsers();
const user = users.find((user) => user.id == session.data.userId);
if (!user)
throw createError({
statusCode: 401,
statusMessage: 'Session failed',
});
const userHashPassword = user.password;
if (isPasswordValid(authorization, userHashPassword)) {
return; return;
} }
throw createError({ throw createError({

16
src/server/middleware/setup.ts

@ -0,0 +1,16 @@
/* First setup of wg-easy app */
export default defineEventHandler(async (event) => {
const url = getRequestURL(event);
if (
url.pathname.startsWith('/setup') ||
url.pathname === '/api/account/new'
) {
return;
}
const users = await Database.getUsers();
if (users.length === 0) {
return sendRedirect(event, '/setup', 302);
}
});

16
src/server/utils/Database.ts

@ -0,0 +1,16 @@
/**
* Changing the Database Provider
* This design allows for easy swapping of different database implementations.
*
*/
import InMemory from '~/adapters/database/inmemory';
const provider = new InMemory();
provider.connect().catch((err) => {
console.error(err);
process.exit(1);
});
export default provider;

3
src/server/utils/config.ts

@ -7,7 +7,6 @@ const version = packageJSON.release.version;
export const RELEASE = version; export const RELEASE = version;
export const PORT = process.env.PORT || '51821'; export const PORT = process.env.PORT || '51821';
export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
export const PASSWORD_HASH = process.env.PASSWORD_HASH;
export const MAX_AGE = process.env.MAX_AGE export const MAX_AGE = process.env.MAX_AGE
? parseInt(process.env.MAX_AGE, 10) * 60 ? parseInt(process.env.MAX_AGE, 10) * 60
: 0; : 0;
@ -64,7 +63,7 @@ export const ENABLE_PROMETHEUS_METRICS =
export const PROMETHEUS_METRICS_PASSWORD = export const PROMETHEUS_METRICS_PASSWORD =
process.env.PROMETHEUS_METRICS_PASSWORD; process.env.PROMETHEUS_METRICS_PASSWORD;
export const REQUIRES_PASSWORD = !!PASSWORD_HASH; export const REQUIRES_PASSWORD = true;
export const REQUIRES_PROMETHEUS_PASSWORD = !!PROMETHEUS_METRICS_PASSWORD; export const REQUIRES_PROMETHEUS_PASSWORD = !!PROMETHEUS_METRICS_PASSWORD;
export const SESSION_CONFIG = { export const SESSION_CONFIG = {

46
src/server/utils/password.ts

@ -1,17 +1,47 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
/** /**
* Checks if `password` matches the PASSWORD_HASH. * Checks if `password` matches the user password.
*
* If environment variable is not set, the password is always invalid.
* *
* @param {string} password String to test * @param {string} password String to test
* @returns {boolean} true if matching environment, otherwise false * @returns {boolean} `true` if matching user password, otherwise `false`
*/
export function isPasswordValid(password: string, hash: string): boolean {
return bcrypt.compareSync(password, hash);
}
/**
* Checks if a password is strong based on following criteria :
*
* - minimum length of 12 characters
* - contains at least one uppercase letter
* - contains at least one lowercase letter
* - contains at least one number
* - contains at least one special character (e.g., !@#$%^&*(),.?":{}|<>).
*
* @param {string} password - The password to validate
* @returns {boolean} `true` if the password is strong, otherwise `false`
*/ */
export function isPasswordValid(password: string, hash?: string): boolean {
if (hash) { export function isPasswordStrong(password: string): boolean {
return bcrypt.compareSync(password, hash); if (password.length < 12) {
return false;
} }
return false; const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
}
/**
* Hashes a password using the bcrypt algorithm.
*
* @param {string} password - The plaintext password to hash
* @returns {string} The bcrypt hash of the password
*/
export function hashPasswordWithBcrypt(password: string): string {
return bcrypt.hashSync(password, 12);
} }

5
src/server/utils/types.ts

@ -26,6 +26,10 @@ const file = z
.string({ message: 'File must be a valid string' }) .string({ message: 'File must be a valid string' })
.pipe(safeStringRefine); .pipe(safeStringRefine);
const username = z
.string({ message: 'Username must be a valid string' })
.pipe(safeStringRefine);
const password = z const password = z
.string({ message: 'Password must be a valid string' }) .string({ message: 'Password must be a valid string' })
.pipe(safeStringRefine); .pipe(safeStringRefine);
@ -83,6 +87,7 @@ export const fileType = z.object(
export const passwordType = z.object( export const passwordType = z.object(
{ {
username: username,
password: password, password: password,
remember: remember, remember: remember,
}, },

14
src/stores/auth.ts

@ -4,8 +4,16 @@ export const useAuthStore = defineStore('Auth', () => {
/** /**
* @throws if unsuccessful * @throws if unsuccessful
*/ */
async function login(password: string, remember: boolean) { async function signup(username: string, password: string) {
const response = await api.createSession({ password, remember }); const response = await api.newAccount({ username, password });
return response.success;
}
/**
* @throws if unsuccessful
*/
async function login(username: string, password: string, remember: boolean) {
const response = await api.createSession({ username, password, remember });
requiresPassword.value = response.requiresPassword; requiresPassword.value = response.requiresPassword;
return true as const; return true as const;
} }
@ -26,5 +34,5 @@ export const useAuthStore = defineStore('Auth', () => {
requiresPassword.value = session.requiresPassword; requiresPassword.value = session.requiresPassword;
} }
return { requiresPassword, login, logout, update }; return { requiresPassword, login, logout, update, signup };
}); });

17
src/utils/api.ts

@ -49,15 +49,17 @@ class API {
} }
async createSession({ async createSession({
username,
password, password,
remember, remember,
}: { }: {
username: string;
password: string | null; password: string | null;
remember: boolean; remember: boolean;
}) { }) {
return $fetch('/api/session', { return $fetch('/api/session', {
method: 'post', method: 'post',
body: { password, remember }, body: { username, password, remember },
}); });
} }
@ -161,6 +163,19 @@ class API {
method: 'get', method: 'get',
}); });
} }
async newAccount({
username,
password,
}: {
username: string;
password: string;
}) {
return $fetch('/api/account/new', {
method: 'post',
body: { username, password },
});
}
} }
type WGClientReturn = Awaited< type WGClientReturn = Awaited<

49
src/wgpw.js

@ -1,49 +0,0 @@
// Import needed libraries
import bcrypt from 'bcryptjs';
// Function to generate hash
const generateHash = async (password) => {
try {
const salt = await bcrypt.genSalt(12);
const hash = await bcrypt.hash(password, salt);
console.log(`PASSWORD_HASH='${hash}'`);
} catch (error) {
throw new Error(`Failed to generate hash : ${error}`);
}
};
// Function to compare password with hash
const comparePassword = async (password, hash) => {
try {
const match = await bcrypt.compare(password, hash);
if (match) {
console.log('Password matches the hash !');
} else {
console.log('Password does not match the hash.');
}
} catch (error) {
throw new Error(`Failed to compare password and hash : ${error}`);
}
};
(async () => {
try {
// Retrieve command line arguments
const args = process.argv.slice(2); // Ignore the first two arguments
if (args.length > 2) {
throw new Error('Usage : wgpw YOUR_PASSWORD [HASH]');
}
const [password, hash] = args;
if (password && hash) {
await comparePassword(password, hash);
} else if (password) {
await generateHash(password);
}
} catch (error) {
console.error(error);
process.exit(1);
}
})();

5
src/wgpw.sh

@ -1,5 +0,0 @@
#!/bin/sh
# This script is intended to be run only inside a docker container, not on the development host machine
set -e
# proxy command
node /app/wgpw.mjs "$@"
Loading…
Cancel
Save