Browse Source

feat: remove required password from environment

pull/1228/head
tetuaoro 2 years ago
parent
commit
75f05e1f22
  1. 1
      .gitignore
  2. 6
      Dockerfile
  3. 3
      README.md
  4. 1
      docker-compose.yml
  5. 1
      src/config.js
  6. 52
      src/lib/Server.js
  7. 25
      src/lib/database/Interface.js
  8. 101
      src/lib/database/SQLite.js
  9. 1353
      src/package-lock.json
  10. 4
      src/package.json
  11. 11
      src/server.js
  12. 13
      src/services/Database.js
  13. 80
      src/templates/Dashboard.vue
  14. 54
      src/wgpw.mjs
  15. 5
      src/wgpw.sh
  16. 54
      src/www/css/app.css
  17. 128
      src/www/index.html
  18. 12
      src/www/js/api.js
  19. 7
      src/www/js/app.js
  20. 7
      src/www/js/i18n.js
  21. 2
      src/www/js/vendor/vue-dashboard.umd.min.js
  22. 1
      src/www/js/vendor/vue-dashboard.umd.min.js.map

1
.gitignore

@ -4,3 +4,4 @@
/src/node_modules /src/node_modules
.DS_Store .DS_Store
*.swp *.swp
/.vscode

6
Dockerfile

@ -26,10 +26,6 @@ COPY --from=build_node_modules /app /app
# than what runs inside of docker. # than what runs inside of docker.
COPY --from=build_node_modules /node_modules /node_modules COPY --from=build_node_modules /node_modules /node_modules
# Copy the needed wg-password scripts
COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw
RUN chmod +x /bin/wgpw
# Install Linux packages # Install Linux packages
RUN apk add --no-cache \ RUN apk add --no-cache \
dpkg \ dpkg \
@ -42,7 +38,7 @@ RUN apk add --no-cache \
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save
# Set Environment # Set Environment
ENV DEBUG=Server,WireGuard ENV DEBUG=Server,WireGuard,Database
# Run Web UI # Run Web UI
WORKDIR /app WORKDIR /app

3
README.md

@ -65,7 +65,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 \
@ -81,7 +80,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`.
@ -103,7 +101,6 @@ These options can be configured by setting environment variables using `-e KEY="
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------| | - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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. |

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

1
src/config.js

@ -5,7 +5,6 @@ const { release: { version } } = require('./package.json');
module.exports.RELEASE = version; module.exports.RELEASE = version;
module.exports.PORT = process.env.PORT || '51821'; module.exports.PORT = process.env.PORT || '51821';
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH;
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/'; module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0'; module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0';
module.exports.WG_HOST = process.env.WG_HOST; module.exports.WG_HOST = process.env.WG_HOST;

