mirror of
https://github.com/WeeJeWel/wg-easy.git
synced 2024-12-05 08:49:24 +08:00
wip
This commit is contained in:
parent
eaf3d5c3fb
commit
d7bb645470
BIN
assets/wg-easy.sketch
Normal file
BIN
assets/wg-easy.sketch
Normal file
Binary file not shown.
11
config/wg0 copy.conf
Normal file
11
config/wg0 copy.conf
Normal file
@ -0,0 +1,11 @@
|
||||
[Interface]
|
||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o= # wg genkey > privatekey
|
||||
# PublicKey = BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0=
|
||||
Address = 10.8.0.1/24
|
||||
ListenPort = 51820
|
||||
DNS = 1.1.1.1
|
||||
|
||||
[Peer]
|
||||
PublicKey = i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4=
|
||||
PresharedKey = MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU=
|
||||
AllowedIPs = 10.8.0.2/32
|
@ -1,10 +1,15 @@
|
||||
|
||||
# Note: Do not edit this file directly.
|
||||
# Your changes will be overwritten!
|
||||
|
||||
# Server
|
||||
[Interface]
|
||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o= # wg genkey > privatekey
|
||||
# PublicKey = BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0=
|
||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o=
|
||||
Address = 10.8.0.1/24
|
||||
ListenPort = 51820
|
||||
DNS = 1.1.1.1
|
||||
|
||||
# Client: Emile (af3111a4-7343-4380-a293-ed498d9aa3b8)
|
||||
[Peer]
|
||||
PublicKey = i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4=
|
||||
PresharedKey = MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU=
|
||||
|
22
config/wg0.json
Normal file
22
config/wg0.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"server": {
|
||||
"privateKey": "iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o=",
|
||||
"publicKey": "BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0=",
|
||||
"address": "10.8.0.1/24",
|
||||
"port": "51820",
|
||||
"dns": "1.1.1.1"
|
||||
},
|
||||
"clients": {
|
||||
"af3111a4-7343-4380-a293-ed498d9aa3b8": {
|
||||
"name": "Emile",
|
||||
"createdAt": "2021-05-22T20:02:45.372Z",
|
||||
"updatedAt": "2021-05-22T20:02:45.372Z",
|
||||
"privateKey": "sHUUDbaZBQshfOvvF8HeebhhXq3rDKWlW1Vm+6XMklU=",
|
||||
"publicKey": "i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4=",
|
||||
"preSharedKey": "MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU=",
|
||||
"address": "10.8.0.2/32",
|
||||
"allowedIPs": "10.8.0.2/32",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
7
src/.eslintrc.json
Normal file
7
src/.eslintrc.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "athom",
|
||||
"rules": {
|
||||
"consistent-return": "off",
|
||||
"no-shadow": "off"
|
||||
}
|
||||
}
|
1
src/.gitignore
vendored
Normal file
1
src/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/node_modules
|
8
src/config.js
Normal file
8
src/config.js
Normal file
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports.PORT = process.env.PORT || 80;
|
||||
module.exports.PASSWORD = process.env.PASSWORD || 'wireguard';
|
||||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
|
||||
module.exports.WG_HOST = process.env.WG_HOST || '127.0.0.1';
|
||||
module.exports.WG_PORT = process.env.WG_PORT || 51820;
|
||||
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.6.0.1/32';
|
104
src/lib/Server.js
Normal file
104
src/lib/Server.js
Normal file
@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const express = require('express');
|
||||
const expressSession = require('express-session');
|
||||
const debug = require('debug')('Server');
|
||||
|
||||
const Util = require('./Util');
|
||||
const ServerError = require('./ServerError');
|
||||
const WireGuard = require('../services/WireGuard');
|
||||
|
||||
const {
|
||||
PORT,
|
||||
PASSWORD,
|
||||
} = require('../config');
|
||||
|
||||
module.exports = class Server {
|
||||
|
||||
constructor() {
|
||||
// Express
|
||||
this.app = express()
|
||||
.use('/', express.static(path.join(__dirname, '..', 'www')))
|
||||
.use(express.json())
|
||||
.use(expressSession({
|
||||
secret: String(Math.random()),
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
}))
|
||||
|
||||
// Authentication
|
||||
.get('/api/session', Util.promisify(async req => {
|
||||
return {
|
||||
authenticated: !!(req.session && req.session.authenticated),
|
||||
};
|
||||
}))
|
||||
.post('/api/session', Util.promisify(async req => {
|
||||
const {
|
||||
password,
|
||||
} = req.body;
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
throw new ServerError('Missing: Password', 401);
|
||||
}
|
||||
|
||||
if (password !== PASSWORD) {
|
||||
throw new ServerError('Incorrect Password', 401);
|
||||
}
|
||||
|
||||
req.session.authenticated = true;
|
||||
req.session.save();
|
||||
|
||||
debug(`New Session: ${req.session.id})`);
|
||||
}))
|
||||
|
||||
// WireGuard
|
||||
.use(Util.requireSession)
|
||||
.delete('/api/session', Util.promisify(async req => {
|
||||
const sessionId = req.session.id;
|
||||
|
||||
req.session.destroy();
|
||||
|
||||
debug(`Deleted Session: ${sessionId}`);
|
||||
}))
|
||||
.get('/api/wireguard/client', Util.promisify(async req => {
|
||||
return WireGuard.getClients();
|
||||
}))
|
||||
.get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
|
||||
res.header('Content-Type', 'image/svg+xml');
|
||||
res.send(svg);
|
||||
}))
|
||||
.get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => {
|
||||
const { clientId } = req.params;
|
||||
const client = await WireGuard.getClient({ clientId });
|
||||
const config = await WireGuard.getClientConfiguration({ clientId });
|
||||
res.header('Content-Disposition', `attachment; filename="${client.name}.conf"`);
|
||||
res.header('Content-Type', 'text/plain');
|
||||
res.send(config);
|
||||
}))
|
||||
.post('/api/wireguard/client', Util.promisify(async req => {
|
||||
const { name } = req.body;
|
||||
return WireGuard.createClient({ name });
|
||||
}))
|
||||
.delete('/api/wireguard/client/:clientId', Util.promisify(async req => {
|
||||
const { clientId } = req.params;
|
||||
return WireGuard.deleteClient({ clientId });
|
||||
}))
|
||||
.post('/api/wireguard/client/:clientId/enable', Util.promisify(async req => {
|
||||
const { clientId } = req.params;
|
||||
return WireGuard.enableClient({ clientId });
|
||||
}))
|
||||
.post('/api/wireguard/client/:clientId/disable', Util.promisify(async req => {
|
||||
const { clientId } = req.params;
|
||||
return WireGuard.disableClient({ clientId });
|
||||
}))
|
||||
|
||||
.listen(PORT, () => {
|
||||
debug(`Listening on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
10
src/lib/ServerError.js
Normal file
10
src/lib/ServerError.js
Normal file
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = class ServerError extends Error {
|
||||
|
||||
constructor(message, statusCode = 500) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
};
|
46
src/lib/Util.js
Normal file
46
src/lib/Util.js
Normal file
@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = class Util {
|
||||
|
||||
static requireSession(req, res, next) {
|
||||
if (req.session && req.session.authenticated) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
error: 'Not Logged In',
|
||||
});
|
||||
}
|
||||
|
||||
static promisify(fn) {
|
||||
return function(req, res) {
|
||||
Promise.resolve().then(async () => fn(req, res))
|
||||
.then(result => {
|
||||
if (res.headersSent) return;
|
||||
|
||||
if (typeof result === 'undefined') {
|
||||
return res
|
||||
.status(204)
|
||||
.end();
|
||||
}
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json(result);
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof error === 'string') {
|
||||
error = new Error(error);
|
||||
}
|
||||
|
||||
return res
|
||||
.status(error.statusCode || 500)
|
||||
.json({
|
||||
error: error.message || error.toString(),
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
};
|
183
src/lib/WireGuard.js
Normal file
183
src/lib/WireGuard.js
Normal file
@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
const ServerError = require('./ServerError');
|
||||
|
||||
const {
|
||||
WG_PATH,
|
||||
WG_HOST,
|
||||
WG_PORT,
|
||||
WG_DEFAULT_ADDRESS,
|
||||
} = require('../config');
|
||||
|
||||
module.exports = class WireGuard {
|
||||
|
||||
constructor() {
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
const config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
|
||||
this.config = JSON.parse(config);
|
||||
} catch (err) {
|
||||
this.config = {
|
||||
// TODO: Generate new config
|
||||
server: {
|
||||
address: WG_DEFAULT_ADDRESS,
|
||||
},
|
||||
clients: {},
|
||||
};
|
||||
// TODO: Save JSON config
|
||||
}
|
||||
|
||||
console.log('this.config', this.config);
|
||||
|
||||
await this.saveConfig();
|
||||
}).catch(err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
async saveConfig() {
|
||||
let result = `
|
||||
# Note: Do not edit this file directly.
|
||||
# Your changes will be overwritten!
|
||||
|
||||
# Server
|
||||
[Interface]
|
||||
PrivateKey = ${this.config.server.privateKey}
|
||||
Address = ${this.config.server.address}
|
||||
ListenPort = ${this.config.server.port}
|
||||
DNS = ${this.config.server.dns}`;
|
||||
|
||||
for (const [clientId, client] of Object.entries(this.config.clients)) {
|
||||
if (!client.enabled) continue;
|
||||
|
||||
result += `
|
||||
|
||||
# Client: ${client.name} (${clientId})
|
||||
[Peer]
|
||||
PublicKey = ${client.publicKey}
|
||||
PresharedKey = ${client.preSharedKey}
|
||||
AllowedIPs = ${client.allowedIPs}`;
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(this.config, false, 2));
|
||||
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result);
|
||||
}
|
||||
|
||||
async getClients() {
|
||||
return Object.entries(this.config.clients).map(([clientId, client]) => ({
|
||||
id: clientId,
|
||||
name: client.name,
|
||||
enabled: client.enabled,
|
||||
publicKey: client.publicKey,
|
||||
createdAt: client.createdAt,
|
||||
updatedAt: client.updatedAt,
|
||||
allowedIPs: client.allowedIPs,
|
||||
|
||||
// TODO:
|
||||
latestHandshake: new Date(),
|
||||
transferRx: 0,
|
||||
transferTx: 0,
|
||||
}));
|
||||
// const { stdout } = await Util.exec('sudo cat /etc/wireguard/configs/clients.txt');
|
||||
// return stdout
|
||||
// .trim()
|
||||
// .split('\n')
|
||||
// .filter(line => {
|
||||
// return line.length > 0;
|
||||
// })
|
||||
// .map(line => {
|
||||
// const [ name, publicKey, createdAt ] = line.split(' ');
|
||||
// return {
|
||||
// name,
|
||||
// publicKey,
|
||||
// createdAt: new Date(Number(createdAt + '000')),
|
||||
// };
|
||||
// });
|
||||
}
|
||||
|
||||
async getClient({ clientId }) {
|
||||
const client = this.config.clients[clientId];
|
||||
if (!client) {
|
||||
throw new ServerError(`Client Not Found: ${clientId}`, 404);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
async getClientConfiguration({ clientId }) {
|
||||
const client = await this.getClient({ clientId });
|
||||
|
||||
return `
|
||||
[Interface]
|
||||
PrivateKey = ${client.privateKey}
|
||||
Address = ${client.address}
|
||||
DNS = ${this.config.server.dns}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${client.publicKey}
|
||||
PresharedKey = ${client.preSharedKey}
|
||||
AllowedIPs = ${client.allowedIPs}
|
||||
Endpoint = ${WG_HOST}:${WG_PORT}`;
|
||||
}
|
||||
|
||||
async getClientQRCodeSVG({ clientId }) {
|
||||
const config = await this.getClientConfiguration({ clientId });
|
||||
return QRCode.toString(config, {
|
||||
type: 'svg',
|
||||
width: 512,
|
||||
});
|
||||
}
|
||||
|
||||
async createClient({ name }) {
|
||||
if (!name) {
|
||||
throw new Error('Missing: Name');
|
||||
}
|
||||
|
||||
// try {
|
||||
// await this.getClient({ name });
|
||||
// throw new Error(`Duplicate Client: ${name}`);
|
||||
// } catch( err ) {
|
||||
// if( err.message.startsWith('Duplicate Client') ) {
|
||||
// throw err;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // TODO: This is unsafe
|
||||
// await this.ssh.exec(`pivpn add -n ${name}`);
|
||||
|
||||
// return this.getClient({ name });
|
||||
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
async deleteClient({ clientId }) {
|
||||
if (this.config.clients[clientId]) {
|
||||
delete this.config.clients[clientId];
|
||||
await this.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
async enableClient({ clientId }) {
|
||||
const client = await this.getClient({ clientId });
|
||||
client.enabled = true;
|
||||
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
async disableClient({ clientId }) {
|
||||
const client = await this.getClient({ clientId });
|
||||
client.enabled = false;
|
||||
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
};
|
3560
src/package-lock.json
generated
Normal file
3560
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
src/package.json
Normal file
31
src/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "wg-easy",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"serve": "DEBUG=Server PASSWORD=p WG_PATH=../config/ nodemon server.js"
|
||||
},
|
||||
"author": "Emile Nijssen",
|
||||
"license": "GPL",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"qrcode": "^1.4.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-athom": "^2.1.0",
|
||||
"nodemon": "^2.0.7"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"www/*"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
}
|
||||
}
|
3
src/server.js
Normal file
3
src/server.js
Normal file
@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
require('./services/Server');
|
5
src/services/Server.js
Normal file
5
src/services/Server.js
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const Server = require('../lib/Server');
|
||||
|
||||
module.exports = new Server();
|
5
src/services/WireGuard.js
Normal file
5
src/services/WireGuard.js
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const WireGuard = require('../lib/WireGuard');
|
||||
|
||||
module.exports = new WireGuard();
|
BIN
src/www/img/apple-touch-icon.png
Normal file
BIN
src/www/img/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
src/www/img/favicon.png
Normal file
BIN
src/www/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
380
src/www/index.html
Normal file
380
src/www/index.html
Normal file
@ -0,0 +1,380 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>WireGuard</title>
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="img/favicon.png">
|
||||
<link rel="apple-touch-icon" href="img/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50">
|
||||
<div id="app" class="container mx-auto">
|
||||
<div v-if="authenticated === true">
|
||||
<h1 class="text-4xl font-medium mt-10 mb-2">WireGuard</h1>
|
||||
<h2 class="text-sm text-gray-400 mb-10"><span class="cursor-pointer hover:underline" @click="logout">
|
||||
Logout
|
||||
<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"
|
||||
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>
|
||||
</span></h2>
|
||||
|
||||
<div class="shadow-md rounded-lg bg-white overflow-hidden">
|
||||
<div class="flex flex-row flex-auto items-center p-3 px-5 border border-b-2 border-gray-100">
|
||||
<div class="flex-grow">
|
||||
<p class="text-2xl font-medium">Clients</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
class="hover:bg-green-400 hover:border-green-400 hover:text-white text-gray-700 border-2 border-gray-100 py-2 px-4 rounded inline-flex items-center transition">
|
||||
<svg class="w-4 mr-2" 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" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">New</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id"
|
||||
class="p-5 flex flex-row">
|
||||
<div class="h-10 w-10 mr-5 rounded-full bg-gray-50 relative">
|
||||
<svg class="w-6 m-2 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
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" />
|
||||
</svg>
|
||||
<img v-if="client.avatar" :src="client.avatar" class="w-10 rounded-full absolute top-0 left-0" />
|
||||
|
||||
<div v-if="client.latestHandshake && ((new Date() - new Date(client.latestHandshake) < 1000 * 60 * 10))">
|
||||
<div class="animate-ping w-4 h-4 p-1 bg-green-100 rounded-full absolute -bottom-1 -right-1"></div>
|
||||
<div class="w-2 h-2 bg-green-300 rounded-full absolute bottom-0 right-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow">
|
||||
<div class="text-gray-700" :title="'Created at ' + dateTime(new Date(client.createdAt))">{{client.name}}
|
||||
</div>
|
||||
<div v-if="client.allowedIps" class="text-gray-300 text-xs">{{client.iface}}
|
||||
· {{client.allowedIps.split('/')[0]}}
|
||||
<span v-if="client.transferTx" title="Download">
|
||||
·
|
||||
<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"
|
||||
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" />
|
||||
</svg>
|
||||
{{client.transferTx | bytes}}
|
||||
</span>
|
||||
<span v-if="client.transferRx" title="Upload">
|
||||
·
|
||||
<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"
|
||||
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" />
|
||||
</svg>
|
||||
{{client.transferRx | bytes}}
|
||||
</span>
|
||||
<span v-if="client.latestHandshake"
|
||||
:title="'Last seen at ' + dateTime(new Date(client.latestHandshake))">
|
||||
· {{new Date(client.latestHandshake) | timeago}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<div class="text-gray-400">
|
||||
|
||||
<!-- Enable/Disable -->
|
||||
<div @click="disableClient(client)" v-if="client.enabled === true"
|
||||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-green-400 cursor-pointer hover:bg-green-500 transition-all">
|
||||
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
|
||||
</div>
|
||||
<div @click="enableClient(client)" v-if="client.enabled === false"
|
||||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 cursor-pointer hover:bg-gray-300 transition-all">
|
||||
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
|
||||
</div>
|
||||
|
||||
<!-- Show QR-->
|
||||
<button class="align-middle bg-gray-100 hover:bg-blue-400 hover:text-white p-2 rounded transition"
|
||||
title="Show QR Code" @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"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Download Config -->
|
||||
<a :href="'/api/wireguard/client/' + client.id + '/configuration'" download
|
||||
class="align-middle inline-block bg-gray-100 hover:bg-blue-400 hover:text-white p-2 rounded transition"
|
||||
title="Download Configuration">
|
||||
<svg class="w-5" 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"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Delete -->
|
||||
<button class="align-middle bg-gray-100 hover:bg-red-400 hover:text-white p-2 rounded transition"
|
||||
title="Delete Client" @click="clientDelete = client">
|
||||
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="clients && clients.length === 0">
|
||||
<p class="text-center m-10 text-gray-400 text-sm">There are no clients yet.<br /><br />
|
||||
<button @click="clientCreate = true; clientCreateName = '';"
|
||||
class="bg-green-400 text-white hover:bg-green-500 border-2 border-none py-2 px-4 rounded inline-flex items-center transition">
|
||||
<svg class="w-4 mr-2" 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"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span class="text-sm">New Client</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="clients === null" class="text-gray-200 p-5">
|
||||
<svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="currentColor">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code-->
|
||||
<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">
|
||||
<div class="bg-white rounded-md shadow-lg relative p-8">
|
||||
<button @click="qrcode = null" class="absolute right-4 top-4 text-gray-600 hover:text-gray-800">
|
||||
<svg class="w-8" 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" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<img :src="qrcode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<div v-if="clientCreate" class="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div
|
||||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg class="h-6 w-6 text-green-600" 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"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
New Client
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
<input class="rounded p-2 border-2 border-gray-100 focus:border-green-200 outline-none w-full"
|
||||
type="text" v-model.trim="clientCreateName" placeholder="Name" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button v-if="clientCreateName.length" type="button" @click="createClient(); clientCreate = null"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-400 text-base font-medium text-white hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Create
|
||||
</button>
|
||||
<button v-else type="button"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-200 text-base font-medium text-white sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed">
|
||||
Create
|
||||
</button>
|
||||
<button type="button" @click="clientCreate = null"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Dialog -->
|
||||
<div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!--
|
||||
Background overlay, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0"
|
||||
To: "opacity-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100"
|
||||
To: "opacity-0"
|
||||
-->
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<!--
|
||||
Modal panel, show/hide based on modal state.
|
||||
|
||||
Entering: "ease-out duration-300"
|
||||
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||
Leaving: "ease-in duration-200"
|
||||
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
-->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div
|
||||
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||
Delete Client
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Are you sure you want to delete <strong>{{clientDelete.name}}</strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" @click="deleteClient(clientDelete); clientDelete = null"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Delete
|
||||
</button>
|
||||
<button type="button" @click="clientDelete = null"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="authenticated === false">
|
||||
<h1 class="text-4xl font-medium my-16 text-gray-700 text-center">WireGuard</h1>
|
||||
|
||||
<form @submit="login" class="shadow rounded-md bg-white mx-auto w-64 p-5 overflow-hidden mt-10">
|
||||
<!-- Avatar -->
|
||||
<div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-gray-100 relative overflow-hidden">
|
||||
<svg class="w-10 h-10 m-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input type="password" name="password" placeholder="password" v-model="password"
|
||||
class="px-2 py-1 text-sm text-gray-500 mb-5 border-2 border-gray-100 rounded-lg w-full focus:border-green-400 outline-none" />
|
||||
|
||||
<button v-if="authenticating"
|
||||
class="bg-green-400 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed">
|
||||
<svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="currentColor">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<input v-if="!authenticating && password" type="submit"
|
||||
class="bg-green-400 w-full rounded shadow py-2 text-sm text-white hover:bg-green-500 transition cursor-pointer"
|
||||
value="Sign In">
|
||||
<input v-if="!authenticating && !password" type="submit"
|
||||
class="bg-gray-200 w-full rounded shadow py-2 text-sm text-white cursor-not-allowed" value="Sign In">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="authenticated === null" class="text-gray-300 pt-24 pb-12">
|
||||
|
||||
<svg class="w-5 animate-spin mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="text-center m-10 text-gray-300 text-xs">Made by <a target="_blank" class="hover:underline"
|
||||
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> · <a class="hover:underline"
|
||||
href="https://github.com/weejewel/wg-easy" target="_blank">GitHub</a></p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/vendor/vue.min.js"></script>
|
||||
<script src="/js/vendor/md5.min.js"></script>
|
||||
<script src="/js/vendor/timeago.min.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
90
src/www/js/api.js
Normal file
90
src/www/js/api.js
Normal file
@ -0,0 +1,90 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
'use strict';
|
||||
|
||||
class API {
|
||||
|
||||
async call({ method, path, body }) {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body
|
||||
? JSON.stringify(body)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(json.error || res.statusText);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
return this.call({
|
||||
method: 'get',
|
||||
path: '/session',
|
||||
});
|
||||
}
|
||||
|
||||
async createSession({ password }) {
|
||||
return this.call({
|
||||
method: 'post',
|
||||
path: '/session',
|
||||
body: { password },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSession() {
|
||||
return this.call({
|
||||
method: 'delete',
|
||||
path: '/session',
|
||||
});
|
||||
}
|
||||
|
||||
async getClients() {
|
||||
return this.call({
|
||||
method: 'get',
|
||||
path: '/wireguard/client',
|
||||
});
|
||||
}
|
||||
|
||||
async createClient({ name }) {
|
||||
return this.call({
|
||||
method: 'post',
|
||||
path: '/wireguard/client',
|
||||
body: { name },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteClient({ clientId }) {
|
||||
return this.call({
|
||||
method: 'delete',
|
||||
path: `/wireguard/client/${clientId}`,
|
||||
});
|
||||
}
|
||||
|
||||
async enableClient({ clientId }) {
|
||||
return this.call({
|
||||
method: 'post',
|
||||
path: `/wireguard/client/${clientId}/enable`,
|
||||
});
|
||||
}
|
||||
|
||||
async disableClient({ clientId }) {
|
||||
return this.call({
|
||||
method: 'post',
|
||||
path: `/wireguard/client/${clientId}/disable`,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
140
src/www/js/app.js
Normal file
140
src/www/js/app.js
Normal file
@ -0,0 +1,140 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-alert */
|
||||
/* eslint-disable no-undef */
|
||||
/* eslint-disable no-new */
|
||||
|
||||
'use strict';
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
authenticated: null,
|
||||
authenticating: false,
|
||||
password: null,
|
||||
|
||||
clients: null,
|
||||
clientDelete: null,
|
||||
clientCreate: null,
|
||||
clientCreateName: '',
|
||||
qrcode: null,
|
||||
},
|
||||
methods: {
|
||||
dateTime: value => {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}).format(value);
|
||||
},
|
||||
async refresh() {
|
||||
if (!this.authenticated) return;
|
||||
|
||||
const clients = await this.api.getClients();
|
||||
this.clients = clients.map(client => {
|
||||
if (client.name.includes('@') && client.name.includes('.')) {
|
||||
client.avatar = `https://www.gravatar.com/avatar/${md5(client.name)}?d=blank`;
|
||||
}
|
||||
|
||||
return client;
|
||||
});
|
||||
},
|
||||
login(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.password) return;
|
||||
if (this.authenticating) return;
|
||||
|
||||
this.authenticating = true;
|
||||
this.api.createSession({
|
||||
password: this.password,
|
||||
})
|
||||
.then(async () => {
|
||||
const session = await this.api.getSession();
|
||||
this.authenticated = session.authenticated;
|
||||
return this.refresh();
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message || err.toString());
|
||||
})
|
||||
.finally(() => {
|
||||
this.authenticating = false;
|
||||
});
|
||||
},
|
||||
logout(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.api.deleteSession()
|
||||
.then(() => {
|
||||
this.authenticated = false;
|
||||
this.clients = null;
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message || err.toString());
|
||||
});
|
||||
},
|
||||
createClient() {
|
||||
const name = this.clientCreateName;
|
||||
if (!name) return;
|
||||
|
||||
this.api.createClient({ name })
|
||||
.catch(err => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
},
|
||||
deleteClient(clientId) {
|
||||
this.api.deleteClient({ clientId: client.id })
|
||||
.catch(err => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
},
|
||||
enableClient(client) {
|
||||
this.api.enableClient({ clientId: client.id })
|
||||
.catch(err => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
},
|
||||
disableClient(client) {
|
||||
this.api.disableClient({ clientId: client.id })
|
||||
.catch(err => alert(err.message || err.toString()))
|
||||
.finally(() => this.refresh().catch(console.error));
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
timeago: value => {
|
||||
return timeago().format(value);
|
||||
},
|
||||
bytes: (bytes, decimals, kib, maxunit) => {
|
||||
kib = kib || false;
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'Not an number';
|
||||
const k = kib ? 1024 : 1000;
|
||||
const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2;
|
||||
const sizes = kib
|
||||
? ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB']
|
||||
: ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
|
||||
let i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
if (maxunit !== undefined) {
|
||||
const index = sizes.indexOf(maxunit);
|
||||
if (index !== -1) i = index;
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.api = new API();
|
||||
this.api.getSession()
|
||||
.then(session => {
|
||||
this.authenticated = session.authenticated;
|
||||
this.refresh().catch(err => {
|
||||
alert(err.message || err.toString());
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message || err.toString());
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
this.refresh().catch(console.error);
|
||||
}, 1000);
|
||||
},
|
||||
});
|
1
src/www/js/vendor/md5.min.js
vendored
Normal file
1
src/www/js/vendor/md5.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<<t%32,n[14+(t+64>>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h<n.length;h+=16)c=l(r=c,e=f,o=i,u=a,n[h],7,-680876936),a=l(a,c,f,i,n[h+1],12,-389564586),i=l(i,a,c,f,n[h+2],17,606105819),f=l(f,i,a,c,n[h+3],22,-1044525330),c=l(c,f,i,a,n[h+4],7,-176418897),a=l(a,c,f,i,n[h+5],12,1200080426),i=l(i,a,c,f,n[h+6],17,-1473231341),f=l(f,i,a,c,n[h+7],22,-45705983),c=l(c,f,i,a,n[h+8],7,1770035416),a=l(a,c,f,i,n[h+9],12,-1958414417),i=l(i,a,c,f,n[h+10],17,-42063),f=l(f,i,a,c,n[h+11],22,-1990404162),c=l(c,f,i,a,n[h+12],7,1804603682),a=l(a,c,f,i,n[h+13],12,-40341101),i=l(i,a,c,f,n[h+14],17,-1502002290),c=v(c,f=l(f,i,a,c,n[h+15],22,1236535329),i,a,n[h+1],5,-165796510),a=v(a,c,f,i,n[h+6],9,-1069501632),i=v(i,a,c,f,n[h+11],14,643717713),f=v(f,i,a,c,n[h],20,-373897302),c=v(c,f,i,a,n[h+5],5,-701558691),a=v(a,c,f,i,n[h+10],9,38016083),i=v(i,a,c,f,n[h+15],14,-660478335),f=v(f,i,a,c,n[h+4],20,-405537848),c=v(c,f,i,a,n[h+9],5,568446438),a=v(a,c,f,i,n[h+14],9,-1019803690),i=v(i,a,c,f,n[h+3],14,-187363961),f=v(f,i,a,c,n[h+8],20,1163531501),c=v(c,f,i,a,n[h+13],5,-1444681467),a=v(a,c,f,i,n[h+2],9,-51403784),i=v(i,a,c,f,n[h+7],14,1735328473),c=g(c,f=v(f,i,a,c,n[h+12],20,-1926607734),i,a,n[h+5],4,-378558),a=g(a,c,f,i,n[h+8],11,-2022574463),i=g(i,a,c,f,n[h+11],16,1839030562),f=g(f,i,a,c,n[h+14],23,-35309556),c=g(c,f,i,a,n[h+1],4,-1530992060),a=g(a,c,f,i,n[h+4],11,1272893353),i=g(i,a,c,f,n[h+7],16,-155497632),f=g(f,i,a,c,n[h+10],23,-1094730640),c=g(c,f,i,a,n[h+13],4,681279174),a=g(a,c,f,i,n[h],11,-358537222),i=g(i,a,c,f,n[h+3],16,-722521979),f=g(f,i,a,c,n[h+6],23,76029189),c=g(c,f,i,a,n[h+9],4,-640364487),a=g(a,c,f,i,n[h+12],11,-421815835),i=g(i,a,c,f,n[h+15],16,530742520),c=m(c,f=g(f,i,a,c,n[h+2],23,-995338651),i,a,n[h],6,-198630844),a=m(a,c,f,i,n[h+7],10,1126891415),i=m(i,a,c,f,n[h+14],15,-1416354905),f=m(f,i,a,c,n[h+5],21,-57434055),c=m(c,f,i,a,n[h+12],6,1700485571),a=m(a,c,f,i,n[h+3],10,-1894986606),i=m(i,a,c,f,n[h+10],15,-1051523),f=m(f,i,a,c,n[h+1],21,-2054922799),c=m(c,f,i,a,n[h+8],6,1873313359),a=m(a,c,f,i,n[h+15],10,-30611744),i=m(i,a,c,f,n[h+6],15,-1560198380),f=m(f,i,a,c,n[h+13],21,1309151649),c=m(c,f,i,a,n[h+4],6,-145523070),a=m(a,c,f,i,n[h+11],10,-1120210379),i=m(i,a,c,f,n[h+2],15,718787259),f=m(f,i,a,c,n[h+9],21,-343485551),c=d(c,r),f=d(f,e),i=d(i,o),a=d(a,u);return[c,f,i,a]}function a(n){for(var t="",r=32*n.length,e=0;e<r;e+=8)t+=String.fromCharCode(n[e>>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e<t.length;e+=1)t[e]=0;for(var r=8*n.length,e=0;e<r;e+=8)t[e>>5]|=(255&n.charCodeAt(e/8))<<e%32;return t}function e(n){for(var t,r="0123456789abcdef",e="",o=0;o<n.length;o+=1)t=n.charCodeAt(o),e+=r.charAt(t>>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16<o.length&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}(r(n),r(t))}function t(n,t,r){return t?r?u(t,n):e(u(t,n)):r?o(n):e(o(n))}"function"==typeof define&&define.amd?define(function(){return t}):"object"==typeof module&&module.exports?module.exports=t:n.md5=t}(this);
|
1
src/www/js/vendor/timeago.min.js
vendored
Normal file
1
src/www/js/vendor/timeago.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(t,e){"object"==typeof module&&module.exports?module.exports=e(t):t.timeago=e(t)}("undefined"!=typeof window?window:this,function(){function t(t){return t instanceof Date?t:isNaN(t)?/^\d+$/.test(t)?new Date(e(t,10)):(t=(t||"").trim().replace(/\.\d+/,"").replace(/-/,"/").replace(/-/,"/").replace(/T/," ").replace(/Z/," UTC").replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"),new Date(t)):new Date(e(t))}function e(t){return parseInt(t)}function n(t,n,r){n=d[n]?n:d[r]?r:"en";var i=0;for(agoin=t<0?1:0,t=Math.abs(t);t>=l[i]&&i<p;i++)t/=l[i];return t=e(t),i*=2,t>(0===i?9:1)&&(i+=1),d[n](t,i)[agoin].replace("%s",t)}function r(e,n){return n=n?t(n):new Date,(n-t(e))/1e3}function i(t){for(var e=1,n=0,r=Math.abs(t);t>=l[n]&&n<p;n++)t/=l[n],e*=l[n];return r%=e,r=r?e-r:e,Math.ceil(r)}function o(t){return t.getAttribute?t.getAttribute(h):t.attr?t.attr(h):void 0}function a(t,e){function a(o,c,f,s){var d=r(c,t);o.innerHTML=n(d,f,e),u["k"+s]=setTimeout(function(){a(o,c,f,s)},1e3*i(d))}var u={};return e||(e="en"),this.format=function(i,o){return n(r(i,t),o,e)},this.render=function(t,e){void 0===t.length&&(t=[t]);for(var n=0;n<t.length;n++)a(t[n],o(t[n]),e,++c)},this.cancel=function(){for(var t in u)clearTimeout(u[t]);u={}},this.setLocale=function(t){e=t},this}function u(t,e){return new a(t,e)}var c=0,f="second_minute_hour_day_week_month_year".split("_"),s="秒_分钟_小时_天_周_月_年".split("_"),d={en:function(t,e){if(0===e)return["just now","right now"];var n=f[parseInt(e/2)];return t>1&&(n+="s"),[t+" "+n+" ago","in "+t+" "+n]},zh_CN:function(t,e){if(0===e)return["刚刚","片刻后"];var n=s[parseInt(e/2)];return[t+n+"前",t+n+"后"]}},l=[60,60,24,7,365/7/12,12],p=6,h="datetime";return u.register=function(t,e){d[t]=e},u});
|
6
src/www/js/vendor/vue.min.js
vendored
Normal file
6
src/www/js/vendor/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
src/www/manifest.json
Normal file
11
src/www/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "WireGuard",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "img/favicon.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user