Compare commits

..

16 Commits

Author SHA1 Message Date
Donald Zou 3a34a0eb40 Adjusted some UI 2024-08-13 12:29:58 -04:00
Donald Zou e3f82e136a Adjusted some UI 2024-08-12 18:04:41 -04:00
Donald Zou 8a7df4ba9f Update wgd.sh 2024-08-12 01:19:55 -04:00
Donald Zou e86d1a4c7a Updated langugage 2024-08-12 00:34:54 -04:00
Donald Zou 5b9d0b60a1 Adjusted some UI 2024-08-11 19:20:52 -04:00
Donald Zou 7eff2f0c49 Fixed issue #250 2024-08-11 19:20:42 -04:00
Donald Zou 97236bb01d Fixed new configuration hang when error 2024-08-11 19:20:03 -04:00
Donald Zou 96ccb03eea Adjusted some code for electron version 2024-08-11 16:39:00 -04:00
Donald Zou 55f55820c5 Update wg-dashboard.service 2024-08-11 11:02:08 -04:00
Donald Zou 955839d513 I think cross server actually worked 2024-08-11 01:48:13 -04:00
Donald Zou a650e628e5 CORS SUCCESS!!! 2024-08-10 19:23:50 -04:00
Donald Zou 54142b73fb Ohhhhh kay testing CORS :) 2024-08-10 19:03:21 -04:00
Donald Zou 55e0d2695d Update .gitignore 2024-08-10 12:58:41 -04:00
Donald Zou 2f90ab15dc Let's try ElectronJS 2024-08-10 12:58:14 -04:00
Donald Zou fd3fc66bfc Thinking of adding Electron.js 2024-08-10 00:25:25 -04:00
Donald Zou a352a94d8a Update .gitignore 2024-08-10 00:24:07 -04:00
25 changed files with 2887 additions and 195 deletions
-1
View File
@@ -48,5 +48,4 @@ coverage
*.sw?
*.tsbuildinfo
proxy.js
.vite/*
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
# if [ -z "$(ls -A /etc/wireguard)" ]; then
# mv /wg0.conf /etc/wireguard
# echo "Moved conf file to /etc/wireguard"
# else
# rm wg0.conf
# echo "Removed unneeded conf file"
# fi
# wg-quick up wg0
chmod u+x /opt/wgdashboard/wgd.sh
if [ ! -f "/opt/wgdashboard/wg-dashboard.ini" ]; then
/opt/wgdashboard/wgd.sh install
fi
/opt/wgdashboard/wgd.sh debug
+55 -27
View File
@@ -47,7 +47,11 @@ UPDATE = None
app = Flask("WGDashboard")
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 5206928
app.secret_key = secrets.token_urlsafe(32)
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
cors = CORS(app, resources={r"/api/*": {
"origins": "*",
"methods": "DELETE, POST, GET, OPTIONS",
"allow_headers": ["Content-Type", "wg-dashboard-apikey"]
}})
class ModelEncoder(JSONEncoder):
def default(self, o: Any) -> Any:
@@ -493,7 +497,7 @@ class WireguardConfiguration:
if self.Name not in existingTables:
sqldb.cursor().execute(
"""
CREATE TABLE %s (
CREATE TABLE '%s'(
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
@@ -509,7 +513,7 @@ class WireguardConfiguration:
if f'{self.Name}_restrict_access' not in existingTables:
sqldb.cursor().execute(
"""
CREATE TABLE %s_restrict_access (
CREATE TABLE '%s_restrict_access' (
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
@@ -524,7 +528,7 @@ class WireguardConfiguration:
if f'{self.Name}_transfer' not in existingTables:
sqldb.cursor().execute(
"""
CREATE TABLE %s_transfer (
CREATE TABLE '%s_transfer' (
id VARCHAR NOT NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL,
cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, time DATETIME
@@ -535,7 +539,7 @@ class WireguardConfiguration:
if f'{self.Name}_deleted' not in existingTables:
sqldb.cursor().execute(
"""
CREATE TABLE %s_deleted (
CREATE TABLE '%s_deleted' (
id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
@@ -559,7 +563,7 @@ class WireguardConfiguration:
def __getRestrictedPeers(self):
self.RestrictedPeers = []
restricted = sqldb.cursor().execute("SELECT * FROM %s_restrict_access" % self.Name).fetchall()
restricted = sqldb.cursor().execute("SELECT * FROM '%s_restrict_access'" % self.Name).fetchall()
for i in restricted:
self.RestrictedPeers.append(Peer(i, self))
@@ -584,7 +588,7 @@ class WireguardConfiguration:
p[pCounter][split[0]] = split[1]
for i in p:
if "PublicKey" in i.keys():
checkIfExist = sqldb.cursor().execute("SELECT * FROM %s WHERE id = ?" % self.Name,
checkIfExist = sqldb.cursor().execute("SELECT * FROM '%s' WHERE id = ?" % self.Name,
((i['PublicKey']),)).fetchone()
if checkIfExist is None:
newPeer = {
@@ -612,7 +616,7 @@ class WireguardConfiguration:
}
sqldb.cursor().execute(
"""
INSERT INTO %s
INSERT INTO '%s'
VALUES (:id, :private_key, :DNS, :endpoint_allowed_ip, :name, :total_receive, :total_sent,
:total_data, :endpoint, :status, :latest_handshake, :allowed_ip, :cumu_receive, :cumu_sent,
:cumu_data, :mtu, :keepalive, :remote_endpoint, :preshared_key);
@@ -621,7 +625,7 @@ class WireguardConfiguration:
sqldb.commit()
self.Peers.append(Peer(newPeer, self))
else:
sqldb.cursor().execute("UPDATE %s SET allowed_ip = ? WHERE id = ?" % self.Name,
sqldb.cursor().execute("UPDATE '%s' SET allowed_ip = ? WHERE id = ?" % self.Name,
(i.get("AllowedIPs", "N/A"), i['PublicKey'],))
sqldb.commit()
self.Peers.append(Peer(checkIfExist, self))
@@ -649,11 +653,11 @@ class WireguardConfiguration:
self.toggleConfiguration()
for i in listOfPublicKeys:
p = sqldb.cursor().execute("SELECT * FROM %s_restrict_access WHERE id = ?" % self.Name, (i,)).fetchone()
p = sqldb.cursor().execute("SELECT * FROM '%s_restrict_access' WHERE id = ?" % self.Name, (i,)).fetchone()
if p is not None:
sqldb.cursor().execute("INSERT INTO %s SELECT * FROM %s_restrict_access WHERE id = ?"
sqldb.cursor().execute("INSERT INTO '%s' SELECT * FROM %s_restrict_access WHERE id = ?"
% (self.Name, self.Name,), (p['id'],))
sqldb.cursor().execute("DELETE FROM %s_restrict_access WHERE id = ?"
sqldb.cursor().execute("DELETE FROM '%s_restrict_access' WHERE id = ?"
% self.Name, (p['id'],))
subprocess.check_output(f"wg set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip']}",
shell=True, stderr=subprocess.STDOUT)
@@ -676,11 +680,11 @@ class WireguardConfiguration:
try:
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
shell=True, stderr=subprocess.STDOUT)
sqldb.cursor().execute("INSERT INTO %s_restrict_access SELECT * FROM %s WHERE id = ?" %
sqldb.cursor().execute("INSERT INTO '%s_restrict_access' SELECT * FROM %s WHERE id = ?" %
(self.Name, self.Name,), (pf.id,))
sqldb.cursor().execute("UPDATE %s_restrict_access SET status = 'stopped' WHERE id = ?" %
sqldb.cursor().execute("UPDATE '%s_restrict_access' SET status = 'stopped' WHERE id = ?" %
(self.Name,), (pf.id,))
sqldb.cursor().execute("DELETE FROM %s WHERE id = ?" % self.Name, (pf.id,))
sqldb.cursor().execute("DELETE FROM '%s' WHERE id = ?" % self.Name, (pf.id,))
numOfRestrictedPeers += 1
except Exception as e:
numOfFailedToRestrictPeers += 1
@@ -707,7 +711,7 @@ class WireguardConfiguration:
try:
subprocess.check_output(f"wg set {self.Name} peer {pf.id} remove",
shell=True, stderr=subprocess.STDOUT)
sqldb.cursor().execute("DELETE FROM %s WHERE id = ?" % self.Name, (pf.id,))
sqldb.cursor().execute("DELETE FROM '%s' WHERE id = ?" % self.Name, (pf.id,))
numOfDeletedPeers += 1
except Exception as e:
numOfFailedToDeletePeers += 1
@@ -727,7 +731,7 @@ class WireguardConfiguration:
d = i.toJson()
sqldb.execute(
'''
UPDATE %s SET private_key = :private_key,
UPDATE '%s' SET private_key = :private_key,
DNS = :DNS, endpoint_allowed_ip = :endpoint_allowed_ip, name = :name,
total_receive = :total_receive, total_sent = :total_sent, total_data = :total_data,
endpoint = :endpoint, status = :status, latest_handshake = :latest_handshake,
@@ -764,10 +768,10 @@ class WireguardConfiguration:
else:
status = "stopped"
if int(latestHandshake[count + 1]) > 0:
sqldb.execute("UPDATE %s SET latest_handshake = ?, status = ? WHERE id= ?" % self.Name
sqldb.execute("UPDATE '%s' SET latest_handshake = ?, status = ? WHERE id= ?" % self.Name
, (str(minus).split(".", maxsplit=1)[0], status, latestHandshake[count],))
else:
sqldb.execute("UPDATE %s SET latest_handshake = 'No Handshake', status = ? WHERE id= ?" % self.Name
sqldb.execute("UPDATE '%s' SET latest_handshake = 'No Handshake', status = ? WHERE id= ?" % self.Name
, (status, latestHandshake[count],))
sqldb.commit()
count += 2
@@ -783,7 +787,7 @@ class WireguardConfiguration:
for i in range(len(data_usage)):
if len(data_usage[i]) == 3:
cur_i = sqldb.cursor().execute(
"SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM %s WHERE id= ? "
"SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM '%s' WHERE id= ? "
% self.Name, (data_usage[i][0],)).fetchone()
if cur_i is not None:
total_sent = cur_i['total_sent']
@@ -797,7 +801,7 @@ class WireguardConfiguration:
total_receive = cur_total_receive
else:
sqldb.cursor().execute(
"UPDATE %s SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" %
"UPDATE '%s' SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" %
self.Name, (cumulative_receive, cumulative_sent,
cumulative_sent + cumulative_receive,
data_usage[i][0],))
@@ -807,7 +811,7 @@ class WireguardConfiguration:
_, p = self.searchPeer(data_usage[i][0])
if p.total_receive != total_receive or p.total_sent != total_sent:
sqldb.cursor().execute(
"UPDATE %s SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
"UPDATE '%s' SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?"
% self.Name, (total_receive, total_sent,
total_receive + total_sent, data_usage[i][0],))
except Exception as e:
@@ -824,7 +828,7 @@ class WireguardConfiguration:
data_usage = data_usage.decode("UTF-8").split()
count = 0
for _ in range(int(len(data_usage) / 2)):
sqldb.execute("UPDATE %s SET endpoint = ? WHERE id = ?" % self.Name
sqldb.execute("UPDATE '%s' SET endpoint = ? WHERE id = ?" % self.Name
, (data_usage[count + 1], data_usage[count],))
sqldb.commit()
count += 2
@@ -962,7 +966,7 @@ class Peer:
return ResponseObject(False,
"Update peer failed when saving the configuration.")
sqldb.cursor().execute(
'''UPDATE %s SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
'''UPDATE '%s' SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?,
keepalive = ?, preshared_key = ? WHERE id = ?''' % self.configuration.Name,
(name, private_key, dns_addresses, endpoint_allowed_ip, mtu,
keepalive, preshared_key, self.id,)
@@ -1014,11 +1018,11 @@ PersistentKeepalive = {str(self.keepalive)}
def resetDataUsage(self, type):
try:
if type == "total":
sqldb.cursor().execute("UPDATE %s SET total_data = 0, cumu_data = 0, total_receive = 0, cumu_receive = 0, total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
sqldb.cursor().execute("UPDATE '%s' SET total_data = 0, cumu_data = 0, total_receive = 0, cumu_receive = 0, total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
elif type == "receive":
sqldb.cursor().execute("UPDATE %s SET total_receive = 0, cumu_receive = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
sqldb.cursor().execute("UPDATE '%s' SET total_receive = 0, cumu_receive = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
elif type == "sent":
sqldb.cursor().execute("UPDATE %s SET total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
sqldb.cursor().execute("UPDATE '%s' SET total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, ))
else:
return False
except Exception as e:
@@ -1092,6 +1096,7 @@ class DashboardConfig:
self.SetConfig(section, key, value, True)
self.__createAPIKeyTable()
self.DashboardAPIKeys = self.__getAPIKeys()
self.APIAccessed = False
def __createAPIKeyTable(self):
existingTable = sqldb.cursor().execute("SELECT name FROM sqlite_master WHERE type='table' AND name = 'DashboardAPIKeys'").fetchall()
@@ -1339,12 +1344,17 @@ def _getWireguardConfigurationAvailableIP(configName: str) -> tuple[bool, list[s
return False, None
'''
API Routes
'''
@app.before_request
def auth_req():
if request.method.lower() == 'options':
return ResponseObject(True)
DashboardConfig.APIAccessed = False
if "api" in request.path:
if str(request.method) == "GET":
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=str(request.args))
@@ -1361,6 +1371,7 @@ def auth_req():
apiKeyExist = len(list(filter(lambda x : x.Key == apiKey, DashboardConfig.DashboardAPIKeys))) == 1
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"API Key Access: {('true' if apiKeyExist else 'false')} - Key: {apiKey}")
if not apiKeyExist:
DashboardConfig.APIAccessed = False
response = Flask.make_response(app, {
"status": False,
"message": "API Key does not exist",
@@ -1369,7 +1380,9 @@ def auth_req():
response.content_type = "application/json"
response.status_code = 401
return response
DashboardConfig.APIAccessed = True
else:
DashboardConfig.APIAccessed = False
if ('/static/' not in request.path and "username" not in session and "/" != request.path
and "validateAuthentication" not in request.path and "authenticate" not in request.path
and "getDashboardConfiguration" not in request.path and "getDashboardTheme" not in request.path
@@ -1385,6 +1398,10 @@ def auth_req():
response.status_code = 401
return response
@app.route('/api/handshake', methods=["GET", "OPTIONS"])
def API_ValidateAPIKey():
return ResponseObject(True)
@app.route('/api/validateAuthentication', methods=["GET"])
def API_ValidateAuthentication():
@@ -1397,6 +1414,17 @@ def API_ValidateAuthentication():
@app.route('/api/authenticate', methods=['POST'])
def API_AuthenticateLogin():
data = request.get_json()
if DashboardConfig.APIAccessed:
authToken = hashlib.sha256(f"{request.headers.get('wg-dashboard-apikey')}{datetime.now()}".encode()).hexdigest()
session['username'] = authToken
resp = ResponseObject(True, DashboardConfig.GetConfig("Other", "welcome_session")[1])
print(data['host'])
resp.set_cookie("authToken", authToken, domain=data['host'])
session.permanent = True
return resp
valid = bcrypt.checkpw(data['password'].encode("utf-8"),
DashboardConfig.GetConfig("Account", "password")[1].encode("utf-8"))
totpEnabled = DashboardConfig.GetConfig("Account", "enable_totp")[1]
File diff suppressed because one or more lines are too long
+28 -28
View File
File diff suppressed because one or more lines are too long
+2286 -2
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -1,11 +1,12 @@
{
"name": "app",
"version": "0.0.0",
"version": "4.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build electron": "vite build --mode electron && cd ../../../../WGDashboard-Desktop && electron-builder",
"preview": "vite preview"
},
"dependencies": {
@@ -17,6 +18,7 @@
"bootstrap-icons": "^1.11.2",
"cidr-tools": "^7.0.4",
"dayjs": "^1.11.12",
"electron-builder": "^24.13.3",
"fuse.js": "^7.0.0",
"i": "^0.3.7",
"is-cidr": "^5.0.3",
+27 -3
View File
@@ -1,13 +1,37 @@
<script setup >
<script setup>
import { RouterView } from 'vue-router'
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {computed, watch} from "vue";
const store = DashboardConfigurationStore();
store.initCrossServerConfiguration();
if (window.IS_WGDASHBOARD_DESKTOP){
store.IsElectronApp = true;
store.CrossServerConfiguration.Enable = true;
}
watch(store.CrossServerConfiguration, () => {
store.syncCrossServerConfiguration()
}, {
deep: true
});
const getActiveCrossServer = computed(() => {
if (store.ActiveServerConfiguration){
return store.CrossServerConfiguration.ServerList[store.ActiveServerConfiguration]
}
return undefined
})
</script>
<template>
<nav class="navbar bg-dark sticky-top" data-bs-theme="dark">
<div class="container-fluid">
<nav class="navbar bg-dark sticky-top border-bottom border-secondary-subtle" data-bs-theme="dark">
<div class="container-fluid d-flex text-body align-items-center">
<span class="navbar-brand mb-0 h1">WGDashboard</span>
<small class="ms-auto text-muted" v-if="getActiveCrossServer !== undefined">
<i class="bi bi-server me-2"></i>{{getActiveCrossServer.host}}
</small>
<a role="button"><i class="bi bi-list"></i></a>
</div>
</nav>
<Suspense>
@@ -87,6 +87,12 @@ export default {
},
computed: {
getUrl(){
const crossServer = this.store.getActiveCrossServer();
if(crossServer){
return `${crossServer.host}/${this.$router.resolve(
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href}`
}
return window.location.origin
+ window.location.pathname
+ this.$router.resolve(
+7 -2
View File
@@ -14,7 +14,7 @@ export default {
</script>
<template>
<div class="col-md-3 col-lg-2 d-md-block p-3" style="height: calc(-50px + 100vh);">
<div class="col-md-3 col-lg-2 d-md-block p-3 navbar-container bg-body" style="height: calc(-50px + 100vh);">
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" >
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column px-2">
@@ -75,5 +75,10 @@ export default {
</template>
<style scoped>
@media screen and (max-width: 768px) {
.navbar-container{
position: absolute;
z-index: 1000;
}
}
</style>
@@ -47,7 +47,7 @@ export default {
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox"
v-model="this.status"
role="switch" id="allowAPIKeysSwitch">
role="switch" id="allowMFAKeysSwitch">
</div>
<button class="btn bg-warning-subtle text-warning-emphasis border-1 border-warning-subtle ms-auto rounded-3 shadow-sm"
v-if="this.status" @click="this.resetMFA()">
@@ -65,7 +65,7 @@ export default {
<div class="card mb-4 shadow rounded-3">
<div class="card-header d-flex">
API Keys
<div class="form-check form-switch ms-auto">
<div class="form-check form-switch ms-auto" v-if="!this.store.getActiveCrossServer()">
<input class="form-check-input" type="checkbox"
v-model="this.value"
@change="this.toggleDashboardAPIKeys()"
@@ -78,6 +78,7 @@ export default {
<div class="card-body position-relative d-flex flex-column gap-2" v-if="this.value">
<button class="ms-auto btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle rounded-3 shadow-sm"
@click="this.newDashboardAPIKey = true"
v-if="!this.store.getActiveCrossServer()"
>
<i class="bi bi-key me-2"></i> Create
</button>
@@ -35,26 +35,31 @@ export default {
<template>
<div class="card rounded-3 shadow-sm">
<div class="card-body d-flex gap-3 align-items-center" v-if="!this.confirmDelete">
<div class="card-body d-flex gap-3 align-items-center apiKey-card-body" v-if="!this.confirmDelete">
<div class="d-flex align-items-center gap-2">
<small class="text-muted">Key</small>{{this.apiKey.Key}}
<small class="text-muted">Key</small>
<span style="word-break: break-all">{{this.apiKey.Key}}</span>
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<small class="text-muted">Expire At</small>
{{this.apiKey.ExpiredAt ? this.apiKey.ExpiredAt : 'Never'}}
</div>
<a role="button" class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3" @click="this.confirmDelete = true">
<a role="button" class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3"
v-if="!this.store.getActiveCrossServer()"
@click="this.confirmDelete = true">
<i class="bi bi-trash-fill"></i>
</a>
</div>
<div v-else class="card-body d-flex gap-3 align-items-center justify-content-end">
<div v-else class="card-body d-flex gap-3 align-items-center justify-content-end"
v-if="!this.store.getActiveCrossServer()">
Are you sure to delete this API key?
<a role="button" class="btn btn-sm bg-success-subtle text-success-emphasis rounded-3"
@click="this.deleteAPIKey()"
>
<i class="bi bi-check-lg"></i>
</a>
<a role="button" class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3" @click="this.confirmDelete = false">
<a role="button" class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3"
@click="this.confirmDelete = false">
<i class="bi bi-x-lg"></i>
</a>
</div>
@@ -62,5 +67,23 @@ export default {
</template>
<style scoped>
@media screen and (max-width: 992px) {
.apiKey-card-body{
flex-direction: column !important;
align-items: start !important;
div.ms-auto{
margin-left: 0 !important;
}
div{
width: 100%;
align-items: start !important;
}
small{
margin-right: auto;
}
}
}
</style>
@@ -0,0 +1,164 @@
<script>
import dayjs from "dayjs";
export default {
name: "RemoteServer",
props: {
server: Object
},
data(){
return{
active: false,
startTime: undefined,
endTime: undefined,
errorMsg: "",
refreshing: false
}
},
methods: {
async handshake(){
this.active = false;
this.refreshing = true;
if (this.server.host && this.server.apiKey){
this.startTime = undefined;
this.endTime = undefined;
this.startTime = dayjs()
await fetch(`${this.server.host}/api/handshake`, {
headers: {
"content-type": "application/json",
"wg-dashboard-apikey": this.server.apiKey
},
method: "GET",
signal: AbortSignal.timeout(5000)
}).then(res => {
if (res.status === 200){
return res.json()
}
throw new Error(res.statusText)
}).then(() => {
this.endTime = dayjs()
this.active = true;
}).catch((res) => {
this.active = false;
this.errorMsg = res;
});
this.refreshing = false;
}
},
async connect(){
await fetch(`${this.server.host}/api/authenticate`, {
headers: {
"content-type": "application/json",
"wg-dashboard-apikey": this.server.apiKey
},
body: JSON.stringify({
host: window.location.hostname
}),
method: "POST",
signal: AbortSignal.timeout(5000),
}).then(res => res.json()).then(res => {
this.$emit("setActiveServer")
this.$router.push('/')
})
}
},
mounted() {
this.handshake()
},
computed: {
getHandshakeTime(){
if (this.startTime && this.endTime){
return `${dayjs().subtract(this.startTime).millisecond()}ms`
}else{
if (this.refreshing){
return `Pinging...`
}
return this.errorMsg ? this.errorMsg : "N/A"
}
}
}
}
</script>
<template>
<div class="card rounded-3">
<div class="card-body">
<div class="d-flex gap-3 w-100 remoteServerContainer">
<div class="d-flex gap-3 align-items-center flex-grow-1">
<i class="bi bi-server"></i>
<input class="form-control form-control-sm"
@blur="this.handshake()"
v-model="this.server.host"
type="url">
</div>
<div class="d-flex gap-3 align-items-center flex-grow-1">
<i class="bi bi-key-fill"></i>
<input class="form-control form-control-sm"
@blur="this.handshake()"
v-model="this.server.apiKey"
type="text">
</div>
<div class="d-flex gap-2 button-group">
<button
@click="this.$emit('delete')"
class="ms-auto btn btn-sm bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle">
<i class="bi bi-trash"></i>
</button>
<button
@click="this.connect()"
:class="{disabled: !this.active}"
class="ms-auto btn btn-sm bg-success-subtle text-success-emphasis border-1 border-success-subtle">
<i class="bi bi-arrow-right-circle"></i>
</button>
</div>
</div>
</div>
<div class="card-footer gap-2 d-flex align-items-center">
<span class="dot ms-0 me-2" :class="[this.active ? 'active':'inactive']"></span>
<small>{{this.getHandshakeTime}}</small>
<div class="spin ms-auto text-primary-emphasis" v-if="this.refreshing">
<i class="bi bi-arrow-clockwise"></i>
</div>
<a role="button"
v-else
@click="this.handshake()"
class="text-primary-emphasis text-decoration-none ms-auto disabled">
<i class="bi bi-arrow-clockwise me"></i>
</a>
</div>
</div>
</template>
<style scoped>
.dot.inactive{
background-color: #dc3545;
box-shadow: 0 0 0 0.2rem #dc354545;
}
.spin{
animation: spin 1s infinite cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
@keyframes spin {
0%{
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
@media screen and (max-width: 768px) {
.remoteServerContainer{
flex-direction: column;
}
.remoteServerContainer .button-group button{
width: 100%;
}
}
</style>
@@ -0,0 +1,40 @@
<script>
import RemoteServer from "@/components/signInComponents/RemoteServer.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
export default {
name: "RemoteServerList",
setup(){
const store = DashboardConfigurationStore();
return {store}
},
components: {RemoteServer}
}
</script>
<template>
<div class="w-100 mt-3">
<div class="d-flex align-items-center mb-3">
<h5 class="mb-0">Server List</h5>
<button
@click="this.store.addCrossServerConfiguration()"
class="btn bg-primary-subtle text-primary-emphasis border-1 border-primary-subtle shadow-sm ms-auto">
<i class="bi bi-plus-circle-fill me-2"></i>Server
</button>
</div>
<div class="w-100 d-flex gap-3 flex-column p-3 border border-1 border-secondary-subtle rounded-3"
style="height: 400px; overflow-y: scroll">
<RemoteServer v-for="(server, key) in this.store.CrossServerConfiguration.ServerList"
@setActiveServer="this.store.setActiveCrossServer(key)"
@delete="this.store.deleteCrossServerConfiguration(key)"
:key="key"
:server="server"></RemoteServer>
<h6 class="text-muted m-auto" v-if="Object.keys(this.store.CrossServerConfiguration.ServerList).length === 0">
Click<i class="bi bi-plus-circle-fill mx-1"></i>to add your server</h6>
</div>
</div>
</template>
<style scoped>
</style>
+15 -6
View File
@@ -138,19 +138,28 @@ router.beforeEach(async (to, from, next) => {
}
if (to.meta.requiresAuth){
if (cookie.getCookie("authToken") && await checkAuth()){
if (!dashboardConfigurationStore.getActiveCrossServer()){
if (cookie.getCookie("authToken") && await checkAuth()){
await dashboardConfigurationStore.getConfiguration()
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
await wireguardConfigurationsStore.getConfigurations();
}
dashboardConfigurationStore.Redirect = undefined;
next()
}else{
dashboardConfigurationStore.Redirect = to;
next("/signin")
dashboardConfigurationStore.newMessage("WGDashboard", "Session Ended", "warning")
}
}else{
await dashboardConfigurationStore.getConfiguration()
if (!wireguardConfigurationsStore.Configurations && to.name !== "Configuration List"){
await wireguardConfigurationsStore.getConfigurations();
}
dashboardConfigurationStore.Redirect = undefined;
next()
}else{
dashboardConfigurationStore.Redirect = to;
next("/signin")
dashboardConfigurationStore.newMessage("WGDashboard", "Session Ended", "warning")
}
}else {
next();
}
});
@@ -10,9 +10,53 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
Peers: {
Selecting: false,
RefreshInterval: undefined
}
},
CrossServerConfiguration:{
Enable: false,
ServerList: {}
},
ActiveServerConfiguration: undefined,
IsElectronApp: false
}),
actions: {
initCrossServerConfiguration(){
const currentConfiguration = localStorage.getItem('CrossServerConfiguration');
if (localStorage.getItem("ActiveCrossServerConfiguration") !== null){
this.ActiveServerConfiguration = localStorage.getItem("ActiveCrossServerConfiguration");
}
if (currentConfiguration === null){
localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
}else{
this.CrossServerConfiguration = JSON.parse(currentConfiguration)
}
},
syncCrossServerConfiguration(){
localStorage.setItem('CrossServerConfiguration', JSON.stringify(this.CrossServerConfiguration))
},
addCrossServerConfiguration(){
this.CrossServerConfiguration.ServerList[v4().toString()] = {host: "", apiKey: "", active: false}
},
deleteCrossServerConfiguration(key){
delete this.CrossServerConfiguration.ServerList[key];
},
getActiveCrossServer(){
const key = localStorage.getItem('ActiveCrossServerConfiguration');
if (key !== null){
return this.CrossServerConfiguration.ServerList[key]
}
return undefined
},
setActiveCrossServer(key){
this.ActiveServerConfiguration = key;
localStorage.setItem('ActiveCrossServerConfiguration', key)
},
removeActiveCrossServer(){
this.ActiveServerConfiguration = undefined;
localStorage.removeItem('ActiveCrossServerConfiguration')
},
async getConfiguration(){
await fetchGet("/api/getDashboardConfiguration", {}, (res) => {
if (res.status) this.Configuration = res.data
@@ -27,6 +71,7 @@ export const DashboardConfigurationStore = defineStore('DashboardConfigurationSt
},
async signOut(){
await fetchGet("/api/signout", {}, (res) => {
this.removeActiveCrossServer();
this.$router.go('/signin')
});
},
+30 -10
View File
@@ -1,18 +1,38 @@
import router from "@/router/index.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
const getHeaders = () => {
let headers = {
"content-type": "application/json"
}
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
headers['wg-dashboard-apikey'] = apiKey.apiKey
}
return headers
}
const getUrl = (url) => {
const store = DashboardConfigurationStore();
const apiKey = store.getActiveCrossServer();
if (apiKey){
return `${apiKey.host}${url}`
}
return url
}
export const fetchGet = async (url, params=undefined, callback=undefined) => {
const urlSearchParams = new URLSearchParams(params);
await fetch(`${url}?${urlSearchParams.toString()}`, {
headers: {
"content-type": "application/json"
}
await fetch(`${getUrl(url)}?${urlSearchParams.toString()}`, {
headers: getHeaders()
})
.then((x) => {
const store = DashboardConfigurationStore();
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
router.push({path: '/signin'})
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
@@ -22,14 +42,13 @@ export const fetchGet = async (url, params=undefined, callback=undefined) => {
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
router.push({path: '/signin'})
})
}
export const fetchPost = async (url, body, callback) => {
await fetch(`${url}`, {
headers: {
"content-type": "application/json"
},
await fetch(`${getUrl(url)}`, {
headers: getHeaders(),
method: "POST",
body: JSON.stringify(body)
}).then((x) => {
@@ -37,7 +56,7 @@ export const fetchPost = async (url, body, callback) => {
if (!x.ok){
if (x.status !== 200){
if (x.status === 401){
router.push({path: '/signin'})
store.newMessage("WGDashboard", "Session Ended", "warning")
}
throw new Error(x.statusText)
@@ -47,5 +66,6 @@ export const fetchPost = async (url, body, callback) => {
}
}).then(x => callback ? callback(x) : undefined).catch(x => {
console.log(x)
router.push({path: '/signin'})
})
}
@@ -56,7 +56,7 @@ export default {
this.errorMessage = res.message;
document.querySelector(`#${res.data}`).classList.remove("is-valid")
document.querySelector(`#${res.data}`).classList.add("is-invalid")
this.loading = false;
}
})
}
@@ -126,15 +126,14 @@ export default {
</script>
<template>
<div class="mt-4">
<div class="mt-5">
<div class="container mb-4">
<div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="/">
<RouterLink to="/" class="text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left"></i>
<i class="bi bi-chevron-left me-4"></i> New Configuration
</h3>
</RouterLink>
<h3 class="text-body mb-0">New Configuration</h3>
</div>
<form class="text-body d-flex flex-column gap-3"
+2 -2
View File
@@ -77,8 +77,8 @@ export default {
<AccountSettingsInputPassword
targetData="password">
</AccountSettingsInputPassword>
<hr class="m-0">
<AccountSettingsMFA></AccountSettingsMFA>
<hr class="m-0" v-if="!this.dashboardConfigurationStore.getActiveCrossServer()">
<AccountSettingsMFA v-if="!this.dashboardConfigurationStore.getActiveCrossServer()"></AccountSettingsMFA>
</div>
</div>
<DashboardAPIKeys></DashboardAPIKeys>
+68 -47
View File
@@ -2,20 +2,24 @@
import {fetchGet, fetchPost} from "../utilities/fetch.js";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import Message from "@/components/messageCentreComponent/message.vue";
import RemoteServerList from "@/components/signInComponents/RemoteServerList.vue";
export default {
name: "signin",
components: {Message},
components: {RemoteServerList, Message},
async setup(){
const store = DashboardConfigurationStore()
let theme = ""
let theme = "dark"
let totpEnabled = false;
await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme = res.data
});
await fetchGet("/api/isTotpEnabled", {}, (res) => {
totpEnabled = res.data
});
if (!store.IsElectronApp){
await fetchGet("/api/getDashboardTheme", {}, (res) => {
theme = res.data
});
await fetchGet("/api/isTotpEnabled", {}, (res) => {
totpEnabled = res.data
});
}
store.removeActiveCrossServer();
return {store, theme, totpEnabled}
},
data(){
@@ -82,57 +86,66 @@ export default {
</script>
<template>
<div class="container-fluid login-container-fluid d-flex main flex-column" :data-bs-theme="this.theme">
<div class="login-box m-auto" style="width: 600px;">
<div class="m-auto">
<div class="card px-4 py-5 rounded-4 shadow-lg">
<div class="card-body">
<h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3"><strong>WGDashboard</strong></span>
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
{{this.loginErrorMessage}}
</div>
<form @submit="(e) => {e.preventDefault(); this.auth();}">
<div class="form-group text-body">
<label for="username" class="text-left" style="font-size: 1rem">
<i class="bi bi-person-circle"></i></label>
<input type="text" v-model="username" class="form-control" id="username" name="username"
autocomplete="on"
placeholder="Username" required>
</div>
<div class="form-group text-body">
<label for="password" class="text-left" style="font-size: 1rem"><i class="bi bi-key-fill"></i></label>
<input type="password"
v-model="password" class="form-control" id="password" name="password"
autocomplete="on"
placeholder="Password" required>
</div>
<div class="form-group text-body" v-if="totpEnabled">
<label for="totp" class="text-left" style="font-size: 1rem"><i class="bi bi-lock-fill"></i></label>
<input class="form-control totp"
required
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
placeholder="OTP from your authenticator"
v-model="this.totp"
>
</div>
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand signInBtn" ref="signInBtn">
<div class="container-fluid login-container-fluid d-flex main flex-column py-4 text-body"
style="overflow-y: scroll"
:data-bs-theme="this.theme">
<div class="login-box m-auto" >
<div class="m-auto" style="width: 700px;">
<h4 class="mb-0 text-body">Welcome to</h4>
<span class="dashboardLogo display-3"><strong>WGDashboard</strong></span>
<div class="alert alert-danger mt-2 mb-0" role="alert" v-if="loginError">
{{this.loginErrorMessage}}
</div>
<form @submit="(e) => {e.preventDefault(); this.auth();}"
v-if="!this.store.CrossServerConfiguration.Enable">
<div class="form-group text-body">
<label for="username" class="text-left" style="font-size: 1rem">
<i class="bi bi-person-circle"></i></label>
<input type="text" v-model="username" class="form-control" id="username" name="username"
autocomplete="on"
placeholder="Username" required>
</div>
<div class="form-group text-body">
<label for="password" class="text-left" style="font-size: 1rem"><i class="bi bi-key-fill"></i></label>
<input type="password"
v-model="password" class="form-control" id="password" name="password"
autocomplete="on"
placeholder="Password" required>
</div>
<div class="form-group text-body" v-if="totpEnabled">
<label for="totp" class="text-left" style="font-size: 1rem"><i class="bi bi-lock-fill"></i></label>
<input class="form-control totp"
required
id="totp" maxlength="6" type="text" inputmode="numeric" autocomplete="one-time-code"
placeholder="OTP from your authenticator"
v-model="this.totp"
>
</div>
<button class="btn btn-lg btn-dark ms-auto mt-4 w-100 d-flex btn-brand signInBtn" ref="signInBtn">
<span v-if="!this.loading" class="d-flex w-100">
Sign In<i class="ms-auto bi bi-chevron-right"></i>
</span>
<span v-else class="d-flex w-100 align-items-center">
<span v-else class="d-flex w-100 align-items-center">
Signing In...
<span class="spinner-border ms-auto spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</span>
</span>
</button>
</form>
</button>
</form>
<RemoteServerList v-else></RemoteServerList>
<div class="d-flex mt-3" v-if="!this.store.IsElectronApp">
<div class="form-check form-switch ms-auto">
<input
v-model="this.store.CrossServerConfiguration.Enable"
class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked">
<label class="form-check-label" for="flexSwitchCheckChecked">Access Remote Server</label>
</div>
</div>
</div>
</div>
<small class="text-muted pb-3 d-block w-100 text-center">
<small class="text-muted pb-3 d-block w-100 text-center mt-3">
WGDashboard v4.0 | Developed with by
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
</small>
@@ -146,5 +159,13 @@ export default {
</template>
<style scoped>
@media screen and (max-width: 768px) {
.login-box{
width: 100% !important;
}
.login-box div{
width: auto !important;
}
}
</style>
+51 -20
View File
@@ -5,27 +5,58 @@ import {proxy} from "./proxy.js";
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
base: "/static/app/dist",
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
export default defineConfig(({mode}) => {
if (mode === 'electron'){
return {
emptyOutDir: false,
base: './',
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: '../../../../WGDashboard-Desktop',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
}
}
},
server:{
proxy: {
'/api': proxy
}
},
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
return {
base: "/static/app/dist",
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
proxy: {
'/api': proxy
},
host: '0.0.0.0'
},
build: {
outDir: 'dist',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
/**{*/
/* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";*/
/*}*/
*{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.dp__input{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
+8 -4
View File
@@ -1,14 +1,18 @@
[Unit]
After=syslog.target network-online.target
Wants=wg-quick.target
ConditionPathIsDirectory=/etc/wireguard
[Service]
Environment="VIRTUAL_ENV={{VIRTUAL_ENV}}"
WorkingDirectory={{APP_ROOT}}
ExecStart={{VIRTUAL_ENV}}/bin/python3 {{APP_ROOT}}/dashboard.py
Type=forking
PIDFile=/opt/wgdashboard/src/gunicorn.pid
WorkingDirectory=/opt/wgdashboard/src
ExecStart=/opt/wgdashboard/src/wgd.sh start
ExecStop=/opt/wgdashboard/src/wgd.sh stop
ExecReload=/opt/wgdashboard/src/wgd.sh restart
TimeoutSec=120
PrivateTmp=yes
Restart=always
[Install]
WantedBy=multi-user.target
+9 -6
View File
@@ -180,16 +180,19 @@ install_wgd(){
else
printf "[WGDashboard] \xE2\x9C\x94 Python is installed\n"
fi
version_pass=$(python3 -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 10) else print("0");')
if [ $version_pass == "0" ]
then
printf "[WGDashboard] WGDashboard required Python 3.10 or above\n"
exit 1
fi
_installPythonVenv
_installPythonPip
version_pass=$(python3 -c 'import sys; print("1") if (sys.version_info.major == 3 and sys.version_info.minor >= 10) else print("0");')
if [ $version_pass == "0" ]
then
printf "[WGDashboard] WGDashboard required Python 3.7 or above\n"
exit 1
fi
if [ ! -d "db" ]
then