mirror of https://github.com/wg-easy/wg-easy
52 changed files with 5073 additions and 12756 deletions
@ -1,11 +0,0 @@ |
|||
{ |
|||
"extends": "athom", |
|||
"ignorePatterns": [ |
|||
"**/vendor/*.js" |
|||
], |
|||
"rules": { |
|||
"consistent-return": "off", |
|||
"no-shadow": "off", |
|||
"max-len": "off" |
|||
} |
|||
} |
@ -1,39 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const { release: { version } } = require('./package.json'); |
|||
|
|||
module.exports.RELEASE = version; |
|||
module.exports.PORT = process.env.PORT || '51821'; |
|||
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; |
|||
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH; |
|||
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/'; |
|||
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0'; |
|||
module.exports.WG_HOST = process.env.WG_HOST; |
|||
module.exports.WG_PORT = process.env.WG_PORT || '51820'; |
|||
module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820'; |
|||
module.exports.WG_MTU = process.env.WG_MTU || null; |
|||
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0'; |
|||
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x'; |
|||
module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string' |
|||
? process.env.WG_DEFAULT_DNS |
|||
: '1.1.1.1'; |
|||
module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; |
|||
|
|||
module.exports.WG_PRE_UP = process.env.WG_PRE_UP || ''; |
|||
module.exports.WG_POST_UP = process.env.WG_POST_UP || ` |
|||
iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; |
|||
iptables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; |
|||
iptables -A FORWARD -i wg0 -j ACCEPT; |
|||
iptables -A FORWARD -o wg0 -j ACCEPT; |
|||
`.split('\n').join(' ');
|
|||
|
|||
module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; |
|||
module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ` |
|||
iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; |
|||
iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; |
|||
iptables -D FORWARD -i wg0 -j ACCEPT; |
|||
iptables -D FORWARD -o wg0 -j ACCEPT; |
|||
`.split('\n').join(' ');
|
|||
module.exports.LANG = process.env.LANG || 'en'; |
|||
module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false'; |
|||
module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; |
@ -1,313 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const bcrypt = require('bcryptjs'); |
|||
const crypto = require('node:crypto'); |
|||
const { createServer } = require('node:http'); |
|||
const { stat, readFile } = require('node:fs/promises'); |
|||
const { resolve, sep } = require('node:path'); |
|||
|
|||
const expressSession = require('express-session'); |
|||
const debug = require('debug')('Server'); |
|||
|
|||
const { |
|||
createApp, |
|||
createError, |
|||
createRouter, |
|||
defineEventHandler, |
|||
fromNodeMiddleware, |
|||
getRouterParam, |
|||
toNodeListener, |
|||
readBody, |
|||
setHeader, |
|||
serveStatic, |
|||
} = require('h3'); |
|||
|
|||
const WireGuard = require('../services/WireGuard'); |
|||
|
|||
const { |
|||
PORT, |
|||
WEBUI_HOST, |
|||
RELEASE, |
|||
PASSWORD_HASH, |
|||
LANG, |
|||
UI_TRAFFIC_STATS, |
|||
UI_CHART_TYPE, |
|||
} = require('../config'); |
|||
|
|||
const requiresPassword = !!PASSWORD_HASH; |
|||
|
|||
/** |
|||
* Checks if `password` matches the PASSWORD_HASH. |
|||
* |
|||
* If environment variable is not set, the password is always invalid. |
|||
* |
|||
* @param {string} password String to test |
|||
* @returns {boolean} true if matching environment, otherwise false |
|||
*/ |
|||
const isPasswordValid = (password) => { |
|||
if (typeof password !== 'string') { |
|||
return false; |
|||
} |
|||
|
|||
if (PASSWORD_HASH) { |
|||
return bcrypt.compareSync(password, PASSWORD_HASH); |
|||
} |
|||
|
|||
return false; |
|||
}; |
|||
|
|||
module.exports = class Server { |
|||
|
|||
constructor() { |
|||
const app = createApp(); |
|||
this.app = app; |
|||
|
|||
app.use(fromNodeMiddleware(expressSession({ |
|||
secret: crypto.randomBytes(256).toString('hex'), |
|||
resave: true, |
|||
saveUninitialized: true, |
|||
}))); |
|||
|
|||
const router = createRouter(); |
|||
app.use(router); |
|||
|
|||
router |
|||
.get('/api/release', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return RELEASE; |
|||
})) |
|||
|
|||
.get('/api/lang', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${LANG}"`; |
|||
})) |
|||
|
|||
.get('/api/ui-traffic-stats', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${UI_TRAFFIC_STATS}"`; |
|||
})) |
|||
|
|||
.get('/api/ui-chart-type', defineEventHandler((event) => { |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${UI_CHART_TYPE}"`; |
|||
})) |
|||
|
|||
// Authentication
|
|||
.get('/api/session', defineEventHandler((event) => { |
|||
const authenticated = requiresPassword |
|||
? !!(event.node.req.session && event.node.req.session.authenticated) |
|||
: true; |
|||
|
|||
return { |
|||
requiresPassword, |
|||
authenticated, |
|||
}; |
|||
})) |
|||
.post('/api/session', defineEventHandler(async (event) => { |
|||
const { password } = await readBody(event); |
|||
|
|||
if (!requiresPassword) { |
|||
// if no password is required, the API should never be called.
|
|||
// Do not automatically authenticate the user.
|
|||
throw createError({ |
|||
status: 401, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
|
|||
if (!isPasswordValid(password)) { |
|||
throw createError({ |
|||
status: 401, |
|||
message: 'Incorrect Password', |
|||
}); |
|||
} |
|||
|
|||
event.node.req.session.authenticated = true; |
|||
event.node.req.session.save(); |
|||
|
|||
debug(`New Session: ${event.node.req.session.id}`); |
|||
|
|||
return { success: true }; |
|||
})); |
|||
|
|||
// WireGuard
|
|||
app.use( |
|||
fromNodeMiddleware((req, res, next) => { |
|||
if (!requiresPassword || !req.url.startsWith('/api/')) { |
|||
return next(); |
|||
} |
|||
|
|||
if (req.session && req.session.authenticated) { |
|||
return next(); |
|||
} |
|||
|
|||
if (req.url.startsWith('/api/') && req.headers['authorization']) { |
|||
if (isPasswordValid(req.headers['authorization'])) { |
|||
return next(); |
|||
} |
|||
return res.status(401).json({ |
|||
error: 'Incorrect Password', |
|||
}); |
|||
} |
|||
|
|||
return res.status(401).json({ |
|||
error: 'Not Logged In', |
|||
}); |
|||
}), |
|||
); |
|||
|
|||
const router2 = createRouter(); |
|||
app.use(router2); |
|||
|
|||
router2 |
|||
.delete('/api/session', defineEventHandler((event) => { |
|||
const sessionId = event.node.req.session.id; |
|||
|
|||
event.node.req.session.destroy(); |
|||
|
|||
debug(`Deleted Session: ${sessionId}`); |
|||
return { success: true }; |
|||
})) |
|||
.get('/api/wireguard/client', defineEventHandler(() => { |
|||
return WireGuard.getClients(); |
|||
})) |
|||
.get('/api/wireguard/client/:clientId/qrcode.svg', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
const svg = await WireGuard.getClientQRCodeSVG({ clientId }); |
|||
setHeader(event, 'Content-Type', 'image/svg+xml'); |
|||
return svg; |
|||
})) |
|||
.get('/api/wireguard/client/:clientId/configuration', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
const client = await WireGuard.getClient({ clientId }); |
|||
const config = await WireGuard.getClientConfiguration({ clientId }); |
|||
const configName = client.name |
|||
.replace(/[^a-zA-Z0-9_=+.-]/g, '-') |
|||
.replace(/(-{2,}|-$)/g, '-') |
|||
.replace(/-$/, '') |
|||
.substring(0, 32); |
|||
setHeader(event, 'Content-Disposition', `attachment; filename="${configName || clientId}.conf"`); |
|||
setHeader(event, 'Content-Type', 'text/plain'); |
|||
return config; |
|||
})) |
|||
.post('/api/wireguard/client', defineEventHandler(async (event) => { |
|||
const { name } = await readBody(event); |
|||
await WireGuard.createClient({ name }); |
|||
return { success: true }; |
|||
})) |
|||
.delete('/api/wireguard/client/:clientId', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
await WireGuard.deleteClient({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.post('/api/wireguard/client/:clientId/enable', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
await WireGuard.enableClient({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.post('/api/wireguard/client/:clientId/disable', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
await WireGuard.disableClient({ clientId }); |
|||
return { success: true }; |
|||
})) |
|||
.put('/api/wireguard/client/:clientId/name', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
const { name } = await readBody(event); |
|||
await WireGuard.updateClientName({ clientId, name }); |
|||
return { success: true }; |
|||
})) |
|||
.put('/api/wireguard/client/:clientId/address', defineEventHandler(async (event) => { |
|||
const clientId = getRouterParam(event, 'clientId'); |
|||
if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { |
|||
throw createError({ status: 403 }); |
|||
} |
|||
const { address } = await readBody(event); |
|||
await WireGuard.updateClientAddress({ clientId, address }); |
|||
return { success: true }; |
|||
})); |
|||
|
|||
const safePathJoin = (base, target) => { |
|||
// Manage web root (edge case)
|
|||
if (target === '/') { |
|||
return `${base}${sep}`; |
|||
} |
|||
|
|||
// Prepend './' to prevent absolute paths
|
|||
const targetPath = `.${sep}${target}`; |
|||
|
|||
// Resolve the absolute path
|
|||
const resolvedPath = resolve(base, targetPath); |
|||
|
|||
// Check if resolvedPath is a subpath of base
|
|||
if (resolvedPath.startsWith(`${base}${sep}`)) { |
|||
return resolvedPath; |
|||
} |
|||
|
|||
throw createError({ |
|||
status: 400, |
|||
message: 'Bad Request', |
|||
}); |
|||
}; |
|||
|
|||
// backup_restore
|
|||
const router3 = createRouter(); |
|||
app.use(router3); |
|||
|
|||
router3 |
|||
.get('/api/wireguard/backup', defineEventHandler(async (event) => { |
|||
const config = await WireGuard.backupConfiguration(); |
|||
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"'); |
|||
setHeader(event, 'Content-Type', 'text/json'); |
|||
return config; |
|||
})) |
|||
.put('/api/wireguard/restore', defineEventHandler(async (event) => { |
|||
const { file } = await readBody(event); |
|||
await WireGuard.restoreConfiguration(file); |
|||
return { success: true }; |
|||
})); |
|||
|
|||
// Static assets
|
|||
const publicDir = '/app/www'; |
|||
app.use( |
|||
defineEventHandler((event) => { |
|||
return serveStatic(event, { |
|||
getContents: (id) => { |
|||
return readFile(safePathJoin(publicDir, id)); |
|||
}, |
|||
getMeta: async (id) => { |
|||
const filePath = safePathJoin(publicDir, id); |
|||
|
|||
const stats = await stat(filePath).catch(() => {}); |
|||
if (!stats || !stats.isFile()) { |
|||
return; |
|||
} |
|||
|
|||
if (id.endsWith('.html')) setHeader(event, 'Content-Type', 'text/html'); |
|||
if (id.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript'); |
|||
if (id.endsWith('.json')) setHeader(event, 'Content-Type', 'application/json'); |
|||
if (id.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css'); |
|||
if (id.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png'); |
|||
|
|||
return { |
|||
size: stats.size, |
|||
mtime: stats.mtimeMs, |
|||
}; |
|||
}, |
|||
}); |
|||
}), |
|||
); |
|||
|
|||
createServer(toNodeListener(app)).listen(PORT, WEBUI_HOST); |
|||
debug(`Listening on http://${WEBUI_HOST}:${PORT}`); |
|||
} |
|||
|
|||
}; |
@ -1,10 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
module.exports = class ServerError extends Error { |
|||
|
|||
constructor(message, statusCode = 500) { |
|||
super(message); |
|||
this.statusCode = statusCode; |
|||
} |
|||
|
|||
}; |
@ -1,80 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const childProcess = require('child_process'); |
|||
|
|||
module.exports = class Util { |
|||
|
|||
static isValidIPv4(str) { |
|||
const blocks = str.split('.'); |
|||
if (blocks.length !== 4) return false; |
|||
|
|||
for (let value of blocks) { |
|||
value = parseInt(value, 10); |
|||
if (Number.isNaN(value)) return false; |
|||
if (value < 0 || value > 255) return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static promisify(fn) { |
|||
// eslint-disable-next-line func-names
|
|||
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); |
|||
} |
|||
|
|||
// eslint-disable-next-line no-console
|
|||
console.error(error); |
|||
|
|||
return res |
|||
.status(error.statusCode || 500) |
|||
.json({ |
|||
error: error.message || error.toString(), |
|||
stack: error.stack, |
|||
}); |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
static async exec(cmd, { |
|||
log = true, |
|||
} = {}) { |
|||
if (typeof log === 'string') { |
|||
// eslint-disable-next-line no-console
|
|||
console.log(`$ ${log}`); |
|||
} else if (log === true) { |
|||
// eslint-disable-next-line no-console
|
|||
console.log(`$ ${cmd}`); |
|||
} |
|||
|
|||
if (process.platform !== 'linux') { |
|||
return ''; |
|||
} |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
childProcess.exec(cmd, { |
|||
shell: 'bash', |
|||
}, (err, stdout) => { |
|||
if (err) return reject(err); |
|||
return resolve(String(stdout).trim()); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
}; |
@ -0,0 +1,5 @@ |
|||
export default defineNuxtRouteMiddleware(async (to, from) => { |
|||
if (REQUIRES_PASSWORD || !to.path.startsWith('/api/')) { |
|||
return abortNavigation(); |
|||
} |
|||
}) |
File diff suppressed because it is too large
File diff suppressed because it is too large
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -1,29 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
require('./services/Server'); |
|||
|
|||
const WireGuard = require('./services/WireGuard'); |
|||
|
|||
WireGuard.getConfig() |
|||
.catch((err) => { |
|||
// eslint-disable-next-line no-console
|
|||
console.error(err); |
|||
|
|||
// eslint-disable-next-line no-process-exit
|
|||
process.exit(1); |
|||
}); |
|||
|
|||
// Handle terminate signal
|
|||
process.on('SIGTERM', async () => { |
|||
// eslint-disable-next-line no-console
|
|||
console.log('SIGTERM signal received.'); |
|||
await WireGuard.Shutdown(); |
|||
// eslint-disable-next-line no-process-exit
|
|||
process.exit(0); |
|||
}); |
|||
|
|||
// Handle interrupt signal
|
|||
process.on('SIGINT', () => { |
|||
// eslint-disable-next-line no-console
|
|||
console.log('SIGINT signal received.'); |
|||
}); |
@ -0,0 +1,6 @@ |
|||
export default defineEventHandler((event) => { |
|||
assertMethod(event, "GET"); |
|||
const {LANG} = useRuntimeConfig(); |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${LANG}"`; |
|||
}) |
@ -0,0 +1,6 @@ |
|||
export default defineEventHandler((event) => { |
|||
assertMethod(event, "GET"); |
|||
const {RELEASE} = useRuntimeConfig(); |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return RELEASE; |
|||
}) |
@ -0,0 +1,51 @@ |
|||
import { REQUIRES_PASSWORD, SERVER_DEBUG, SESSION_CONFIG } from "~/utils/config"; |
|||
import { isPasswordValid } from "~/utils/password"; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
if (isMethod(event, "GET")) { |
|||
const session = await useSession(event, SESSION_CONFIG); |
|||
const authenticated = REQUIRES_PASSWORD |
|||
? !!(session.data && session.data.authenticated) |
|||
: true; |
|||
|
|||
return { |
|||
REQUIRES_PASSWORD, |
|||
authenticated, |
|||
}; |
|||
} else if (isMethod(event, "POST")) { |
|||
const session = await useSession(event, SESSION_CONFIG); |
|||
const { password } = await readBody(event); |
|||
|
|||
if (!REQUIRES_PASSWORD) { |
|||
// if no password is required, the API should never be called.
|
|||
// Do not automatically authenticate the user.
|
|||
throw createError({ |
|||
status: 401, |
|||
message: 'Invalid state', |
|||
}); |
|||
} |
|||
|
|||
if (!isPasswordValid(password)) { |
|||
throw createError({ |
|||
status: 401, |
|||
message: 'Incorrect Password', |
|||
}); |
|||
} |
|||
|
|||
const data = await session.update({ |
|||
authenticated: true |
|||
}); |
|||
|
|||
SERVER_DEBUG(`New Session: ${data.id}`); |
|||
|
|||
return { success: true }; |
|||
} else if (isMethod(event, "DELETE")) { |
|||
const session = await useSession(event, SESSION_CONFIG); |
|||
const sessionId = session.id; |
|||
|
|||
await session.clear(); |
|||
|
|||
SERVER_DEBUG(`Deleted Session: ${sessionId}`); |
|||
return { success: true }; |
|||
} |
|||
}) |
@ -0,0 +1,6 @@ |
|||
export default defineEventHandler((event) => { |
|||
assertMethod(event, "GET"); |
|||
const {UI_CHART_TYPE} = useRuntimeConfig(); |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${UI_CHART_TYPE}"`; |
|||
}) |
@ -0,0 +1,6 @@ |
|||
export default defineEventHandler((event) => { |
|||
assertMethod(event, "GET"); |
|||
const {UI_TRAFFIC_STATS} = useRuntimeConfig(); |
|||
setHeader(event, 'Content-Type', 'application/json'); |
|||
return `"${UI_TRAFFIC_STATS}"`; |
|||
}) |
@ -0,0 +1,5 @@ |
|||
import WireGuard from "~/utils/WireGuard"; |
|||
|
|||
export default defineEventHandler(() => { |
|||
return WireGuard.getClients(); |
|||
}) |
@ -1,5 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const Server = require('../lib/Server'); |
|||
|
|||
module.exports = new Server(); |
@ -1,5 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const WireGuard = require('../lib/WireGuard'); |
|||
|
|||
module.exports = new WireGuard(); |
@ -0,0 +1,26 @@ |
|||
import childProcess from 'child_process'; |
|||
|
|||
export function exec(cmd, { |
|||
log = true, |
|||
} = {}) { |
|||
if (typeof log === 'string') { |
|||
// eslint-disable-next-line no-console
|
|||
console.log(`$ ${log}`); |
|||
} else if (log === true) { |
|||
// eslint-disable-next-line no-console
|
|||
console.log(`$ ${cmd}`); |
|||
} |
|||
|
|||
if (process.platform !== 'linux') { |
|||
return Promise.resolve(""); |
|||
} |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
childProcess.exec(cmd, { |
|||
shell: 'bash', |
|||
}, (err, stdout) => { |
|||
if (err) return reject(err); |
|||
return resolve(String(stdout).trim()); |
|||
}); |
|||
}); |
|||
} |
@ -0,0 +1,50 @@ |
|||
import type {SessionConfig} from 'h3'; |
|||
|
|||
import {getRandomHex} from '~/utils/crypto' |
|||
import packageJSON from '../package.json'; |
|||
import debug from 'debug'; |
|||
const version = packageJSON.release.version; |
|||
|
|||
export const RELEASE = version; |
|||
export const PORT = process.env.PORT || '51821'; |
|||
export const WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0'; |
|||
export const PASSWORD_HASH = process.env.PASSWORD_HASH; |
|||
export const WG_PATH = process.env.WG_PATH || '/etc/wireguard/'; |
|||
export const WG_DEVICE = process.env.WG_DEVICE || 'eth0'; |
|||
export const WG_HOST = process.env.WG_HOST; |
|||
export const WG_PORT = process.env.WG_PORT || '51820'; |
|||
export const WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820'; |
|||
export const WG_MTU = process.env.WG_MTU || null; |
|||
export const WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0'; |
|||
export const WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x'; |
|||
export const WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string' |
|||
? process.env.WG_DEFAULT_DNS |
|||
: '1.1.1.1'; |
|||
export const WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; |
|||
|
|||
export const WG_PRE_UP = process.env.WG_PRE_UP || ''; |
|||
export const WG_POST_UP = process.env.WG_POST_UP || ` |
|||
iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${WG_DEVICE} -j MASQUERADE; |
|||
iptables -A INPUT -p udp -m udp --dport ${WG_PORT} -j ACCEPT; |
|||
iptables -A FORWARD -i wg0 -j ACCEPT; |
|||
iptables -A FORWARD -o wg0 -j ACCEPT; |
|||
`.split('\n').join(' ');
|
|||
|
|||
export const WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; |
|||
export const WG_POST_DOWN = process.env.WG_POST_DOWN || ` |
|||
iptables -t nat -D POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${WG_DEVICE} -j MASQUERADE; |
|||
iptables -D INPUT -p udp -m udp --dport ${WG_PORT} -j ACCEPT; |
|||
iptables -D FORWARD -i wg0 -j ACCEPT; |
|||
iptables -D FORWARD -o wg0 -j ACCEPT; |
|||
`.split('\n').join(' ');
|
|||
export const LANG = process.env.LANG || 'en'; |
|||
export const UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false'; |
|||
export const UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; |
|||
|
|||
export const REQUIRES_PASSWORD = !!PASSWORD_HASH; |
|||
|
|||
export const SESSION_CONFIG = { |
|||
password: getRandomHex(256) |
|||
} satisfies SessionConfig; |
|||
|
|||
export const SERVER_DEBUG = debug('Server'); |
@ -0,0 +1,5 @@ |
|||
export function getRandomHex(size: number) { |
|||
const array = new Uint8Array(size); |
|||
crypto.getRandomValues(array); |
|||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); |
|||
} |
@ -0,0 +1,12 @@ |
|||
export function isValidIPv4(str) { |
|||
const blocks = str.split('.'); |
|||
if (blocks.length !== 4) return false; |
|||
|
|||
for (let value of blocks) { |
|||
value = parseInt(value, 10); |
|||
if (Number.isNaN(value)) return false; |
|||
if (value < 0 || value > 255) return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
@ -0,0 +1,23 @@ |
|||
import bcrypt from 'bcrypt'; |
|||
|
|||
import { PASSWORD_HASH } from "~/utils/config"; |
|||
|
|||
/** |
|||
* Checks if `password` matches the PASSWORD_HASH. |
|||
* |
|||
* If environment variable is not set, the password is always invalid. |
|||
* |
|||
* @param {string} password String to test |
|||
* @returns {boolean} true if matching environment, otherwise false |
|||
*/ |
|||
export function isPasswordValid(password: string): boolean { |
|||
if (typeof password !== 'string') { |
|||
return false; |
|||
} |
|||
|
|||
if (PASSWORD_HASH) { |
|||
return bcrypt.compareSync(password, PASSWORD_HASH); |
|||
} |
|||
|
|||
return false; |
|||
}; |
@ -1,5 +1,3 @@ |
|||
'use strict'; |
|||
|
|||
// Import needed libraries
|
|||
import bcrypt from 'bcryptjs'; |
|||
|
File diff suppressed because it is too large
@ -1,617 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<title>WireGuard</title> |
|||
<meta charset="utf-8"/> |
|||
<link href="./css/app.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> |
|||
<style> |
|||
[v-cloak] { |
|||
display: none; |
|||
} |
|||
.line-chart .apexcharts-svg{ |
|||
transform: translateY(3px); |
|||
} |
|||
</style> |
|||
|
|||
<body class="bg-gray-50 dark:bg-neutral-800"> |
|||
<div id="app"> |
|||
<div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6"> |
|||
<div v-if="authenticated === true"> |
|||
<div class="flex flex-col-reverse xxs:flex-row flex-auto items-center items-end gap-3"> |
|||
<h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4"> |
|||
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg mr-2" /><span class="align-middle">WireGuard</span> |
|||
</h1> |
|||
<div class="flex items-center grow-0 gap-3 items-end self-end xxs:self-center"> |
|||
<!-- Dark / light theme --> |
|||
<button @click="toggleTheme" |
|||
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition" :title="$t(`theme.${uiTheme}`)"> |
|||
<svg v-if="uiTheme === 'light'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" |
|||
class="w-5 h-5"> |
|||
<path stroke-linecap="round" stroke-linejoin="round" |
|||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" /> |
|||
</svg> |
|||
<svg v-else-if="uiTheme === 'dark'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" |
|||
class="w-5 h-5 text-neutral-400"> |
|||
<path stroke-linecap="round" stroke-linejoin="round" |
|||
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" /> |
|||
</svg> |
|||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" |
|||
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400"> |
|||
<path |
|||
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M3.8,12c0-4.5,3.7-8.2,8.2-8.2v16.5C7.5,20.2,3.8,16.5,3.8,12z" /> |
|||
</svg> |
|||
<path stroke-linecap="round" stroke-linejoin="round" |
|||
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" /> |
|||
</svg> |
|||
</button> |
|||
<!-- Show / hide charts --> |
|||
<label v-if="uiChartType > 0" class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group" :title="$t('toggleCharts')"> |
|||
<input type="checkbox" value="" class="sr-only peer" v-model="uiShowCharts" @change="toggleCharts"> |
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" fill="currentColor" |
|||
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition"> |
|||
<path |
|||
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z" /> |
|||
</svg> |
|||
</label> |
|||
<span v-if="requiresPassword" |
|||
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline" |
|||
@click="logout"> |
|||
{{$t("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> |
|||
</div> |
|||
</div> |
|||
<div class="text-sm text-gray-400 dark:text-neutral-400 mb-5"></div> |
|||
<div v-if="latestRelease" |
|||
class="bg-red-800 dark:bg-red-100 p-4 text-white dark:text-red-600 text-sm font-small mb-10 rounded-md shadow-lg" |
|||
:title="`v${currentRelease} → v${latestRelease.version}`"> |
|||
<div class="container mx-auto flex flex-row flex-auto items-center"> |
|||
<div class="flex-grow"> |
|||
<p class="font-bold">{{$t("updateAvailable")}}</p> |
|||
<p>{{latestRelease.changelog}}</p> |
|||
</div> |
|||
|
|||
<a href="https://github.com/wg-easy/wg-easy#updating" target="_blank" |
|||
class="p-3 rounded-md bg-white dark:bg-red-100 float-right font-sm font-semibold text-red-800 dark:text-red-600 flex-shrink-0 border-2 border-red-800 dark:border-red-600 hover:border-white dark:hover:border-red-600 hover:text-white dark:hover:text-red-100 hover:bg-red-800 dark:hover:bg-red-600 transition-all"> |
|||
{{$t("update")}} → |
|||
</a> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"> |
|||
<div class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600"> |
|||
<div class="flex-grow"> |
|||
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p> |
|||
</div> |
|||
<div class="flex md:block md:flex-shrink-0"> |
|||
<!-- Restore configuration --> |
|||
<label for="inputRC" :title="$t('titleRestoreConfig')" |
|||
class="hover:cursor-pointer hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-r-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-l-full md:rounded inline-flex items-center transition"> |
|||
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> |
|||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path> |
|||
</svg> |
|||
<span class="max-md:hidden text-sm">{{$t("restore")}}</span> |
|||
<input id="inputRC" type="file" name="configurationfile" accept="text/*,.json" @change="restoreConfig" class="hidden"/> |
|||
</label> |
|||
<!-- Backup configuration --> |
|||
<a href="./api/wireguard/backup" :title="$t('titleBackupConfig')" |
|||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-x-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 md:rounded inline-flex items-center transition"> |
|||
<svg inline class="w-4 md:mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> |
|||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"></path> |
|||
</svg> |
|||
<span class="max-md:hidden text-sm">{{$t("backup")}}</span> |
|||
</a> |
|||
<!-- New client --> |
|||
<button @click="clientCreate = true; clientCreateName = '';" |
|||
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 max-md:border-l-0 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded-r-full md:rounded inline-flex items-center transition"> |
|||
<svg class="w-4 md: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="max-md:hidden text-sm">{{$t("new")}}</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div> |
|||
<!-- Client --> |
|||
<div v-if="clients && clients.length > 0" v-for="client in clients" :key="client.id" |
|||
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid"> |
|||
|
|||
<!-- Chart --> |
|||
<div v-if="uiChartType" :class="`absolute z-0 bottom-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" > |
|||
<apexchart width="100%" height="100%" :options="chartOptionsTX" :series="client.transferTxSeries"> |
|||
</apexchart> |
|||
</div> |
|||
<div v-if="uiChartType" :class="`absolute z-0 top-0 left-0 right-0 h-6 ${uiChartType === 1 && 'line-chart'}`" > |
|||
<apexchart width="100%" height="100%" :options="chartOptionsRX" :series="client.transferRxSeries" |
|||
style="transform: scaleY(-1);"> |
|||
</apexchart> |
|||
</div> |
|||
|
|||
<div class="relative py-3 md:py-5 px-3 z-10 flex flex-col sm:flex-row justify-between gap-3"> |
|||
<div class="flex gap-3 md:gap-4 w-full items-center "> |
|||
|
|||
<!-- Avatar --> |
|||
<div class="h-10 w-10 mt-2 self-start 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.latestHandshakeAt && ((new Date() - new Date(client.latestHandshakeAt) < 1000 * 60 * 10))"> |
|||
<div |
|||
class="animate-ping w-4 h-4 p-1 bg-red-100 dark:bg-red-100 rounded-full absolute -bottom-1 -right-1"> |
|||
</div> |
|||
<div class="w-2 h-2 bg-red-800 dark:bg-red-600 rounded-full absolute bottom-0 right-0"></div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Name & Info --> |
|||
<div class="flex flex-col xxs:flex-row w-full gap-2"> |
|||
|
|||
<!-- Name --> |
|||
<div class="flex flex-col flex-grow gap-1"> |
|||
<div class="text-gray-700 dark:text-neutral-200 group text-sm md:text-base" |
|||
:title="$t('createdOn') + dateTime(new Date(client.createdAt))"> |
|||
|
|||
<!-- Show --> |
|||
<input v-show="clientEditNameId === client.id" v-model="clientEditName" |
|||
v-on:keyup.enter="updateClientName(client, clientEditName); clientEditName = null; clientEditNameId = null;" |
|||
v-on:keyup.escape="clientEditName = null; clientEditNameId = null;" |
|||
:ref="'client-' + client.id + '-name'" |
|||
class="rounded px-1 border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 dark:placeholder:text-neutral-500 outline-none w-30" /> |
|||
<span v-show="clientEditNameId !== client.id" |
|||
class="border-t-2 border-b-2 border-transparent">{{client.name}}</span> |
|||
|
|||
<!-- Edit --> |
|||
<span v-show="clientEditNameId !== client.id" |
|||
@click="clientEditName = client.name; clientEditNameId = client.id; setTimeout(() => $refs['client-' + client.id + '-name'][0].select(), 1);" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"> |
|||
<svg xmlns="http://www.w3.org/2000/svg" |
|||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none" |
|||
viewBox="0 0 24 24" stroke="currentColor"> |
|||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
|||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> |
|||
</svg> |
|||
</span> |
|||
</div> |
|||
<!-- Address --> |
|||
<div class=" block md:inline-block pb-1 md:pb-0 text-gray-500 dark:text-neutral-400 text-xs"> |
|||
<span class="group"> |
|||
<!-- Show --> |
|||
<input v-show="clientEditAddressId === client.id" v-model="clientEditAddress" |
|||
v-on:keyup.enter="updateClientAddress(client, clientEditAddress); clientEditAddress = null; clientEditAddressId = null;" |
|||
v-on:keyup.escape="clientEditAddress = null; clientEditAddressId = null;" |
|||
:ref="'client-' + client.id + '-address'" |
|||
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-20 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500" /> |
|||
<span v-show="clientEditAddressId !== client.id" |
|||
class="inline-block ">{{client.address}}</span> |
|||
|
|||
<!-- Edit --> |
|||
<span v-show="clientEditAddressId !== client.id" |
|||
@click="clientEditAddress = client.address; clientEditAddressId = client.id; setTimeout(() => $refs['client-' + client.id + '-address'][0].select(), 1);" |
|||
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"> |
|||
<svg xmlns="http://www.w3.org/2000/svg" |
|||
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none" |
|||
viewBox="0 0 24 24" stroke="currentColor"> |
|||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
|||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> |
|||
</svg> |
|||
</span> |
|||
</span> |
|||
<!-- Inline Transfer TX --> |
|||
<span v-if="!uiTrafficStats && client.transferTx" class="whitespace-nowrap" :title="$t('totalDownload') + bytes(client.transferTx)"> |
|||
· |
|||
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> |
|||
<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.transferTxCurrent | bytes}}/s |
|||
</span> |
|||
|
|||
<!-- Inline Transfer RX --> |
|||
<span v-if="!uiTrafficStats && client.transferRx" class="whitespace-nowrap" :title="$t('totalUpload') + bytes(client.transferRx)"> |
|||
· |
|||
<svg class="align-middle h-3 inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> |
|||
<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.transferRxCurrent | bytes}}/s |
|||
</span> |
|||
<!-- Last seen --> |
|||
<span class="text-gray-400 dark:text-neutral-500 whitespace-nowrap" v-if="client.latestHandshakeAt" |
|||
:title="$t('lastSeen') + dateTime(new Date(client.latestHandshakeAt))"> |
|||
{{!uiTrafficStats ? " · " : ""}}{{new Date(client.latestHandshakeAt) | timeago}} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Info --> |
|||
<div v-if="uiTrafficStats" |
|||
class="flex gap-2 items-center shrink-0 text-gray-400 dark:text-neutral-400 text-xs mt-px justify-end"> |
|||
|
|||
<!-- Transfer TX --> |
|||
<div class="min-w-20 md:min-w-24" v-if="client.transferTx"> |
|||
<span class="flex gap-1" :title="$t('totalDownload') + bytes(client.transferTx)"> |
|||
<svg class="align-middle h-3 inline mt-0.5" 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> |
|||
<div> |
|||
<span class="text-gray-700 dark:text-neutral-200">{{client.transferTxCurrent | |
|||
bytes}}/s</span> |
|||
<!-- Total TX --> |
|||
<br><span class="font-regular" style="font-size:0.85em">{{bytes(client.transferTx)}}</span> |
|||
</div> |
|||
</span> |
|||
|
|||
</div> |
|||
|
|||
<!-- Transfer RX --> |
|||
<div class="min-w-20 md:min-w-24" v-if="client.transferRx"> |
|||
<span class="flex gap-1" :title="$t('totalUpload') + bytes(client.transferRx)"> |
|||
|
|||
<svg class="align-middle h-3 inline mt-0.5" 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> |
|||
<div> |
|||
<span class="text-gray-700 dark:text-neutral-200">{{client.transferRxCurrent | |
|||
bytes}}/s</span> |
|||
<!-- Total RX --> |
|||
<br><span class="font-regular" style="font-size:0.85em">{{bytes(client.transferRx)}}</span> |
|||
</div> |
|||
</span> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
<!-- </div> --> <!-- <div class="flex flex-grow items-center"> --> |
|||
</div> |
|||
|
|||
<div class="flex items-center justify-end"> |
|||
<div class="text-gray-400 dark:text-neutral-400 flex gap-1 items-center justify-between"> |
|||
|
|||
<!-- Enable/Disable --> |
|||
<div @click="disableClient(client)" v-if="client.enabled === true" :title="$t('disableClient')" |
|||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 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" :title="$t('enableClient')" |
|||
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 dark:bg-neutral-400 cursor-pointer hover:bg-gray-300 dark:hover:bg-neutral-500 transition-all"> |
|||
|
|||
<div class="rounded-full w-4 h-4 m-1 bg-white"></div> |
|||
</div> |
|||
|
|||
<!-- Show QR--> |
|||
|
|||
<button :disabled="!client.downloadableConfig" |
|||
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition" |
|||
:class="{ |
|||
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig, |
|||
'is-disabled': !client.downloadableConfig |
|||
}" |
|||
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('showQR')" |
|||
@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 :disabled="!client.downloadableConfig" |
|||
:href="'./api/wireguard/client/' + client.id + '/configuration'" |
|||
:download="client.downloadableConfig ? 'configuration' : null" |
|||
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 p-2 rounded transition" |
|||
:class="{ |
|||
'hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white': client.downloadableConfig, |
|||
'is-disabled': !client.downloadableConfig |
|||
}" |
|||
:title="!client.downloadableConfig ? $t('noPrivKey') : $t('downloadConfig')" |
|||
@click="if(!client.downloadableConfig) { $event.preventDefault(); }"> |
|||
<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 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition" |
|||
:title="$t('deleteClient')" @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> |
|||
<div v-if="clients && clients.length === 0"> |
|||
<p class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm"> |
|||
{{$t("noClients")}}<br /><br /> |
|||
<button @click="clientCreate = true; clientCreateName = '';" |
|||
class="bg-red-800 hover:bg-red-700 text-white 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">{{$t("newClient")}}</span> |
|||
</button> |
|||
</p> |
|||
</div> |
|||
<div v-if="clients === null" class="text-gray-200 dark:text-red-300 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 z-20"> |
|||
<div class="bg-white rounded-md shadow-lg relative p-8"> |
|||
<button @click="qrcode = null" |
|||
class="absolute right-4 top-4 text-gray-600 dark:text-neutral-500 hover:text-gray-800 dark:hover:text-neutral-700"> |
|||
<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-center 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 dark:bg-black opacity-75 dark:opacity-50"></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 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
To: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
Leaving: "ease-in duration-200" |
|||
From: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
--> |
|||
<div |
|||
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full" |
|||
role="dialog" aria-modal="true" aria-labelledby="modal-headline"> |
|||
<div class="bg-white dark:bg-neutral-700 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-800 sm:mx-0 sm:h-10 sm:w-10"> |
|||
<svg class="h-6 w-6 text-white" inline xmlns="http://www.w3.org/2000/svg" |
|||
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 dark:text-neutral-200" id="modal-headline"> |
|||
{{$t("newClient")}} |
|||
</h3> |
|||
<div class="mt-2"> |
|||
<p class="text-sm text-gray-500"> |
|||
<input |
|||
class="rounded p-2 border-2 dark:bg-neutral-700 dark:text-neutral-200 border-gray-100 dark:border-neutral-600 focus:border-gray-200 focus:dark:border-neutral-500 dark:placeholder:text-neutral-400 outline-none w-full" |
|||
type="text" v-model.trim="clientCreateName" :placeholder="$t('name')" /> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="bg-gray-50 dark:bg-neutral-700 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-red-800 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm"> |
|||
{{$t("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 dark:bg-neutral-400 text-base font-medium text-white dark:text-neutral-300 sm:ml-3 sm:w-auto sm:text-sm cursor-not-allowed"> |
|||
{{$t("create")}} |
|||
</button> |
|||
<button type="button" @click="clientCreate = null" |
|||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> |
|||
{{$t("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-center 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 dark:bg-black opacity-75 dark:opacity-50"></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 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
To: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
Leaving: "ease-in duration-200" |
|||
From: "opacity-100 tranneutral-y-0 sm:scale-100" |
|||
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95" |
|||
--> |
|||
<div |
|||
class="inline-block align-bottom bg-white dark:bg-neutral-700 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full" |
|||
role="dialog" aria-modal="true" aria-labelledby="modal-headline"> |
|||
<div class="bg-white dark:bg-neutral-700 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 dark:text-neutral-200" id="modal-headline"> |
|||
{{$t("deleteClient")}} |
|||
</h3> |
|||
<div class="mt-2"> |
|||
<p class="text-sm text-gray-500 dark:text-neutral-300"> |
|||
{{$t("deleteDialog1")}} <strong>{{clientDelete.name}}</strong>? {{$t("deleteDialog2")}} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="bg-gray-50 dark:bg-neutral-600 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 dark:bg-red-600 text-base font-medium text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm"> |
|||
{{$t("deleteClient")}} |
|||
</button> |
|||
<button type="button" @click="clientDelete = null" |
|||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-neutral-500 shadow-sm px-4 py-2 bg-white dark:bg-neutral-500 text-base font-medium text-gray-700 dark:text-neutral-50 hover:bg-gray-50 dark:hover:bg-neutral-600 dark:hover:border-neutral-600 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"> |
|||
{{$t("cancel")}} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-if="authenticated === false"> |
|||
<h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center"> |
|||
<img src="./img/logo.png" width="32" class="inline align-middle dark:bg" /> |
|||
<span class="align-middle">WireGuard</span> |
|||
</h1> |
|||
|
|||
<form @submit="login" |
|||
class="shadow rounded-md bg-white dark:bg-neutral-700 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-red-800 dark:bg-red-800 relative overflow-hidden"> |
|||
<svg class="w-10 h-10 m-5 text-white dark:text-white" xmlns="http://www.w3.org/2000/svg" |
|||
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="$t('password')" v-model="password" |
|||
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" /> |
|||
|
|||
<button v-if="authenticating" |
|||
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark: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-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer" |
|||
:value="$t('signIn')"> |
|||
<input v-if="!authenticating && !password" type="submit" |
|||
class="bg-gray-200 dark:bg-neutral-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed" |
|||
:value="$t('signIn')"> |
|||
</form> |
|||
</div> |
|||
|
|||
<div v-if="authenticated === null" class="text-gray-300 dark:text-red-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> |
|||
|
|||
</div> |
|||
|
|||
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <a class="hover:underline" target="_blank" |
|||
href="https://github.com/wg-easy/wg-easy">WireGuard Easy</a> © 2021-2024 by <a class="hover:underline" target="_blank" |
|||
href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> is licensed under <a class="hover:underline" target="_blank" |
|||
href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> · <a class="hover:underline" |
|||
href="https://github.com/sponsors/WeeJeWel" target="_blank">{{$t("donate")}}</a></p> |
|||
|
|||
</div> |
|||
|
|||
<script src="./js/vendor/vue.min.js"></script> |
|||
<script src="./js/vendor/vue-i18n.min.js"></script> |
|||
<script src="./js/vendor/apexcharts.min.js"></script> |
|||
<script src="./js/vendor/vue-apexcharts.min.js"></script> |
|||
<script src="./js/vendor/sha256.min.js"></script> |
|||
<script src="./js/vendor/timeago.full.min.js"></script> |
|||
<script src="./js/api.js"></script> |
|||
<script src="./js/i18n.js"></script> |
|||
<script src="./js/app.js"></script> |
|||
</body> |
|||
|
|||
</html> |
@ -1,444 +0,0 @@ |
|||
/* eslint-disable no-console */ |
|||
/* eslint-disable no-alert */ |
|||
/* eslint-disable no-undef */ |
|||
/* eslint-disable no-new */ |
|||
|
|||
'use strict'; |
|||
|
|||
function bytes(bytes, decimals, kib, maxunit) { |
|||
kib = kib || false; |
|||
if (bytes === 0) return '0 B'; |
|||
if (Number.isNaN(parseFloat(bytes)) && !Number.isFinite(bytes)) return 'NaN'; |
|||
const k = kib ? 1024 : 1000; |
|||
const dm = decimals != null && !Number.isNaN(decimals) && decimals >= 0 ? decimals : 2; |
|||
const sizes = kib |
|||
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB'] |
|||
: ['B', '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]}`; |
|||
} |
|||
|
|||
const i18n = new VueI18n({ |
|||
locale: localStorage.getItem('lang') || 'en', |
|||
fallbackLocale: 'en', |
|||
messages, |
|||
}); |
|||
|
|||
const UI_CHART_TYPES = [ |
|||
{ type: false, strokeWidth: 0 }, |
|||
{ type: 'line', strokeWidth: 3 }, |
|||
{ type: 'area', strokeWidth: 0 }, |
|||
{ type: 'bar', strokeWidth: 0 }, |
|||
]; |
|||
|
|||
const CHART_COLORS = { |
|||
rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' }, |
|||
tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' }, |
|||
gradient: { light: ['rgba(0,0,0,1.0)', 'rgba(0,0,0,1.0)'], dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'] }, |
|||
}; |
|||
|
|||
new Vue({ |
|||
el: '#app', |
|||
components: { |
|||
apexchart: VueApexCharts, |
|||
}, |
|||
i18n, |
|||
data: { |
|||
authenticated: null, |
|||
authenticating: false, |
|||
password: null, |
|||
requiresPassword: null, |
|||
|
|||
clients: null, |
|||
clientsPersist: {}, |
|||
clientDelete: null, |
|||
clientCreate: null, |
|||
clientCreateName: '', |
|||
clientEditName: null, |
|||
clientEditNameId: null, |
|||
clientEditAddress: null, |
|||
clientEditAddressId: null, |
|||
qrcode: null, |
|||
|
|||
currentRelease: null, |
|||
latestRelease: null, |
|||
|
|||
uiTrafficStats: false, |
|||
|
|||
uiChartType: 0, |
|||
uiShowCharts: localStorage.getItem('uiShowCharts') === '1', |
|||
uiTheme: localStorage.theme || 'auto', |
|||
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)'), |
|||
|
|||
chartOptions: { |
|||
chart: { |
|||
background: 'transparent', |
|||
stacked: false, |
|||
toolbar: { |
|||
show: false, |
|||
}, |
|||
animations: { |
|||
enabled: false, |
|||
}, |
|||
parentHeightOffset: 0, |
|||
sparkline: { |
|||
enabled: true, |
|||
}, |
|||
}, |
|||
colors: [], |
|||
stroke: { |
|||
curve: 'smooth', |
|||
}, |
|||
fill: { |
|||
type: 'gradient', |
|||
gradient: { |
|||
shade: 'dark', |
|||
type: 'vertical', |
|||
shadeIntensity: 0, |
|||
gradientToColors: CHART_COLORS.gradient[this.theme], |
|||
inverseColors: false, |
|||
opacityTo: 0, |
|||
stops: [0, 100], |
|||
}, |
|||
}, |
|||
dataLabels: { |
|||
enabled: false, |
|||
}, |
|||
plotOptions: { |
|||
bar: { |
|||
horizontal: false, |
|||
}, |
|||
}, |
|||
xaxis: { |
|||
labels: { |
|||
show: false, |
|||
}, |
|||
axisTicks: { |
|||
show: false, |
|||
}, |
|||
axisBorder: { |
|||
show: false, |
|||
}, |
|||
}, |
|||
yaxis: { |
|||
labels: { |
|||
show: false, |
|||
}, |
|||
min: 0, |
|||
}, |
|||
tooltip: { |
|||
enabled: false, |
|||
}, |
|||
legend: { |
|||
show: false, |
|||
}, |
|||
grid: { |
|||
show: false, |
|||
padding: { |
|||
left: -10, |
|||
right: 0, |
|||
bottom: -15, |
|||
top: -15, |
|||
}, |
|||
column: { |
|||
opacity: 0, |
|||
}, |
|||
xaxis: { |
|||
lines: { |
|||
show: false, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
methods: { |
|||
dateTime: (value) => { |
|||
return new Intl.DateTimeFormat(undefined, { |
|||
year: 'numeric', |
|||
month: 'short', |
|||
day: 'numeric', |
|||
hour: 'numeric', |
|||
minute: 'numeric', |
|||
}).format(value); |
|||
}, |
|||
async refresh({ |
|||
updateCharts = false, |
|||
} = {}) { |
|||
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://gravatar.com/avatar/${sha256(client.name.toLowerCase().trim())}.jpg`; |
|||
} |
|||
|
|||
if (!this.clientsPersist[client.id]) { |
|||
this.clientsPersist[client.id] = {}; |
|||
this.clientsPersist[client.id].transferRxHistory = Array(50).fill(0); |
|||
this.clientsPersist[client.id].transferRxPrevious = client.transferRx; |
|||
this.clientsPersist[client.id].transferTxHistory = Array(50).fill(0); |
|||
this.clientsPersist[client.id].transferTxPrevious = client.transferTx; |
|||
} |
|||
|
|||
// Debug
|
|||
// client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
|
|||
// client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
|
|||
// client.latestHandshakeAt = new Date();
|
|||
// this.requiresPassword = true;
|
|||
|
|||
this.clientsPersist[client.id].transferRxCurrent = client.transferRx - this.clientsPersist[client.id].transferRxPrevious; |
|||
this.clientsPersist[client.id].transferRxPrevious = client.transferRx; |
|||
this.clientsPersist[client.id].transferTxCurrent = client.transferTx - this.clientsPersist[client.id].transferTxPrevious; |
|||
this.clientsPersist[client.id].transferTxPrevious = client.transferTx; |
|||
|
|||
if (updateCharts) { |
|||
this.clientsPersist[client.id].transferRxHistory.push(this.clientsPersist[client.id].transferRxCurrent); |
|||
this.clientsPersist[client.id].transferRxHistory.shift(); |
|||
|
|||
this.clientsPersist[client.id].transferTxHistory.push(this.clientsPersist[client.id].transferTxCurrent); |
|||
this.clientsPersist[client.id].transferTxHistory.shift(); |
|||
|
|||
this.clientsPersist[client.id].transferTxSeries = [{ |
|||
name: 'Tx', |
|||
data: this.clientsPersist[client.id].transferTxHistory, |
|||
}]; |
|||
|
|||
this.clientsPersist[client.id].transferRxSeries = [{ |
|||
name: 'Rx', |
|||
data: this.clientsPersist[client.id].transferRxHistory, |
|||
}]; |
|||
|
|||
client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory; |
|||
client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory; |
|||
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory); |
|||
|
|||
client.transferTxSeries = this.clientsPersist[client.id].transferTxSeries; |
|||
client.transferRxSeries = this.clientsPersist[client.id].transferRxSeries; |
|||
} |
|||
|
|||
client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent; |
|||
client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent; |
|||
|
|||
client.hoverTx = this.clientsPersist[client.id].hoverTx; |
|||
client.hoverRx = this.clientsPersist[client.id].hoverRx; |
|||
|
|||
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; |
|||
this.requiresPassword = session.requiresPassword; |
|||
return this.refresh(); |
|||
}) |
|||
.catch((err) => { |
|||
alert(err.message || err.toString()); |
|||
}) |
|||
.finally(() => { |
|||
this.authenticating = false; |
|||
this.password = null; |
|||
}); |
|||
}, |
|||
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(client) { |
|||
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)); |
|||
}, |
|||
updateClientName(client, name) { |
|||
this.api.updateClientName({ clientId: client.id, name }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => this.refresh().catch(console.error)); |
|||
}, |
|||
updateClientAddress(client, address) { |
|||
this.api.updateClientAddress({ clientId: client.id, address }) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => this.refresh().catch(console.error)); |
|||
}, |
|||
restoreConfig(e) { |
|||
e.preventDefault(); |
|||
const file = e.currentTarget.files.item(0); |
|||
if (file) { |
|||
file.text() |
|||
.then((content) => { |
|||
this.api.restoreConfiguration(content) |
|||
.then((_result) => alert('The configuration was updated.')) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => this.refresh().catch(console.error)); |
|||
}) |
|||
.catch((err) => alert(err.message || err.toString())); |
|||
} else { |
|||
alert('Failed to load your file!'); |
|||
} |
|||
}, |
|||
toggleTheme() { |
|||
const themes = ['light', 'dark', 'auto']; |
|||
const currentIndex = themes.indexOf(this.uiTheme); |
|||
const newIndex = (currentIndex + 1) % themes.length; |
|||
this.uiTheme = themes[newIndex]; |
|||
localStorage.theme = this.uiTheme; |
|||
this.setTheme(this.uiTheme); |
|||
}, |
|||
setTheme(theme) { |
|||
const { classList } = document.documentElement; |
|||
const shouldAddDarkClass = theme === 'dark' || (theme === 'auto' && this.prefersDarkScheme.matches); |
|||
classList.toggle('dark', shouldAddDarkClass); |
|||
}, |
|||
handlePrefersChange(e) { |
|||
if (localStorage.theme === 'auto') { |
|||
this.setTheme(e.matches ? 'dark' : 'light'); |
|||
} |
|||
}, |
|||
toggleCharts() { |
|||
localStorage.setItem('uiShowCharts', this.uiShowCharts ? 1 : 0); |
|||
}, |
|||
}, |
|||
filters: { |
|||
bytes, |
|||
timeago: (value) => { |
|||
return timeago.format(value, i18n.locale); |
|||
}, |
|||
}, |
|||
mounted() { |
|||
this.prefersDarkScheme.addListener(this.handlePrefersChange); |
|||
this.setTheme(this.uiTheme); |
|||
|
|||
this.api = new API(); |
|||
this.api.getSession() |
|||
.then((session) => { |
|||
this.authenticated = session.authenticated; |
|||
this.requiresPassword = session.requiresPassword; |
|||
this.refresh({ |
|||
updateCharts: this.updateCharts, |
|||
}).catch((err) => { |
|||
alert(err.message || err.toString()); |
|||
}); |
|||
}) |
|||
.catch((err) => { |
|||
alert(err.message || err.toString()); |
|||
}); |
|||
|
|||
setInterval(() => { |
|||
this.refresh({ |
|||
updateCharts: this.updateCharts, |
|||
}).catch(console.error); |
|||
}, 1000); |
|||
|
|||
this.api.getuiTrafficStats() |
|||
.then((res) => { |
|||
this.uiTrafficStats = res; |
|||
}) |
|||
.catch(() => { |
|||
this.uiTrafficStats = false; |
|||
}); |
|||
|
|||
this.api.getChartType() |
|||
.then((res) => { |
|||
this.uiChartType = parseInt(res, 10); |
|||
}) |
|||
.catch(() => { |
|||
this.uiChartType = 0; |
|||
}); |
|||
|
|||
Promise.resolve().then(async () => { |
|||
const lang = await this.api.getLang(); |
|||
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) { |
|||
localStorage.setItem('lang', lang); |
|||
i18n.locale = lang; |
|||
} |
|||
|
|||
const currentRelease = await this.api.getRelease(); |
|||
const latestRelease = await fetch('https://wg-easy.github.io/wg-easy/changelog.json') |
|||
.then((res) => res.json()) |
|||
.then((releases) => { |
|||
const releasesArray = Object.entries(releases).map(([version, changelog]) => ({ |
|||
version: parseInt(version, 10), |
|||
changelog, |
|||
})); |
|||
releasesArray.sort((a, b) => { |
|||
return b.version - a.version; |
|||
}); |
|||
|
|||
return releasesArray[0]; |
|||
}); |
|||
|
|||
if (currentRelease >= latestRelease.version) return; |
|||
|
|||
this.currentRelease = currentRelease; |
|||
this.latestRelease = latestRelease; |
|||
}).catch((err) => console.error(err)); |
|||
}, |
|||
computed: { |
|||
chartOptionsTX() { |
|||
const opts = { |
|||
...this.chartOptions, |
|||
colors: [CHART_COLORS.tx[this.theme]], |
|||
}; |
|||
opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false; |
|||
opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth; |
|||
return opts; |
|||
}, |
|||
chartOptionsRX() { |
|||
const opts = { |
|||
...this.chartOptions, |
|||
colors: [CHART_COLORS.rx[this.theme]], |
|||
}; |
|||
opts.chart.type = UI_CHART_TYPES[this.uiChartType].type || false; |
|||
opts.stroke.width = UI_CHART_TYPES[this.uiChartType].strokeWidth; |
|||
return opts; |
|||
}, |
|||
updateCharts() { |
|||
return this.uiChartType > 0 && this.uiShowCharts; |
|||
}, |
|||
theme() { |
|||
if (this.uiTheme === 'auto') { |
|||
return this.prefersDarkScheme.matches ? 'dark' : 'light'; |
|||
} |
|||
return this.uiTheme; |
|||
}, |
|||
}, |
|||
}); |
@ -1,585 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const messages = { // eslint-disable-line no-unused-vars
|
|||
en: { |
|||
name: 'Name', |
|||
password: 'Password', |
|||
signIn: 'Sign In', |
|||
logout: 'Logout', |
|||
updateAvailable: 'There is an update available!', |
|||
update: 'Update', |
|||
clients: 'Clients', |
|||
new: 'New', |
|||
deleteClient: 'Delete Client', |
|||
deleteDialog1: 'Are you sure you want to delete', |
|||
deleteDialog2: 'This action cannot be undone.', |
|||
cancel: 'Cancel', |
|||
create: 'Create', |
|||
createdOn: 'Created on ', |
|||
lastSeen: 'Last seen on ', |
|||
totalDownload: 'Total Download: ', |
|||
totalUpload: 'Total Upload: ', |
|||
newClient: 'New Client', |
|||
disableClient: 'Disable Client', |
|||
enableClient: 'Enable Client', |
|||
noClients: 'There are no clients yet.', |
|||
noPrivKey: 'This client has no known private key. Cannot create Configuration.', |
|||
showQR: 'Show QR Code', |
|||
downloadConfig: 'Download Configuration', |
|||
madeBy: 'Made by', |
|||
donate: 'Donate', |
|||
toggleCharts: 'Show/hide Charts', |
|||
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' }, |
|||
restore: 'Restore', |
|||
backup: 'Backup', |
|||
titleRestoreConfig: 'Restore your configuration', |
|||
titleBackupConfig: 'Backup your configuration', |
|||
}, |
|||
ua: { |
|||
name: 'Ім`я', |
|||
password: 'Пароль', |
|||
signIn: 'Увійти', |
|||
logout: 'Вихід', |
|||
updateAvailable: 'Доступне оновлення!', |
|||
update: 'Оновити', |
|||
clients: 'Клієнти', |
|||
new: 'Новий', |
|||
deleteClient: 'Видалити клієнта', |
|||
deleteDialog1: 'Ви впевнені, що бажаєте видалити', |
|||
deleteDialog2: 'Цю дію неможливо скасувати.', |
|||
cancel: 'Скасувати', |
|||
create: 'Створити', |
|||
createdOn: 'Створено ', |
|||
lastSeen: 'Останнє підключення в ', |
|||
totalDownload: 'Всього завантажено: ', |
|||
totalUpload: 'Всього відправлено: ', |
|||
newClient: 'Новий клієнт', |
|||
disableClient: 'Вимкнути клієнта', |
|||
enableClient: 'Увімкнути клієнта', |
|||
noClients: 'Ще немає клієнтів.', |
|||
showQR: 'Показати QR-код', |
|||
downloadConfig: 'Завантажити конфігурацію', |
|||
madeBy: 'Зроблено', |
|||
donate: 'Пожертвувати', |
|||
}, |
|||
ru: { |
|||
name: 'Имя', |
|||
password: 'Пароль', |
|||
signIn: 'Войти', |
|||
logout: 'Выйти', |
|||
updateAvailable: 'Доступно обновление!', |
|||
update: 'Обновить', |
|||
clients: 'Клиенты', |
|||
new: 'Создать', |
|||
deleteClient: 'Удалить клиента', |
|||
deleteDialog1: 'Вы уверены, что хотите удалить', |
|||
deleteDialog2: 'Это действие невозможно отменить.', |
|||
cancel: 'Закрыть', |
|||
create: 'Создать', |
|||
createdOn: 'Создано в ', |
|||
lastSeen: 'Последнее подключение в ', |
|||
totalDownload: 'Всего скачано: ', |
|||
totalUpload: 'Всего загружено: ', |
|||
newClient: 'Создать клиента', |
|||
disableClient: 'Выключить клиента', |
|||
enableClient: 'Включить клиента', |
|||
noClients: 'Пока нет клиентов.', |
|||
noPrivKey: 'Невозможно создать конфигурацию: у клиента нет известного приватного ключа.', |
|||
showQR: 'Показать QR-код', |
|||
downloadConfig: 'Скачать конфигурацию', |
|||
madeBy: 'Автор', |
|||
donate: 'Поблагодарить', |
|||
toggleCharts: 'Показать/скрыть графики', |
|||
theme: { dark: 'Темная тема', light: 'Светлая тема', auto: 'Как в системе' }, |
|||
restore: 'Восстановить', |
|||
backup: 'Резервная копия', |
|||
titleRestoreConfig: 'Восстановить конфигурацию', |
|||
titleBackupConfig: 'Создать резервную копию конфигурации', |
|||
}, |
|||
tr: { // Müslüm Barış Korkmazer @babico
|
|||
name: 'İsim', |
|||
password: 'Şifre', |
|||
signIn: 'Giriş Yap', |
|||
logout: 'Çıkış Yap', |
|||
updateAvailable: 'Mevcut bir güncelleme var!', |
|||
update: 'Güncelle', |
|||
clients: 'Kullanıcılar', |
|||
new: 'Yeni', |
|||
deleteClient: 'Kullanıcı Sil', |
|||
deleteDialog1: 'Silmek istediğine emin misin', |
|||
deleteDialog2: 'Bu işlem geri alınamaz.', |
|||
cancel: 'İptal', |
|||
create: 'Oluştur', |
|||
createdOn: 'Şu saatte oluşturuldu: ', |
|||
lastSeen: 'Son görülme tarihi: ', |
|||
totalDownload: 'Toplam İndirme: ', |
|||
totalUpload: 'Toplam Yükleme: ', |
|||
newClient: 'Yeni Kullanıcı', |
|||
disableClient: 'Kullanıcıyı Devre Dışı Bırak', |
|||
enableClient: 'Kullanıcıyı Etkinleştir', |
|||
noClients: 'Henüz kullanıcı yok.', |
|||
noPrivKey: 'Bu istemcinin bilinen bir özel anahtarı yok. Yapılandırma oluşturulamıyor.', |
|||
showQR: 'QR Kodunu Göster', |
|||
downloadConfig: 'Yapılandırmayı İndir', |
|||
madeBy: 'Yapan Kişi: ', |
|||
donate: 'Bağış Yap', |
|||
toggleCharts: 'Grafiği göster/gizle', |
|||
theme: { dark: 'Karanlık tema', light: 'Açık tema', auto: 'Otomatik tema' }, |
|||
restore: 'Geri yükle', |
|||
backup: 'Yedekle', |
|||
titleRestoreConfig: 'Yapılandırmanızı geri yükleyin', |
|||
titleBackupConfig: 'Yapılandırmanızı yedekleyin', |
|||
}, |
|||
no: { // github.com/digvalley
|
|||
name: 'Navn', |
|||
password: 'Passord', |
|||
signIn: 'Logg Inn', |
|||
logout: 'Logg Ut', |
|||
updateAvailable: 'En ny oppdatering er tilgjengelig!', |
|||
update: 'Oppdater', |
|||
clients: 'Klienter', |
|||
new: 'Ny', |
|||
deleteClient: 'Slett Klient', |
|||
deleteDialog1: 'Er du sikker på at du vil slette?', |
|||
deleteDialog2: 'Denne handlingen kan ikke angres', |
|||
cancel: 'Avbryt', |
|||
create: 'Opprett', |
|||
createdOn: 'Opprettet ', |
|||
lastSeen: 'Sist sett ', |
|||
totalDownload: 'Total Nedlasting: ', |
|||
totalUpload: 'Total Opplasting: ', |
|||
newClient: 'Ny Klient', |
|||
disableClient: 'Deaktiver Klient', |
|||
enableClient: 'Aktiver Klient', |
|||
noClients: 'Ingen klienter opprettet enda.', |
|||
showQR: 'Vis QR Kode', |
|||
downloadConfig: 'Last Ned Konfigurasjon', |
|||
madeBy: 'Laget av', |
|||
donate: 'Doner', |
|||
}, |
|||
pl: { // github.com/archont94
|
|||
name: 'Nazwa', |
|||
password: 'Hasło', |
|||
signIn: 'Zaloguj się', |
|||
logout: 'Wyloguj się', |
|||
updateAvailable: 'Dostępna aktualizacja!', |
|||
update: 'Aktualizuj', |
|||
clients: 'Klienci', |
|||
new: 'Stwórz klienta', |
|||
deleteClient: 'Usuń klienta', |
|||
deleteDialog1: 'Jesteś pewny że chcesz usunąć', |
|||
deleteDialog2: 'Tej akcji nie da się cofnąć.', |
|||
cancel: 'Anuluj', |
|||
create: 'Stwórz', |
|||
createdOn: 'Utworzono ', |
|||
lastSeen: 'Ostatnio widziany ', |
|||
totalDownload: 'Całkowite pobieranie: ', |
|||
totalUpload: 'Całkowite wysyłanie: ', |
|||
newClient: 'Nowy klient', |
|||
disableClient: 'Wyłączenie klienta', |
|||
enableClient: 'Włączenie klienta', |
|||
noClients: 'Nie ma jeszcze klientów.', |
|||
showQR: 'Pokaż kod QR', |
|||
downloadConfig: 'Pobierz konfigurację', |
|||
madeBy: 'Stworzone przez', |
|||
donate: 'Wsparcie autora', |
|||
}, |
|||
fr: { // github.com/clem3109
|
|||
name: 'Nom', |
|||
password: 'Mot de passe', |
|||
signIn: 'Se Connecter', |
|||
logout: 'Se déconnecter', |
|||
updateAvailable: 'Une mise à jour est disponible !', |
|||
update: 'Mise à jour', |
|||
clients: 'Clients', |
|||
new: 'Nouveau', |
|||
deleteClient: 'Supprimer ce client', |
|||
deleteDialog1: 'Êtes-vous que vous voulez supprimer', |
|||
deleteDialog2: 'Cette action ne peut pas être annulée.', |
|||
cancel: 'Annuler', |
|||
create: 'Créer', |
|||
createdOn: 'Créé le ', |
|||
lastSeen: 'Dernière connexion le ', |
|||
totalDownload: 'Téléchargement total : ', |
|||
totalUpload: 'Téléversement total : ', |
|||
newClient: 'Nouveau client', |
|||
disableClient: 'Désactiver ce client', |
|||
enableClient: 'Activer ce client', |
|||
noClients: 'Aucun client pour le moment.', |
|||
showQR: 'Afficher le code à réponse rapide (QR Code)', |
|||
downloadConfig: 'Télécharger la configuration', |
|||
madeBy: 'Développé par', |
|||
donate: 'Soutenir', |
|||
restore: 'Restaurer', |
|||
backup: 'Sauvegarder', |
|||
titleRestoreConfig: 'Restaurer votre configuration', |
|||
titleBackupConfig: 'Sauvegarder votre configuration', |
|||
}, |
|||
de: { // github.com/florian-asche
|
|||
name: 'Name', |
|||
password: 'Passwort', |
|||
signIn: 'Anmelden', |
|||
logout: 'Abmelden', |
|||
updateAvailable: 'Eine Aktualisierung steht zur Verfügung!', |
|||
update: 'Aktualisieren', |
|||
clients: 'Clients', |
|||
new: 'Neu', |
|||
deleteClient: 'Client löschen', |
|||
deleteDialog1: 'Möchtest du wirklich löschen?', |
|||
deleteDialog2: 'Diese Aktion kann nicht rückgängig gemacht werden.', |
|||
cancel: 'Abbrechen', |
|||
create: 'Erstellen', |
|||
createdOn: 'Erstellt am ', |
|||
lastSeen: 'Zuletzt Online ', |
|||
totalDownload: 'Gesamt Download: ', |
|||
totalUpload: 'Gesamt Upload: ', |
|||
newClient: 'Neuer Client', |
|||
disableClient: 'Client deaktivieren', |
|||
enableClient: 'Client aktivieren', |
|||
noClients: 'Es wurden noch keine Clients konfiguriert.', |
|||
noPrivKey: 'Es ist kein Private Key für diesen Client bekannt. Eine Konfiguration kann nicht erstellt werden.', |
|||
showQR: 'Zeige den QR Code', |
|||
downloadConfig: 'Konfiguration herunterladen', |
|||
madeBy: 'Erstellt von', |
|||
donate: 'Spenden', |
|||
restore: 'Wiederherstellen', |
|||
backup: 'Sichern', |
|||
titleRestoreConfig: 'Stelle deine Konfiguration wieder her', |
|||
titleBackupConfig: 'Sichere deine Konfiguration', |
|||
}, |
|||
ca: { // github.com/guillembonet
|
|||
name: 'Nom', |
|||
password: 'Contrasenya', |
|||
signIn: 'Iniciar sessió', |
|||
logout: 'Tanca sessió', |
|||
updateAvailable: 'Hi ha una actualització disponible!', |
|||
update: 'Actualitza', |
|||
clients: 'Clients', |
|||
new: 'Nou', |
|||
deleteClient: 'Esborra client', |
|||
deleteDialog1: 'Estàs segur que vols esborrar aquest client?', |
|||
deleteDialog2: 'Aquesta acció no es pot desfer.', |
|||
cancel: 'Cancel·la', |
|||
create: 'Crea', |
|||
createdOn: 'Creat el ', |
|||
lastSeen: 'Última connexió el ', |
|||
totalDownload: 'Baixada total: ', |
|||
totalUpload: 'Pujada total: ', |
|||
newClient: 'Nou client', |
|||
disableClient: 'Desactiva client', |
|||
enableClient: 'Activa client', |
|||
noClients: 'Encara no hi ha cap client.', |
|||
showQR: 'Mostra codi QR', |
|||
downloadConfig: 'Descarrega configuració', |
|||
madeBy: 'Fet per', |
|||
donate: 'Donatiu', |
|||
}, |
|||
es: { // github.com/amarqz
|
|||
name: 'Nombre', |
|||
password: 'Contraseña', |
|||
signIn: 'Iniciar sesión', |
|||
logout: 'Cerrar sesión', |
|||
updateAvailable: '¡Hay una actualización disponible!', |
|||
update: 'Actualizar', |
|||
clients: 'Clientes', |
|||
new: 'Nuevo', |
|||
deleteClient: 'Eliminar cliente', |
|||
deleteDialog1: '¿Estás seguro de que quieres borrar este cliente?', |
|||
deleteDialog2: 'Esta acción no podrá ser revertida.', |
|||
cancel: 'Cancelar', |
|||
create: 'Crear', |
|||
createdOn: 'Creado el ', |
|||
lastSeen: 'Última conexión el ', |
|||
totalDownload: 'Total descargado: ', |
|||
totalUpload: 'Total subido: ', |
|||
newClient: 'Nuevo cliente', |
|||
disableClient: 'Desactivar cliente', |
|||
enableClient: 'Activar cliente', |
|||
noClients: 'Aún no hay ningún cliente.', |
|||
showQR: 'Mostrar código QR', |
|||
downloadConfig: 'Descargar configuración', |
|||
madeBy: 'Hecho por', |
|||
donate: 'Donar', |
|||
toggleCharts: 'Mostrar/Ocultar gráficos', |
|||
theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' }, |
|||
restore: 'Restaurar', |
|||
backup: 'Realizar copia de seguridad', |
|||
titleRestoreConfig: 'Restaurar su configuración', |
|||
titleBackupConfig: 'Realizar copia de seguridad de su configuración', |
|||
}, |
|||
ko: { |
|||
name: '이름', |
|||
password: '암호', |
|||
signIn: '로그인', |
|||
logout: '로그아웃', |
|||
updateAvailable: '업데이트가 있습니다!', |
|||
update: '업데이트', |
|||
clients: '클라이언트', |
|||
new: '추가', |
|||
deleteClient: '클라이언트 삭제', |
|||
deleteDialog1: '삭제 하시겠습니까?', |
|||
deleteDialog2: '이 작업은 취소할 수 없습니다.', |
|||
cancel: '취소', |
|||
create: '생성', |
|||
createdOn: '생성일: ', |
|||
lastSeen: '마지막 사용 날짜: ', |
|||
totalDownload: '총 다운로드: ', |
|||
totalUpload: '총 업로드: ', |
|||
newClient: '새로운 클라이언트', |
|||
disableClient: '클라이언트 비활성화', |
|||
enableClient: '클라이언트 활성화', |
|||
noClients: '아직 클라이언트가 없습니다.', |
|||
showQR: 'QR 코드 표시', |
|||
downloadConfig: '구성 다운로드', |
|||
madeBy: '만든 사람', |
|||
donate: '기부', |
|||
}, |
|||
vi: { |
|||
name: 'Tên', |
|||
password: 'Mật khẩu', |
|||
signIn: 'Đăng nhập', |
|||
logout: 'Đăng xuất', |
|||
updateAvailable: 'Có bản cập nhật mới!', |
|||
update: 'Cập nhật', |
|||
clients: 'Danh sách người dùng', |
|||
new: 'Mới', |
|||
deleteClient: 'Xóa người dùng', |
|||
deleteDialog1: 'Bạn có chắc chắn muốn xóa', |
|||
deleteDialog2: 'Thao tác này không thể hoàn tác.', |
|||
cancel: 'Huỷ', |
|||
create: 'Tạo', |
|||
createdOn: 'Được tạo lúc ', |
|||
lastSeen: 'Lần xem cuối vào ', |
|||
totalDownload: 'Tổng dung lượng tải xuống: ', |
|||
totalUpload: 'Tổng dung lượng tải lên: ', |
|||
newClient: 'Người dùng mới', |
|||
disableClient: 'Vô hiệu hóa người dùng', |
|||
enableClient: 'Kích hoạt người dùng', |
|||
noClients: 'Hiện chưa có người dùng nào.', |
|||
showQR: 'Hiển thị mã QR', |
|||
downloadConfig: 'Tải xuống cấu hình', |
|||
madeBy: 'Được tạo bởi', |
|||
donate: 'Ủng hộ', |
|||
}, |
|||
nl: { |
|||
name: 'Naam', |
|||
password: 'Wachtwoord', |
|||
signIn: 'Inloggen', |
|||
logout: 'Uitloggen', |
|||
updateAvailable: 'Nieuw update beschikbaar!', |
|||
update: 'update', |
|||
clients: 'clients', |
|||
new: 'Nieuw', |
|||
deleteClient: 'client verwijderen', |
|||
deleteDialog1: 'Weet je zeker dat je wilt verwijderen', |
|||
deleteDialog2: 'Deze actie kan niet ongedaan worden gemaakt.', |
|||
cancel: 'Annuleren', |
|||
create: 'Creëren', |
|||
createdOn: 'Gemaakt op ', |
|||
lastSeen: 'Laatst gezien op ', |
|||
totalDownload: 'Totaal Gedownload: ', |
|||
totalUpload: 'Totaal Geupload: ', |
|||
newClient: 'Nieuwe client', |
|||
disableClient: 'client uitschakelen', |
|||
enableClient: 'client inschakelen', |
|||
noClients: 'Er zijn nog geen clients.', |
|||
showQR: 'QR-code weergeven', |
|||
downloadConfig: 'Configuratie downloaden', |
|||
madeBy: 'Gemaakt door', |
|||
donate: 'Doneren', |
|||
}, |
|||
is: { |
|||
name: 'Nafn', |
|||
password: 'Lykilorð', |
|||
signIn: 'Skrá inn', |
|||
logout: 'Útskráning', |
|||
updateAvailable: 'Það er uppfærsla í boði!', |
|||
update: 'Uppfæra', |
|||
clients: 'Viðskiptavinir', |
|||
new: 'Nýtt', |
|||
deleteClient: 'Eyða viðskiptavin', |
|||
deleteDialog1: 'Ertu viss um að þú viljir eyða', |
|||
deleteDialog2: 'Þessi aðgerð getur ekki verið afturkallað.', |
|||
cancel: 'Hætta við', |
|||
create: 'Búa til', |
|||
createdOn: 'Búið til á ', |
|||
lastSeen: 'Síðast séð á ', |
|||
totalDownload: 'Samtals Niðurhlaða: ', |
|||
totalUpload: 'Samtals Upphlaða: ', |
|||
newClient: 'Nýr Viðskiptavinur', |
|||
disableClient: 'Gera viðskiptavin óvirkan', |
|||
enableClient: 'Gera viðskiptavin virkan', |
|||
noClients: 'Engir viðskiptavinir ennþá.', |
|||
showQR: 'Sýna QR-kóða', |
|||
downloadConfig: 'Niðurhal Stillingar', |
|||
madeBy: 'Gert af', |
|||
donate: 'Gefa', |
|||
}, |
|||
pt: { |
|||
name: 'Nome', |
|||
password: 'Palavra Chave', |
|||
signIn: 'Entrar', |
|||
logout: 'Sair', |
|||
updateAvailable: 'Existe uma atualização disponível!', |
|||
update: 'Atualizar', |
|||
clients: 'Clientes', |
|||
new: 'Novo', |
|||
deleteClient: 'Apagar Clientes', |
|||
deleteDialog1: 'Tem certeza que pretende apagar', |
|||
deleteDialog2: 'Esta ação não pode ser revertida.', |
|||
cancel: 'Cancelar', |
|||
create: 'Criar', |
|||
createdOn: 'Criado em ', |
|||
lastSeen: 'Último acesso em ', |
|||
totalDownload: 'Total Download: ', |
|||
totalUpload: 'Total Upload: ', |
|||
newClient: 'Novo Cliente', |
|||
disableClient: 'Desativar Cliente', |
|||
enableClient: 'Ativar Cliente', |
|||
noClients: 'Não existem ainda clientes.', |
|||
showQR: 'Apresentar o código QR', |
|||
downloadConfig: 'Descarregar Configuração', |
|||
madeBy: 'Feito por', |
|||
donate: 'Doar', |
|||
}, |
|||
chs: { |
|||
name: '名称', |
|||
password: '密码', |
|||
signIn: '登录', |
|||
logout: '退出', |
|||
updateAvailable: '有新版本可用!', |
|||
update: '更新', |
|||
clients: '客户端', |
|||
new: '新建', |
|||
deleteClient: '删除客户端', |
|||
deleteDialog1: '您确定要删除', |
|||
deleteDialog2: '此操作无法撤销。', |
|||
cancel: '取消', |
|||
create: '创建', |
|||
createdOn: '创建于 ', |
|||
lastSeen: '最后访问于 ', |
|||
totalDownload: '总下载: ', |
|||
totalUpload: '总上传: ', |
|||
newClient: '新建客户端', |
|||
disableClient: '禁用客户端', |
|||
enableClient: '启用客户端', |
|||
noClients: '目前没有客户端。', |
|||
showQR: '显示二维码', |
|||
downloadConfig: '下载配置', |
|||
madeBy: '由', |
|||
donate: '捐赠', |
|||
}, |
|||
cht: { |
|||
name: '名字', |
|||
password: '密碼', |
|||
signIn: '登入', |
|||
logout: '登出', |
|||
updateAvailable: '有新版本可以使用!', |
|||
update: '更新', |
|||
clients: '使用者', |
|||
new: '建立', |
|||
deleteClient: '刪除使用者', |
|||
deleteDialog1: '您確定要刪除', |
|||
deleteDialog2: '此作業無法復原。', |
|||
cancel: '取消', |
|||
create: '建立', |
|||
createdOn: '建立於 ', |
|||
lastSeen: '最後存取於 ', |
|||
totalDownload: '總下載: ', |
|||
totalUpload: '總上傳: ', |
|||
newClient: '新用戶', |
|||
disableClient: '停用使用者', |
|||
enableClient: '啟用使用者', |
|||
noClients: '目前沒有使用者。', |
|||
showQR: '顯示 QR Code', |
|||
downloadConfig: '下載 Config 檔', |
|||
madeBy: '由', |
|||
donate: '抖內', |
|||
}, |
|||
it: { |
|||
name: 'Nome', |
|||
password: 'Password', |
|||
signIn: 'Accedi', |
|||
logout: 'Esci', |
|||
updateAvailable: 'È disponibile un aggiornamento!', |
|||
update: 'Aggiorna', |
|||
clients: 'Client', |
|||
new: 'Nuovo', |
|||
deleteClient: 'Elimina Client', |
|||
deleteDialog1: 'Sei sicuro di voler eliminare', |
|||
deleteDialog2: 'Questa azione non può essere annullata.', |
|||
cancel: 'Annulla', |
|||
create: 'Crea', |
|||
createdOn: 'Creato il ', |
|||
lastSeen: 'Visto l\'ultima volta il ', |
|||
totalDownload: 'Totale Download: ', |
|||
totalUpload: 'Totale Upload: ', |
|||
newClient: 'Nuovo Client', |
|||
disableClient: 'Disabilita Client', |
|||
enableClient: 'Abilita Client', |
|||
noClients: 'Non ci sono ancora client.', |
|||
showQR: 'Mostra codice QR', |
|||
downloadConfig: 'Scarica configurazione', |
|||
madeBy: 'Realizzato da', |
|||
donate: 'Donazione', |
|||
restore: 'Ripristina', |
|||
backup: 'Backup', |
|||
titleRestoreConfig: 'Ripristina la tua configurazione', |
|||
titleBackupConfig: 'Esegui il backup della tua configurazione', |
|||
}, |
|||
th: { |
|||
name: 'ชื่อ', |
|||
password: 'รหัสผ่าน', |
|||
signIn: 'ลงชื่อเข้าใช้', |
|||
logout: 'ออกจากระบบ', |
|||
updateAvailable: 'มีอัปเดตพร้อมใช้งาน!', |
|||
update: 'อัปเดต', |
|||
clients: 'Clients', |
|||
new: 'ใหม่', |
|||
deleteClient: 'ลบ Client', |
|||
deleteDialog1: 'คุณแน่ใจหรือไม่ว่าต้องการลบ', |
|||
deleteDialog2: 'การกระทำนี้;ไม่สามารถยกเลิกได้', |
|||
cancel: 'ยกเลิก', |
|||
create: 'สร้าง', |
|||
createdOn: 'สร้างเมื่อ ', |
|||
lastSeen: 'เห็นครั้งสุดท้ายเมื่อ ', |
|||
totalDownload: 'ดาวน์โหลดทั้งหมด: ', |
|||
totalUpload: 'อัพโหลดทั้งหมด: ', |
|||
newClient: 'Client ใหม่', |
|||
disableClient: 'ปิดการใช้งาน Client', |
|||
enableClient: 'เปิดการใช้งาน Client', |
|||
noClients: 'ยังไม่มี Clients เลย', |
|||
showQR: 'แสดงรหัส QR', |
|||
downloadConfig: 'ดาวน์โหลดการตั้งค่า', |
|||
madeBy: 'สร้างโดย', |
|||
donate: 'บริจาค', |
|||
}, |
|||
hi: { // github.com/rahilarious
|
|||
name: 'नाम', |
|||
password: 'पासवर्ड', |
|||
signIn: 'लॉगिन', |
|||
logout: 'लॉगआउट', |
|||
updateAvailable: 'अपडेट उपलब्ध है!', |
|||
update: 'अपडेट', |
|||
clients: 'उपयोगकर्ताये', |
|||
new: 'नया', |
|||
deleteClient: 'उपयोगकर्ता हटाएँ', |
|||
deleteDialog1: 'क्या आपको पक्का हटाना है', |
|||
deleteDialog2: 'यह निर्णय पलट नहीं सकता।', |
|||
cancel: 'कुछ ना करें', |
|||
create: 'बनाएं', |
|||
createdOn: 'सर्जन तारीख ', |
|||
lastSeen: 'पिछली बार देखे गए थे ', |
|||
totalDownload: 'कुल डाउनलोड: ', |
|||
totalUpload: 'कुल अपलोड: ', |
|||
newClient: 'नया उपयोगकर्ता', |
|||
disableClient: 'उपयोगकर्ता स्थगित कीजिये', |
|||
enableClient: 'उपयोगकर्ता शुरू कीजिये', |
|||
noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।', |
|||
noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।', |
|||
showQR: 'क्यू आर कोड देखिये', |
|||
downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन', |
|||
madeBy: 'सर्जक', |
|||
donate: 'दान करें', |
|||
}, |
|||
}; |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,7 +0,0 @@ |
|||
/** |
|||
* Minified by jsDelivr using Terser v5.7.1. |
|||
* Original file: /npm/vue-apexcharts@1.6.2/dist/vue-apexcharts.js |
|||
* |
|||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
|||
*/ |
|||
!function (t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e(require("apexcharts/dist/apexcharts.min")) : "function" == typeof define && define.amd ? define(["apexcharts/dist/apexcharts.min"], e) : t.VueApexCharts = e(t.ApexCharts) }(this, (function (t) { "use strict"; function e(t) { return (e = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (t) { return typeof t } : function (t) { return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t })(t) } function n(t, e, n) { return e in t ? Object.defineProperty(t, e, { value: n, enumerable: !0, configurable: !0, writable: !0 }) : t[e] = n, t } t = t && t.hasOwnProperty("default") ? t.default : t; var i = { props: { options: { type: Object }, type: { type: String }, series: { type: Array, required: !0, default: function () { return [] } }, width: { default: "100%" }, height: { default: "auto" } }, data: function () { return { chart: null } }, beforeMount: function () { window.ApexCharts = t }, mounted: function () { this.init() }, created: function () { var t = this; this.$watch("options", (function (e) { !t.chart && e ? t.init() : t.chart.updateOptions(t.options) })), this.$watch("series", (function (e) { !t.chart && e ? t.init() : t.chart.updateSeries(t.series) }));["type", "width", "height"].forEach((function (e) { t.$watch(e, (function () { t.refresh() })) })) }, beforeDestroy: function () { this.chart && this.destroy() }, render: function (t) { return t("div") }, methods: { init: function () { var e = this, n = { chart: { type: this.type || this.options.chart.type || "line", height: this.height, width: this.width, events: {} }, series: this.series }; Object.keys(this.$listeners).forEach((function (t) { n.chart.events[t] = e.$listeners[t] })); var i = this.extend(this.options, n); return this.chart = new t(this.$el, i), this.chart.render() }, isObject: function (t) { return t && "object" === e(t) && !Array.isArray(t) && null != t }, extend: function (t, e) { var i = this; "function" != typeof Object.assign && (Object.assign = function (t) { if (null == t) throw new TypeError("Cannot convert undefined or null to object"); for (var e = Object(t), n = 1; n < arguments.length; n++) { var i = arguments[n]; if (null != i) for (var r in i) i.hasOwnProperty(r) && (e[r] = i[r]) } return e }); var r = Object.assign({}, t); return this.isObject(t) && this.isObject(e) && Object.keys(e).forEach((function (o) { i.isObject(e[o]) && o in t ? r[o] = i.extend(t[o], e[o]) : Object.assign(r, n({}, o, e[o])) })), r }, refresh: function () { return this.destroy(), this.init() }, destroy: function () { this.chart.destroy() }, updateSeries: function (t, e) { return this.chart.updateSeries(t, e) }, updateOptions: function (t, e, n, i) { return this.chart.updateOptions(t, e, n, i) }, toggleSeries: function (t) { return this.chart.toggleSeries(t) }, showSeries: function (t) { this.chart.showSeries(t) }, hideSeries: function (t) { this.chart.hideSeries(t) }, appendSeries: function (t, e) { return this.chart.appendSeries(t, e) }, resetSeries: function () { this.chart.resetSeries() }, zoomX: function (t, e) { this.chart.zoomX(t, e) }, toggleDataPointSelection: function (t, e) { this.chart.toggleDataPointSelection(t, e) }, appendData: function (t) { return this.chart.appendData(t) }, addText: function (t) { this.chart.addText(t) }, addImage: function (t) { this.chart.addImage(t) }, addShape: function (t) { this.chart.addShape(t) }, dataURI: function () { return this.chart.dataURI() }, setLocale: function (t) { return this.chart.setLocale(t) }, addXaxisAnnotation: function (t, e) { this.chart.addXaxisAnnotation(t, e) }, addYaxisAnnotation: function (t, e) { this.chart.addYaxisAnnotation(t, e) }, addPointAnnotation: function (t, e) { this.chart.addPointAnnotation(t, e) }, removeAnnotation: function (t, e) { this.chart.removeAnnotation(t, e) }, clearAnnotations: function () { this.chart.clearAnnotations() } } }; return window.ApexCharts = t, i.install = function (e) { e.ApexCharts = t, window.ApexCharts = t, Object.defineProperty(e.prototype, "$apexcharts", { get: function () { return t } }) }, i })); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,11 +0,0 @@ |
|||
{ |
|||
"name": "WireGuard", |
|||
"display": "standalone", |
|||
"background_color": "#fff", |
|||
"icons": [ |
|||
{ |
|||
"src": "img/favicon.png", |
|||
"type": "image/png" |
|||
} |
|||
] |
|||
} |
@ -1,3 +0,0 @@ |
|||
@tailwind base; |
|||
@tailwind components; |
|||
@tailwind utilities; |
@ -1,22 +0,0 @@ |
|||
{ |
|||
"release": { |
|||
"version": "14" |
|||
}, |
|||
"name": "wg-easy", |
|||
"version": "1.0.1", |
|||
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.", |
|||
"private": true, |
|||
"type": "module", |
|||
"scripts": { |
|||
"build": "nuxt build", |
|||
"dev": "nuxt dev", |
|||
"generate": "nuxt generate", |
|||
"preview": "nuxt preview", |
|||
"postinstall": "nuxt prepare" |
|||
}, |
|||
"dependencies": { |
|||
"@nuxtjs/i18n": "^8.3.3", |
|||
"nuxt": "^3.12.4", |
|||
"vue": "latest" |
|||
} |
|||
} |
Loading…
Reference in new issue