mirror of https://github.com/wg-easy/wg-easy
25 changed files with 4632 additions and 2 deletions
Binary file not shown.
@ -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] |
[Interface] |
||||
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o= # wg genkey > privatekey |
PrivateKey = iOQJS7OUUGPYATsX6nqlL+sOODoiWiN5IOE8Msfw/0o= |
||||
# PublicKey = BkdntwYazhYZzEEHhcYayq6TGw9/YUDQ251s+5bTgC0= |
|
||||
Address = 10.8.0.1/24 |
Address = 10.8.0.1/24 |
||||
ListenPort = 51820 |
ListenPort = 51820 |
||||
DNS = 1.1.1.1 |
DNS = 1.1.1.1 |
||||
|
|
||||
|
# Client: Emile (af3111a4-7343-4380-a293-ed498d9aa3b8) |
||||
[Peer] |
[Peer] |
||||
PublicKey = i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4= |
PublicKey = i8xWKqicnDkNL14I4B+I1zlB8od/booA1joIosWn7X4= |
||||
PresharedKey = MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU= |
PresharedKey = MzplKtOQ44/IaAKri2VKqCoIlg4XiVH7TCp5bcYRTQU= |
||||
|
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"extends": "athom", |
||||
|
"rules": { |
||||
|
"consistent-return": "off", |
||||
|
"no-shadow": "off" |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
/node_modules |
@ -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'; |
@ -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}`); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
}; |
@ -0,0 +1,10 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
module.exports = class ServerError extends Error { |
||||
|
|
||||
|
constructor(message, statusCode = 500) { |
||||
|
super(message); |
||||
|
this.statusCode = statusCode; |
||||
|
} |
||||
|
|
||||
|
}; |
@ -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, |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
}; |
@ -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(); |
||||
|
} |
||||
|
|
||||
|
}; |
File diff suppressed because it is too large
@ -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" |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
require('./services/Server'); |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const Server = require('../lib/Server'); |
||||
|
|
||||
|
module.exports = new Server(); |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const WireGuard = require('../lib/WireGuard'); |
||||
|
|
||||
|
module.exports = new WireGuard(); |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 90 KiB |
@ -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> |
@ -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`, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
@ -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); |
||||
|
}, |
||||
|
}); |
@ -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); |
@ -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}); |
File diff suppressed because one or more lines are too long
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"name": "WireGuard", |
||||
|
"display": "standalone", |
||||
|
"background_color": "#fff", |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "img/favicon.png", |
||||
|
"type": "image/png" |
||||
|
} |
||||
|
] |
||||
|
} |
Loading…
Reference in new issue