52
src/lib/Server.js

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const bcrypt = require('bcryptjs');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const { createServer } = require('node:http'); const { createServer } = require('node:http');
const { stat, readFile } = require('node:fs/promises'); const { stat, readFile } = require('node:fs/promises');
@ -23,37 +22,33 @@ const {
} = require('h3'); } = require('h3');
const WireGuard = require('../services/WireGuard'); const WireGuard = require('../services/WireGuard');
const Database = require('../services/Database');
const { const {
PORT, PORT,
WEBUI_HOST, WEBUI_HOST,
RELEASE, RELEASE,
PASSWORD_HASH,
LANG, LANG,
UI_TRAFFIC_STATS, UI_TRAFFIC_STATS,
UI_CHART_TYPE, UI_CHART_TYPE,
} = require('../config'); } = require('../config');
const requiresPassword = !!PASSWORD_HASH; const requiresPassword = true;
/** /**
* Checks if `password` matches the PASSWORD_HASH. * Checks if `username` & `password` credentials are valid.
*
* If environment variable is not set, the password is always invalid.
* *
* @param {string} username String to test
* @param {string} password String to test * @param {string} password String to test
* @returns {boolean} true if matching environment, otherwise false * @returns {Promise<boolean>} true if matching, otherwise false
*
*/ */
const isPasswordValid = (password) => { const isPasswordValid = async (username, password) => {
if (typeof password !== 'string') { if (typeof username !== 'string' || typeof password !== 'string') {
return false; return false;
} }
if (PASSWORD_HASH) { return Database.comparePassword(username, password);
return bcrypt.compareSync(password, PASSWORD_HASH);
}
return false;
}; };
module.exports = class Server { module.exports = class Server {
@ -104,7 +99,7 @@ module.exports = class Server {
}; };
})) }))
.post('/api/session', defineEventHandler(async (event) => { .post('/api/session', defineEventHandler(async (event) => {
const { password } = await readBody(event); const { username, password } = await readBody(event);
if (!requiresPassword) { if (!requiresPassword) {
// if no password is required, the API should never be called. // if no password is required, the API should never be called.
@ -115,7 +110,8 @@ module.exports = class Server {
}); });
} }
if (!isPasswordValid(password)) { const isMatch = await isPasswordValid(username, password);
if (!isMatch) {
throw createError({ throw createError({
status: 401, status: 401,
message: 'Incorrect Password', message: 'Incorrect Password',
@ -142,13 +138,8 @@ module.exports = class Server {
} }
if (req.url.startsWith('/api/') && req.headers['authorization']) { if (req.url.startsWith('/api/') && req.headers['authorization']) {
if (isPasswordValid(req.headers['authorization'])) {
return next(); return next();
} }
return res.status(401).json({
error: 'Incorrect Password',
});
}
return res.status(401).json({ return res.status(401).json({
error: 'Not Logged In', error: 'Not Logged In',
@ -258,11 +249,11 @@ module.exports = class Server {
}); });
}; };
// backup_restore // backup & restore
const router3 = createRouter(); const routerBackupRestore = createRouter();
app.use(router3); app.use(routerBackupRestore);
router3 routerBackupRestore
.get('/api/wireguard/backup', defineEventHandler(async (event) => { .get('/api/wireguard/backup', defineEventHandler(async (event) => {
const config = await WireGuard.backupConfiguration(); const config = await WireGuard.backupConfiguration();
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"'); setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
@ -275,6 +266,17 @@ module.exports = class Server {
return { success: true }; return { success: true };
})); }));
// admin dashboard
const routerDashboard = createRouter();
app.use(routerDashboard);
routerDashboard
.put('/api/dashboard/setting/password', defineEventHandler(async (event) => {
const { username, oldPassword, newPassword } = await readBody(event);
await Database.updatePassword(username, oldPassword, newPassword);
return { success: true };
}));
// Static assets // Static assets
const publicDir = '/app/www'; const publicDir = '/app/www';
app.use( app.use(

25
src/lib/database/Interface.js

@ -0,0 +1,25 @@
'use strict';
const ServerError = require('../ServerError');
module.exports = class DatabaseInterface {
constructor() {
if (this.constructor === DatabaseInterface) {
throw new ServerError('Abstract class "DatabaseInterface" cannot be instantiated directly');
}
}
async initDb() {
throw new ServerError('You must implement this function');
}
async comparePassword(username, password) {
throw new ServerError('You must implement this function');
}
async updatePassword(username, oldPassword, newPassword) {
throw new ServerError('You must implement this function');
}
};

101
src/lib/database/SQLite.js

@ -0,0 +1,101 @@
'use strict';
const path = require('path');
const debug = require('debug')('Database');
const bcrypt = require('bcryptjs');
const sqlite = require('sqlite3');
const DatabaseInterface = require('./Interface');
const ServerError = require('../ServerError');
const { WG_PATH } = require('../../config');
module.exports = class SQLiteDatabase extends DatabaseInterface {
constructor() {
super();
this.dbPath = path.join(WG_PATH, 'db.sqlite');
this.provider = 'SQLite';
this.db = null;
}
async initDb() {
return new Promise((resolve, reject) => {
this.db = new sqlite.Database(this.dbPath, async (err) => {
if (err) {
reject(new ServerError('Could not initialize the database'));
}
debug(`Connected to the SQLite database at ${this.dbPath}`);
await this.__setupDatabase();
resolve(true);
});
});
}
async __setupDatabase() {
const createAdminTableQuery = `
CREATE TABLE IF NOT EXISTS admin (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)
`;
this.db.run(createAdminTableQuery, (err) => {
if (err) {
throw new ServerError('Could not create admin table');
}
});
/* Default username is 'admin' */
this.db.get('SELECT * FROM admin WHERE username = ?', ['admin'], async (err, row) => {
if (err) {
throw new ServerError('Could not query admin table');
}
if (!row) {
const hashedPassword = await bcrypt.hash('admin', 12);
this.db.run('INSERT INTO admin (username, password) VALUES (?, ?)', ['admin', hashedPassword], (err) => {
if (err) {
throw new ServerError('Could not create initial admin user');
}
debug('Init admin username:password "admin:admin"');
});
}
});
}
async comparePassword(username, password) {
return new Promise((resolve, reject) => {
this.db.get('SELECT password FROM admin WHERE username = ?', [username], async (err, row) => {
if (err || !row) {
return reject(new ServerError('Could not query admin table'));
}
const isMatch = await bcrypt.compare(password, row.password);
resolve(isMatch);
});
});
}
async updatePassword(username, oldPassword, newPassword) {
const isMatch = await this.comparePassword(username, oldPassword);
if (!isMatch) {
throw new ServerError('Invalid credentials', 401);
}
const hashedPassword = await bcrypt.hash(newPassword, 10);
return new Promise((resolve, reject) => {
this.db.run('UPDATE admin SET password = ? WHERE username = ?', [hashedPassword, username], (err) => {
if (err) {
return reject(new ServerError('Could not update password'));
}
resolve(true);
});
});
}
};

1353
src/package-lock.json

File diff suppressed because it is too large

4
src/package.json

@ -8,7 +8,6 @@
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"serve": "DEBUG=Server,WireGuard npx nodemon server.js", "serve": "DEBUG=Server,WireGuard npx nodemon server.js",
"serve-with-password": "PASSWORD=wg npm run serve",
"lint": "eslint .", "lint": "eslint .",
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css" "buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
}, },
@ -19,7 +18,8 @@
"debug": "^4.3.6", "debug": "^4.3.6",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"h3": "^1.12.0", "h3": "^1.12.0",
"qrcode": "^1.5.3" "qrcode": "^1.5.3",
"sqlite3": "^5.1.7"
}, },
"devDependencies": { "devDependencies": {
"eslint-config-athom": "^3.1.3", "eslint-config-athom": "^3.1.3",

11
src/server.js

@ -3,8 +3,19 @@
require('./services/Server'); require('./services/Server');
const WireGuard = require('./services/WireGuard'); const WireGuard = require('./services/WireGuard');
const Database = require('./services/Database');
WireGuard.getConfig() WireGuard.getConfig()
.then(() => {
Database.initDb()
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
});
})
.catch((err) => { .catch((err) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err); console.error(err);

13
src/services/Database.js

@ -0,0 +1,13 @@
'use strict';
const SQLiteDatabase = require('../lib/database/SQLite');
// const MySqlDatabase = require('../lib/database/MySql');
/*
module.exports = {
SQLiteDatabase: new SQLiteDatabase(),
...
};
*/
module.exports = new SQLiteDatabase();

80
src/templates/Dashboard.vue

@ -0,0 +1,80 @@
<script>
export default {
name: 'Dashboard',
data() {
return {
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
};
},
methods: {
async updatePassword(e) {
e.preventDefault();
if (this.newPassword !== this.confirmNewPassword) {
alert('Password Mismatch');
return;
}
// temp username 'admin'
this.api.updatePassword({
username: 'admin',
oldPassword: this.currentPassword,
newPassword: this.newPassword
}).then((result) => {
this.currentPassword = '';
this.newPassword = '';
this.confirmNewPassword = '';
alert('Password updated');
}).catch((err) => {
alert(err.message || err.toString());
});
},
},
mounted() {
this.api = new API();
},
};
</script>
<template>
<div class="w-full shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden">
<div class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-neutral-500/50 dark:border-neutral-600">
<div class="flex-grow">
<p class="text-2xl font-medium dark:text-neutral-200">{{ $t('dashboard') }}</p>
</div>
</div>
<div class="container p-2 flex flex-col md:items-center py-8">
<form @submit="updatePassword" class="w-full md:w-[75%]">
<div class="mb-4">
<label for="currentPassword" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('currentPassword') }}
</label>
<input id="currentPassword" v-model="currentPassword" type="password" placeholder="Current Password"
class="outline-none bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
<div class="mb-4">
<label for="newPassword" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('newPassword') }}
</label>
<input id="newPassword" v-model="newPassword" type="password" placeholder="New Password"
class="outline-none bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
<div class="mb-4">
<label for="confirmNewPassword" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('confirmNewPassword') }}
</label>
<input id="confirmNewPassword" v-model="confirmNewPassword" type="password" placeholder="Confirm New Password"
class="outline-none bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
<button type="submit" class="bg-neutral-400 text-white p-2 rounded-md w-full hover:bg-red-800 transition">
{{ $t('updatePassword') }}
</button>
</form>
</div>
</div>
</template>

54
src/wgpw.mjs

@ -1,54 +0,0 @@
'use strict';
// 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);
// eslint-disable-next-line no-console
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) {
// eslint-disable-next-line no-console
console.log('Password matches the hash !');
} else {
// eslint-disable-next-line no-console
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) {
// eslint-disable-next-line no-console
console.error(error);
// eslint-disable-next-line no-process-exit
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 "$@"

54
src/www/css/app.css

@ -895,8 +895,8 @@ video {
min-width: 5rem; min-width: 5rem;
} }
.max-w-3xl { .max-w-5xl {
max-width: 48rem; max-width: 64rem;
} }
.flex-auto { .flex-auto {
@ -1094,6 +1094,15 @@ video {
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
} }
.border-neutral-500 {
--tw-border-opacity: 1;
border-color: rgb(115 115 115 / var(--tw-border-opacity));
}
.border-neutral-500\/50 {
border-color: rgb(115 115 115 / 0.5);
}
.border-red-800 { .border-red-800 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity)); border-color: rgb(153 27 27 / var(--tw-border-opacity));
@ -1128,6 +1137,11 @@ video {
background-color: rgb(107 114 128 / var(--tw-bg-opacity)); background-color: rgb(107 114 128 / var(--tw-bg-opacity));
} }
.bg-neutral-400 {
--tw-bg-opacity: 1;
background-color: rgb(163 163 163 / var(--tw-bg-opacity));
}
.bg-red-100 { .bg-red-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity)); background-color: rgb(254 226 226 / var(--tw-bg-opacity));
@ -1143,6 +1157,10 @@ video {
background-color: rgb(153 27 27 / var(--tw-bg-opacity)); background-color: rgb(153 27 27 / var(--tw-bg-opacity));
} }
.bg-transparent {
background-color: transparent;
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -1214,6 +1232,11 @@ video {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-1 { .pb-1 {
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
@ -1438,6 +1461,12 @@ video {
transition-duration: 150ms; transition-duration: 150ms;
} }
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-opacity { .transition-opacity {
transition-property: opacity; transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@ -1549,6 +1578,14 @@ video {
display: none; display: none;
} }
.max-md\:flex-col {
flex-direction: column;
}
.max-md\:gap-y-8 {
row-gap: 2rem;
}
.max-md\:border-x-0 { .max-md\:border-x-0 {
border-left-width: 0px; border-left-width: 0px;
border-right-width: 0px; border-right-width: 0px;
@ -1702,6 +1739,10 @@ video {
display: inline-block; display: inline-block;
} }
.md\:w-\[75\%\] {
width: 75%;
}
.md\:min-w-24 { .md\:min-w-24 {
min-width: 6rem; min-width: 6rem;
} }
@ -1710,10 +1751,19 @@ video {
flex-shrink: 0; flex-shrink: 0;
} }
.md\:items-center {
align-items: center;
}
.md\:gap-4 { .md\:gap-4 {
gap: 1rem; gap: 1rem;
} }
.md\:gap-x-8 {
-moz-column-gap: 2rem;
column-gap: 2rem;
}
.md\:rounded { .md\:rounded {
border-radius: 0.25rem; border-radius: 0.25rem;
} }

128
src/www/index.html

@ -15,6 +15,7 @@
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
.line-chart .apexcharts-svg { .line-chart .apexcharts-svg {
transform: translateY(3px); transform: translateY(3px);
} }
@ -22,23 +23,25 @@
<body class="bg-gray-50 dark:bg-neutral-800"> <body class="bg-gray-50 dark:bg-neutral-800">
<div id="app"> <div id="app">
<div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6"> <div v-cloak class="container mx-auto max-w-5xl px-3 md:px-0 mt-4 xs:mt-6">
<div v-if="authenticated === true"> <div v-if="authenticated === true">
<div class="flex flex-col-reverse xxs:flex-row flex-auto items-center items-end gap-3"> <div class="flex flex-col-reverse xxs:flex-row flex-auto items-end gap-3">
<h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4"> <h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4">
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg mr-2" /><span class="align-middle">WireGuard</span> <img src="./img/logo.png" width="32" class="inline align-middle dark:bg mr-2"/><span
class="align-middle">WireGuard</span>
</h1> </h1>
<div class="flex items-center grow-0 gap-3 items-end self-end xxs:self-center"> <div class="flex grow-0 gap-3 items-end self-end xxs:self-center">
<!-- Dark / light theme --> <!-- Dark / light theme -->
<button @click="toggleTheme" <button @click="toggleTheme"
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition" :title="$t(`theme.${uiTheme}`)"> class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition"
<svg v-if="uiTheme === 'light'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" :title="$t(`theme.${uiTheme}`)">
class="w-5 h-5"> <svg v-if="uiTheme === 'light'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/> d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/>
</svg> </svg>
<svg v-else-if="uiTheme === 'dark'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" <svg v-else-if="uiTheme === 'dark'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="w-5 h-5 text-neutral-400"> stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-neutral-400">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/> d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/>
</svg> </svg>
@ -52,19 +55,22 @@
</svg> </svg>
</button> </button>
<!-- Show / hide charts --> <!-- Show / hide charts -->
<label v-if="uiChartType > 0" class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group" :title="$t('toggleCharts')"> <label v-if="uiChartType > 0"
class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group"
:title="$t('toggleCharts')">
<input type="checkbox" value="" class="sr-only peer" v-model="uiShowCharts" @change="toggleCharts"> <input type="checkbox" value="" class="sr-only peer" v-model="uiShowCharts" @change="toggleCharts">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
fill="currentColor"
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition"> class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition">
<path <path
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z"/> d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z"/>
</svg> </svg>
</label> </label>
<span v-if="requiresPassword" <span v-if="requiresPassword"
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline" class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline" @click="logout">
@click="logout">
{{$t("logout")}} {{$t("logout")}}
<svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3 inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/> d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg> </svg>
@ -88,8 +94,28 @@
</div> </div>
</div> </div>
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"> <div class="flex max-md:flex-col max-md:gap-y-8 md:gap-x-8">
<div class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600"> <!-- Navigation -->
<nav class="shadow-md rounded-lg bg-white dark:bg-neutral-700 text-white p-4">
<div class="container mx-auto flex justify-between items-center">
<div class="flex flex-col">
<button @click="showPage = 'home'"
class="flex items-center rounded hover:bg-red-800 transition-colors px-3 py-2 text-lg">
{{$t('navHome')}}
</button>
<button @click="showPage = 'dashboard'"
class="flex items-center rounded hover:bg-red-800 transition-colors px-3 py-2 text-lg">
{{$t('navDashboard')}}
</button>
</div>
</div>
</nav>
<!-- Clients (main) -->
<div v-if="showPage === 'home'"
class="w-full shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden">
<div
class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600">
<div class="flex-grow"> <div class="flex-grow">
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p> <p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
</div> </div>
@ -97,17 +123,24 @@
<!-- Restore configuration --> <!-- Restore configuration -->
<label for="inputRC" :title="$t('titleRestoreConfig')" <label for="inputRC" :title="$t('titleRestoreConfig')"
class="hover:cursor-pointer hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-r-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-l-full md:rounded inline-flex items-center transition"> class="hover:cursor-pointer hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-r-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-l-full md:rounded inline-flex items-center transition">
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path> stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99">
</path>
</svg> </svg>
<span class="max-md:hidden text-sm">{{$t("restore")}}</span> <span class="max-md:hidden text-sm">{{$t("restore")}}</span>
<input id="inputRC" type="file" name="configurationfile" accept="text/*,.json" @change="restoreConfig" class="hidden"/> <input id="inputRC" type="file" name="configurationfile" accept="text/*,.json" @change="restoreConfig"
class="hidden"/>
</label> </label>
<!-- Backup configuration --> <!-- Backup configuration -->
<a href="./api/wireguard/backup" :title="$t('titleBackupConfig')" <a href="./api/wireguard/backup" :title="$t('titleBackupConfig')"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition"> class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition">
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"></path> stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z">
</path>
</svg> </svg>
<span class="max-md:hidden text-sm">{{$t("backup")}}</span> <span class="max-md:hidden text-sm">{{$t("backup")}}</span>
</a> </a>
@ -130,11 +163,13 @@
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid"> class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid">
<!-- Chart --> <!-- Chart -->
<div v-if="uiChartType" :class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" > <div v-if="uiChartType"
:class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`">
<apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries"> <apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries">
</apexchart> </apexchart>
</div> </div>
<div v-if="uiChartType" :class="`absolute z-0 top-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" > <div v-if="uiChartType"
:class="`absolute z-0 top-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`">
<apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries" <apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries"
style="transform: scaleY(-1);"> style="transform: scaleY(-1);">
</apexchart> </apexchart>
@ -215,9 +250,11 @@
</span> </span>
</span> </span>
<!-- Inline Transfer TX --> <!-- Inline Transfer TX -->
<span v-if="!uiTrafficStats && client.transferTx" class="whitespace-nowrap" :title="$t('totalDownload') + bytes(client.transferTx)"> <span v-if="!uiTrafficStats && client.transferTx" class="whitespace-nowrap"
:title="$t('totalDownload') + bytes(client.transferTx)">
· ·
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z" d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
clip-rule="evenodd"/> clip-rule="evenodd"/>
@ -226,9 +263,11 @@
</span> </span>
<!-- Inline Transfer RX --> <!-- Inline Transfer RX -->
<span v-if="!uiTrafficStats && client.transferRx" class="whitespace-nowrap" :title="$t('totalUpload') + bytes(client.transferRx)"> <span v-if="!uiTrafficStats && client.transferRx" class="whitespace-nowrap"
:title="$t('totalUpload') + bytes(client.transferRx)">
· ·
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z" d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
clip-rule="evenodd"/> clip-rule="evenodd"/>
@ -236,7 +275,8 @@
{{client.transferRxCurrent | bytes}}/s {{client.transferRxCurrent | bytes}}/s
</span> </span>
<!-- Last seen --> <!-- Last seen -->
<span class="text-gray-400 dark:text-neutral-500 whitespace-nowrap" v-if="client.latestHandshakeAt" <span class="text-gray-400 dark:text-neutral-500 whitespace-nowrap"
v-if="client.latestHandshakeAt"
:title="$t('lastSeen') + dateTime(new Date(client.latestHandshakeAt))"> :title="$t('lastSeen') + dateTime(new Date(client.latestHandshakeAt))">
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}} {{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}}
</span> </span>
@ -260,7 +300,8 @@
<span class="text-gray-700 dark:text-neutral-200">{{client.transferTxCurrent | <span class="text-gray-700 dark:text-neutral-200">{{client.transferTxCurrent |
bytes}}/s</span> bytes}}/s</span>
<!-- Total TX --> <!-- Total TX -->
<br><span class="font-regular" style="font-size:0.85em">{{bytes(client.transferTx)}}</span> <br><span class="font-regular"
style="font-size:0.85em">{{bytes(client.transferTx)}}</span>
</div> </div>
</span> </span>
@ -280,7 +321,8 @@
<span class="text-gray-700 dark:text-neutral-200">{{client.transferRxCurrent | <span class="text-gray-700 dark:text-neutral-200">{{client.transferRxCurrent |
bytes}}/s</span> bytes}}/s</span>
<!-- Total RX --> <!-- Total RX -->
<br><span class="font-regular" style="font-size:0.85em">{{bytes(client.transferRx)}}</span> <br><span class="font-regular"
style="font-size:0.85em">{{bytes(client.transferRx)}}</span>
</div> </div>
</span> </span>
</div> </div>
@ -312,8 +354,7 @@
:class="{ :class="{
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig, 'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig,
'is-disabled': !client.downloadableConfig 'is-disabled': !client.downloadableConfig
}" }" :title="!client.downloadableConfig ? $t('noPrivKey') : $t('showQR')"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('showQR')"
@click="qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`"> @click="qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
@ -330,8 +371,7 @@
:class="{ :class="{
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig, 'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig,
'is-disabled': !client.downloadableConfig 'is-disabled': !client.downloadableConfig
}" }" :title="!client.downloadableConfig ? $t('noPrivKey') : $t('downloadConfig')"
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('downloadConfig')"
@click="if(!client.downloadableConfig) { $event.preventDefault(); }"> @click="if(!client.downloadableConfig) { $event.preventDefault(); }">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
@ -383,6 +423,10 @@
</div> </div>
</div> </div>
<!-- Dashboard -->
<dashboard v-if="showPage === 'dashboard'"></dashboard>
</div>
<!-- QR Code--> <!-- QR Code-->
<div v-if="qrcode"> <div v-if="qrcode">
<div class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center z-20"> <div class="bg-black bg-opacity-50 fixed top-0 right-0 left-0 bottom-0 flex items-center justify-center z-20">
@ -435,8 +479,8 @@
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div <div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10"> class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-white" inline xmlns="http://www.w3.org/2000/svg" <svg class="h-6 w-6 text-white" inline xmlns="http://www.w3.org/2000/svg" fill="none"
fill="none" viewBox="0 0 24 24" stroke="currentColor"> viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/> d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg> </svg>
@ -553,8 +597,8 @@
class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10"> class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10">
<!-- Avatar --> <!-- Avatar -->
<div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 dark:bg-red-800 relative overflow-hidden"> <div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 dark:bg-red-800 relative overflow-hidden">
<svg class="w-10 h-10 m-5 text-white dark:text-white" xmlns="http://www.w3.org/2000/svg" <svg class="w-10 h-10 m-5 text-white dark:text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
viewBox="0 0 20 20" fill="currentColor"> fill="currentColor">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/>
</svg> </svg>
</div> </div>
@ -595,9 +639,10 @@
</div> </div>
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <a class="hover:underline" target="_blank" <p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <a class="hover:underline"
href="https://github.com/wg-easy/wg-easy">WireGuard Easy</a> © 2021-2024 by <a class="hover:underline" target="_blank" target="_blank" href="https://github.com/wg-easy/wg-easy">WireGuard Easy</a> © 2021-2024 by <a
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> is licensed under <a class="hover:underline" target="_blank" class="hover:underline" target="_blank" href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> is
licensed under <a class="hover:underline" target="_blank"
href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> · <a class="hover:underline" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> · <a class="hover:underline"
href="https://github.com/sponsors/WeeJeWel" target="_blank">{{$t("donate")}}</a></p> href="https://github.com/sponsors/WeeJeWel" target="_blank">{{$t("donate")}}</a></p>
@ -609,6 +654,7 @@
<script src="./js/vendor/vue-apexcharts.min.js"></script> <script src="./js/vendor/vue-apexcharts.min.js"></script>
<script src="./js/vendor/sha256.min.js"></script> <script src="./js/vendor/sha256.min.js"></script>
<script src="./js/vendor/timeago.full.min.js"></script> <script src="./js/vendor/timeago.full.min.js"></script>
<script src="./js/vendor/vue-dashboard.umd.min.js"></script>
<script src="./js/api.js"></script> <script src="./js/api.js"></script>
<script src="./js/i18n.js"></script> <script src="./js/i18n.js"></script>
<script src="./js/app.js"></script> <script src="./js/app.js"></script>

12
src/www/js/api.js

@ -64,11 +64,11 @@ class API {
}); });
} }
async createSession({ password }) { async createSession({ username, password }) {
return this.call({ return this.call({
method: 'post', method: 'post',
path: '/session', path: '/session',
body: { password }, body: { username, password },
}); });
} }
@ -146,4 +146,12 @@ class API {
}); });
} }
async updatePassword({ username, oldPassword, newPassword }) {
return this.call({
method: 'put',
path: '/dashboard/setting/password',
body: { username, oldPassword, newPassword },
});
}
} }

7
src/www/js/app.js

@ -46,11 +46,14 @@ new Vue({
el: '#app', el: '#app',
components: { components: {
apexchart: VueApexCharts, apexchart: VueApexCharts,
dashboard: VueDashboard,
}, },
i18n, i18n,
data: { data: {
authenticated: null, authenticated: null,
authenticating: false, authenticating: false,
// temp username 'admin'
username: 'admin',
password: null, password: null,
requiresPassword: null, requiresPassword: null,
@ -155,6 +158,8 @@ new Vue({
}, },
}, },
}, },
showPage: 'home',
}, },
methods: { methods: {
dateTime: (value) => { dateTime: (value) => {
@ -238,6 +243,8 @@ new Vue({
this.authenticating = true; this.authenticating = true;
this.api.createSession({ this.api.createSession({
// temp username 'admin'
username: this.username,
password: this.password, password: this.password,
}) })
.then(async () => { .then(async () => {

7
src/www/js/i18n.js

@ -34,6 +34,13 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Backup', backup: 'Backup',
titleRestoreConfig: 'Restore your configuration', titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration', titleBackupConfig: 'Backup your configuration',
navHome: 'Home',
navDashboard: 'Dashboard',
dashboard: 'Dashboard',
currentPassword: 'Current Password',
newPassword: 'New Password',
confirmNewPassword: 'Confirm New Password',
updatePassword: 'Update Password',
}, },
ua: { ua: {
name: 'Ім`я', name: 'Ім`я',

2
src/www/js/vendor/vue-dashboard.umd.min.js

@ -0,0 +1,2 @@
(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t():"function"===typeof define&&define.amd?define([],t):"object"===typeof exports?exports["VueDashboard"]=t():e["VueDashboard"]=t()})("undefined"!==typeof self?self:this,(()=>(()=>{"use strict";var e={};(()=>{e.d=(t,r)=>{for(var s in r)e.o(r,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:r[s]})}})(),(()=>{e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{e.p=""})();var t={};if(e.d(t,{default:()=>c}),"undefined"!==typeof window){var r=window.document.currentScript,s=r&&r.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);s&&(e.p=s[1])}var o=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"w-full shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"},[r("div",{staticClass:"flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-neutral-500/50 dark:border-neutral-600"},[r("div",{staticClass:"flex-grow"},[r("p",{staticClass:"text-2xl font-medium dark:text-neutral-200"},[e._v(e._s(e.$t("dashboard")))])])]),r("div",{staticClass:"container p-2 flex flex-col md:items-center py-8"},[r("form",{staticClass:"w-full md:w-[75%]",on:{submit:e.updatePassword}},[r("div",{staticClass:"mb-4"},[r("label",{staticClass:"block text-sm font-medium dark:text-neutral-200 mb-4",attrs:{for:"currentPassword"}},[e._v(" "+e._s(e.$t("currentPassword"))+" ")]),r("input",{directives:[{name:"model",rawName:"v-model",value:e.currentPassword,expression:"currentPassword"}],staticClass:"outline-none bg-transparent border border-neutral-500 p-2 rounded-md w-full",attrs:{id:"currentPassword",type:"password",placeholder:"Current Password",required:""},domProps:{value:e.currentPassword},on:{input:function(t){t.target.composing||(e.currentPassword=t.target.value)}}})]),r("div",{staticClass:"mb-4"},[r("label",{staticClass:"block text-sm font-medium dark:text-neutral-200 mb-4",attrs:{for:"newPassword"}},[e._v(" "+e._s(e.$t("newPassword"))+" ")]),r("input",{directives:[{name:"model",rawName:"v-model",value:e.newPassword,expression:"newPassword"}],staticClass:"outline-none bg-transparent border border-neutral-500 p-2 rounded-md w-full",attrs:{id:"newPassword",type:"password",placeholder:"New Password",required:""},domProps:{value:e.newPassword},on:{input:function(t){t.target.composing||(e.newPassword=t.target.value)}}})]),r("div",{staticClass:"mb-4"},[r("label",{staticClass:"block text-sm font-medium dark:text-neutral-200 mb-4",attrs:{for:"confirmNewPassword"}},[e._v(" "+e._s(e.$t("confirmNewPassword"))+" ")]),r("input",{directives:[{name:"model",rawName:"v-model",value:e.confirmNewPassword,expression:"confirmNewPassword"}],staticClass:"outline-none bg-transparent border border-neutral-500 p-2 rounded-md w-full",attrs:{id:"confirmNewPassword",type:"password",placeholder:"Confirm New Password",required:""},domProps:{value:e.confirmNewPassword},on:{input:function(t){t.target.composing||(e.confirmNewPassword=t.target.value)}}})]),r("button",{staticClass:"bg-neutral-400 text-white p-2 rounded-md w-full hover:bg-red-800 transition",attrs:{type:"submit"}},[e._v(" "+e._s(e.$t("updatePassword"))+" ")])])])])},a=[];const n={name:"Dashboard",data(){return{currentPassword:"",newPassword:"",confirmNewPassword:""}},methods:{async updatePassword(e){e.preventDefault(),this.newPassword===this.confirmNewPassword?this.api.updatePassword({username:"admin",oldPassword:this.currentPassword,newPassword:this.newPassword}).then((e=>{this.currentPassword="",this.newPassword="",this.confirmNewPassword="",alert("Password updated")})).catch((e=>{alert(e.message||e.toString())})):alert("Password Mismatch")}},mounted(){this.api=new API}},d=n;function i(e,t,r,s,o,a,n,d){var i,l="function"===typeof e?e.options:e;if(t&&(l.render=t,l.staticRenderFns=r,l._compiled=!0),s&&(l.functional=!0),a&&(l._scopeId="data-v-"+a),n?(i=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(n)},l._ssrRegister=i):o&&(i=d?function(){o.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:o),i)if(l.functional){l._injectStyles=i;var u=l.render;l.render=function(e,t){return i.call(t),u(e,t)}}else{var c=l.beforeCreate;l.beforeCreate=c?[].concat(c,i):[i]}return{exports:e,options:l}}var l=i(d,o,a,!1,null,null,null);const u=l.exports,c=u;return t=t["default"],t})()));
//# sourceMappingURL=vue-dashboard.umd.min.js.map

1
src/www/js/vendor/vue-dashboard.umd.min.js.map

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save