Major improvements incl. CSS

This commit is contained in:
Henri 2020-11-26 23:21:14 +01:00
parent 3cde7d371d
commit 16ce040ab9
14 changed files with 642 additions and 145 deletions

View File

@ -10,12 +10,26 @@ All notable changes to this project are documented in this file.
### Changed
- n/a
- Improve button naming when adding new client
### Fixed
- n/a
## [0.2.0] - 2020-11-26
### Added
- CSS stylesheet for nicer appearance
### Changed
- Improved user guidance
### Fixed
- Release with activated session handling
## [0.1.0] - 2020-11-19
### Added

View File

@ -2,9 +2,9 @@
A simple web frontend for configuring peers within a WireGuard configuration file to thus administer road warrior clients.
There are lot of user interfaces for administering WireGuard configuration files available. However, many of them have a bunch of dependencies, require root privileges to operate, or are a hassle to set up. "wgfrontend" provides a user interface that can be easily installed by just installing a package from Python's package repository PyPi (i.e. using pip).
There are already a lot of user interfaces for administering WireGuard configuration files available. However, many of them have a bunch of dependencies, require root privileges to operate, or are a hassle to set up. "wgfrontend" provides a user interface that can be easily installed by just installing a package from Python's package repository PyPi (i.e. using pip).
IMPORTANT NOTE: This tool is in an early development stage. Be warned. It is already working but looking "ugly" (the user interface does not have any nice formatting or CSS yet).
IMPORTANT NOTE: This tool is still in development stage. Bug reports are appreciated.
This little tool is independent of the Towalink site connectivity solution (see https://towalink.readthedocs.io).
@ -16,6 +16,7 @@ This little tool is independent of the Towalink site connectivity solution (see
- Config files for WireGuard peers can be downloaded
- Config files for WireGuard peers are shown as QR Code
- Assistant for initial set-up
- Web frontend has responsive design
- Web frontend does not run with root privileges
- Simple installation

View File

@ -7,7 +7,7 @@ with open('README.md', 'r') as f:
setup_kwargs = {
'name': 'wgfrontend',
'version': '0.1.0',
'version': '0.2.0',
'author': 'The Towalink Project',
'author_email': 'pypi.wgfrontend@towalink.net',
'description': 'web-based user interface for configuring WireGuard for roadwarriors',

View File

@ -1,15 +1,20 @@
<html>
<head>
<meta http-equiv='content-type' content='text/html; charset=utf-8' />
<title>Towalink WireGuard Frontend</title>
<link rel='stylesheet' media='screen' href='/static/layout.css' />
</head>
<body>
<div class='container'>
<div class='header'>
{% include 'part_header.html' %}
</div>
<div class='content'>{% block content %}{% endblock %}</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Towalink WireGuard Frontend</title>
<link rel="stylesheet" media="screen" href="/static/styles.css" />
</head>
<body>
{% include 'part_header.html' %}
<section>
<div class="content">
{%- block content %}{% endblock %}
</div>
</body>
</section>
<footer>
<a href="https://github.com/towalink/wgfrontend/" target="_blank">Powered by the open source &quot;Towalink WireGuard Frontend&quot;</a>
</footer>
</body>
</html>

View File

@ -1,24 +1,29 @@
{% extends 'base.html' %}
{% block content %}
<h3>Client Config</h3>
<div class='instructions'>
<a href='https://www.wireguard.com/install/' target="_blank">WireGuard client installation instructions</a>
</div>
<div class='span-18 last'>
<form method="get" action="download">
<table>
<tr>
<td>
{{ peerdata['Description'] }}<br>
{{ peerdata['Address'] }}
</td>
<td>
<img src="/configs/{{ peerdata['Id'] }}.png" alt="QR Code">
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Download Config</button>
<button type="submit" formaction=".." }}">Return</button>
</td>
</tr>
</table>
</form>
</div>
<h3>Client Config</h3>
<div class='form'>
<form method="get" action="download">
<div class="buttonrow">
<button class="button buttonhighlight" type="submit" name="action" value="list" formaction="..">Return to List</button>
</div>
<div class="table">
<div class="table-row">
<div class="table-cell bordertop">
{{ peerdata['Description'] }}<br>
<small>{{ peerdata['Address'] }}</small><br>
</div>
<div class="table-cell twobuttoncell bordertop2">
<button class="button" type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="edit">Edit Client</button>
<button class="button" type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="download">Download Config</button>
</div>
</div>
<div class="table-row" style="text-align: center;">
<img class="qrcode" src="/configs/{{ peerdata['Id'] }}.png" alt="QR Code">
</div>
</div>
</form>
</div>
<div class='instructions'>
See the <a href='https://www.wireguard.com/install/' target="_blank">WireGuard client installation instructions</a>.
</div>
{% endblock %}

View File

@ -1,22 +1,28 @@
{% extends 'base.html' %}
{% block content %}
<h3>Configured Clients</h3>
<div class='span-18 last'>
<form method="get" action="edit">
<table>
<tr>
<td>
<input type="hidden" name="id" value="{{ peerdata['Id'] }}" />
<input type="text" name="description" value="{{ peerdata['Description'] }}" size="40" /><br>
{{ peerdata['Address'] }}
</td>
<td>
<button type="submit" name="action" value="save">Save Data</button>
<button type="submit" name="action" value="delete" formaction="/" onclick="return confirm('Do you really want to delete this client?')">Delete Client</button>
<button type="submit" formaction="/">Return to List</button>
</td>
</tr>
</table>
</form>
</div>
<h3>{% if peerdata['Id'] %}Edit{% else %}New{% endif %} Client</h3>
<div class='form'>
<form method="get" action="..">
<div class="buttonrow">
<button class="button buttonhighlight" type="submit" name="action" value="list">Return to List</button>
</div>
<div class="table">
<div class="table-row">
<div class="table-cell bordertop">
<input type="hidden" name="id" value="{{ peerdata['Id'] }}" />
<input class="inputtext" type="text" name="description" value="{{ peerdata['Description'] }}" size="40" /><br>
<small>{{ peerdata['Address'] }}</small>
</div>
<div class="table-cell twobuttoncell bordertop2">
<button class="button" type="submit" name="action" value="save" formaction="config">{% if peerdata['Id'] %}Save Changes{% else %}Save{%endif %}</button>
{%- if peerdata['Id'] %}
<button class="button" type="submit" name="action" value="delete" onclick="return confirm('Do you really want to delete this client?')">Delete Client</button>
{%- else %}
<button class="button" type="submit" name="action" value="list")">Cancel</button>
{%- endif %}
</div>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,23 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<h3>Configured Clients</h3>
<div class='span-18 last'>
<form method="get" action="edit">
<button type="submit" name="action" value="new">Add Client</button>
<table>
{%- for peer, peerdata in peers.items()|sort(attribute='1.Description') %}
<tr>
<td>
{{ peerdata['Description'] }}<br>
{{ peerdata['Address'] }}
</td>
<td>
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Edit Client</button>
<button type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="config">Get Config</button>
</td>
</tr>
{%- endfor %}
</table>
</form>
</div>
{% endblock %}
<h3>Configured Clients</h3>
<div class='form'>
<form method="get" action="edit">
<div class="buttonrow">
<button class="button buttonhighlight" type="submit" name="action" value="new">Add Client</button>
</div>
<div class="table">
{%- for peer, peerdata in peers.items()|sort(attribute='1.Description') %}
<div class="line"></div>
<div class="table-row">
<div class="table-cell bordertop">
{{ peerdata['Description'] }}<br>
<small>{{ peerdata['Address'] }}</small>
</div>
<div class="table-cell twobuttoncell bordertop2">
<button class="button" type="submit" name="id" value="{{ peerdata['Id'] }}">Edit Client</button>
<button class="button" type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="config">Get Config</button>
</div>
</div>
{%- endfor %}
{% if not peers %}
<div class="table">
<div class="line"></div>
<div class="table-row">
<div class="table-cell bordertop">
There is no client configured up to now.
</div>
</div>
{% endif %}
</div>
</form>
</div>
{% endblock %}

39
src/templates/login.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Towalink WireGuard Frontend - Login</title>
<link rel="stylesheet" media="screen" href="/static/styles.css" />
</head>
<body>
<div class="loginform">
<form method="post" action="do_login">
<div class=loginerror>{{ error_msg }}</div>
<table>
<tr>
<td>
User:
</td>
<td>
<input class="logininput" type="text" name="username" value="{{ username }}" size="40" />
</td>
<tr>
<td>
Password:&nbsp;&nbsp;
</td>
<td>
<input class="logininput" type="password" name="password" size="40" />
<input type="hidden" name="from_page" value="{{ from_page }}" />
</td>
</tr>
<tr>
<td colspan=2 align=right>
<input class="button loginmargin" type="submit" value="Login" />
</td>
</tr>
</table>
</form>
</div>
</body>
</html>

View File

@ -1,3 +1,9 @@
<div class='heading'>
<h2><a href='/'>Towalink WireGuard Frontend</a></h2>
</div>
<header>
<h2><a href="/"><img src="static/logo.svg" alt="Towalink logo"><span>Towalink WireGuard Frontend</span></a></h2>
<div class="user">
<p>{% if sessiondata['username'] %}{{ sessiondata['username'] }}{% else %}not logged in{%endif %}</p>
<form action="/logout">
<input class="button" type="submit" value="Logout" />
</form>
</div>
</header>

View File

@ -12,49 +12,12 @@ import pwdtools
import wgcfg
def login_screen(from_page='..', username='', error_msg='', **kwargs):
"""Based on https://docs.cherrypy.org/en/latest/_modules/cherrypy/lib/cptools.html"""
content="""
<form method="post" action="do_login">
<div align=center>
<span class=errormsg>%s</span>
<table>
<tr>
<td>
Login:
</td>
<td>
<input type="text" name="username" value="%s" size="40" />
</td>
<tr>
<td>
Password:
</td>
<td>
<input type="password" name="password" size="40" />
<input type="hidden" name="from_page" value="%s" />
</td>
</tr>
<tr>
<td colspan=2 align=right>
<input type="submit" value="Login" />
</td>
</tr>
</table>
</div>
</form>
""" % (error_msg, username, from_page)
title='Login'
return ('<html><body>' + content + '</body></html>').encode('utf-8')
# return cherrypy.tools.encode('<html><body>' + content + '</body></html>')
class WebApp():
def __init__(self, cfg):
"""Instance initialization"""
self.cfg = cfg
self.jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
self.jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')))
self.wg = wgcfg.WGCfg(self.cfg.wg_configfile, self.cfg.libdir)
@cherrypy.expose
@ -64,13 +27,21 @@ class WebApp():
self.wg.delete_peer(peer)
peers = self.wg.get_peers()
tmpl = self.jinja_env.get_template('index.html')
return tmpl.render(peers=peers)
return tmpl.render(sessiondata=cherrypy.session, peers=peers)
@cherrypy.expose
def config(self, id):
peer, peerdata = self.wg.get_peer_byid(id)
def config(self, action=None, id=None, description=None):
peerdata = None
if (action == 'save') and id:
peer, peerdata = self.wg.get_peer_byid(id)
peerdata = self.wg.update_peer(peer, description)
if (action == 'save') and not id:
peer = self.wg.create_peer(description)
peerdata = self.wg.get_peer(peer)
if not peerdata:
peer, peerdata = self.wg.get_peer_byid(id)
tmpl = self.jinja_env.get_template('config.html')
return tmpl.render(peerdata=peerdata)
return tmpl.render(sessiondata=cherrypy.session, peerdata=peerdata)
@cherrypy.expose
def edit(self, action='edit', id=None, description=None):
@ -84,43 +55,52 @@ class WebApp():
if action == 'new': # default values for new client
peerdata = { 'Description': description, 'Id': '' }
else: # save changes
peer = self.wg.create_peer(description)
peerdata = self.wg.get_peer(peer)
raise ValueError()
tmpl = self.jinja_env.get_template('edit.html')
return tmpl.render(peerdata=peerdata)
return tmpl.render(sessiondata=cherrypy.session, peerdata=peerdata)
@cherrypy.expose
def download(self, id):
"""Provide the WireGuard config for the client with the given identifier for download"""
peer, peerdata = self.wg.get_peer_byid(id)
config, peerdata = self.wg.get_peerconfig(peer)
cherrypy.response.headers['Content-Disposition'] = f'attachment; filename=wg_{id}.conf'
cherrypy.response.headers['Content-Type'] = 'text/plain' # 'application/x-download' 'application/octet-stream'
return config.encode('utf-8')
@cherrypy.expose
def logout(self):
username = cherrypy.session['username']
cherrypy.session.clear()
return '"{0}" has been logged out'.format(username)
def run_webapp(cfg):
def check_username_and_password(username, password):
def check_username_and_password(self, username, password):
"""Check whether provided username and password are valid when authenticating"""
if (username in cfg.users) and (pwdtools.verify_password(cfg.users[username], password)):
if (username in self.cfg.users) and (pwdtools.verify_password(self.cfg.users[username], password)):
return
return 'invalid username/password'
def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
"""Shows a login form"""
tmpl = self.jinja_env.get_template('login.html')
return tmpl.render(from_page=from_page, username=username, error_msg=error_msg).encode('utf-8')
@cherrypy.expose
def logout(self):
username = cherrypy.session['username']
cherrypy.session.clear()
cherrypy.response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
cherrypy.response.headers['Pragma'] = 'no-cache'
cherrypy.response.headers['Expires'] = '0'
raise cherrypy.HTTPRedirect('/', 302)
return '"{0}" has been logged out'.format(username)
def run_webapp(cfg):
"""Runs the CherryPy web application with the provided configuration data"""
script_path = os.path.dirname(os.path.abspath(__file__))
conf = {
app = WebApp(cfg)
app_conf = {
'/': {
'tools.sessions.on': True,
'tools.staticdir.root': os.path.join(script_path, 'webroot'),
# 'tools.session_auth.on': True,
'tools.session_auth.login_screen': login_screen,
'tools.session_auth.check_username_and_password': check_username_and_password,
'tools.session_auth.on': True,
'tools.session_auth.login_screen': app.login_screen,
'tools.session_auth.check_username_and_password': app.check_username_and_password,
},
'/configs': {
'tools.staticdir.on': True,
@ -128,11 +108,13 @@ def run_webapp(cfg):
'tools.staticdir.dir': cfg.libdir
},
'/static': {
'tools.session_auth.on': False,
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static'
},
'/favicon.ico':
{
'tools.session_auth.on': False,
'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.join(script_path, 'webroot', 'static', 'favicon.ico')
}
@ -144,7 +126,7 @@ def run_webapp(cfg):
cherrypy.config.update({'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
})
cherrypy.quickstart(WebApp(cfg), '/', conf)
cherrypy.quickstart(app, '/', app_conf)
if __name__ == '__main__':

203
src/webroot/static/logo.svg Normal file
View File

@ -0,0 +1,203 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="35mm"
height="35mm"
viewBox="0 0 124.01573 124.01573"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="20200430_Logo_white.svg">
<defs
id="defs4">
<marker
inkscape:stockid="Arrow1Send"
orient="auto"
refY="0"
refX="0"
id="Arrow1Send"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path4527"
d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
style="fill:#0026ff;fill-opacity:1;fill-rule:evenodd;stroke:#0026ff;stroke-width:1pt;stroke-opacity:1"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow2Mend"
orient="auto"
refY="0"
refX="0"
id="Arrow2Mend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path4539"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
transform="scale(-0.6,-0.6)"
inkscape:connector-curvature="0" />
</marker>
<marker
inkscape:stockid="Arrow1Send"
orient="auto"
refY="0"
refX="0"
id="Arrow1Send-0"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path4527-5"
d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
style="fill:#0026ff;fill-opacity:1;fill-rule:evenodd;stroke:#0026ff;stroke-width:1pt;stroke-opacity:1"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
inkscape:stockid="Arrow1Send"
orient="auto"
refY="0"
refX="0"
id="Arrow1Send-0-6"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path4527-5-6"
d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
style="fill:#0026ff;fill-opacity:1;fill-rule:evenodd;stroke:#0026ff;stroke-width:1pt;stroke-opacity:1"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
inkscape:stockid="Arrow1Send"
orient="auto"
refY="0"
refX="0"
id="Arrow1Send-02"
style="overflow:visible"
inkscape:isstock="true">
<path
inkscape:connector-curvature="0"
id="path4527-2"
d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
style="fill:#0026ff;fill-opacity:1;fill-rule:evenodd;stroke:#0026ff;stroke-width:1pt;stroke-opacity:1"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.310501"
inkscape:cx="182.65431"
inkscape:cy="138.66991"
inkscape:document-units="px"
inkscape:current-layer="g5661"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1033"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1"
borderlayer="false" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-928.34647)">
<g
id="g5661">
<rect
y="261.19177"
x="207.31435"
height="71.413086"
width="276.99622"
id="rect4456"
style="opacity:1;fill:none;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="941.92017"
x="86.271614"
height="39.020161"
width="61.361115"
id="rect4476"
style="opacity:1;fill:none;fill-opacity:1;stroke:none;stroke-width:2.70799994;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(0.99987663,-0.01570731,0.01570731,0.99987663,0,0)" />
<rect
y="946.04987"
x="56.911366"
height="43.763786"
width="61.973194"
id="rect4479"
style="opacity:1;fill:none;fill-opacity:1;stroke:none;stroke-width:2.70799994;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(0.99987663,-0.01570731,0.01570731,0.99987663,0,0)" />
<rect
y="939.16394"
x="61.808002"
height="45.293987"
width="62.585274"
id="rect4482"
style="opacity:1;fill:none;fill-opacity:1;stroke:none;stroke-width:2.70799994;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(0.99987663,-0.01570731,0.01570731,0.99987663,0,0)" />
<path
inkscape:transform-center-y="-16.366332"
inkscape:transform-center-x="0.37613837"
d="m 423.28484,447.29862 c 7.63877,-13.12302 -104.87801,16.59839 -91.75499,24.23716 13.12301,7.63877 -16.5984,-104.878 -24.23717,-91.75499 -7.63877,13.12302 104.878,-16.5984 91.75499,-24.23717 -13.12302,-7.63877 16.5984,104.87801 24.23717,91.755 z"
inkscape:randomized="0"
inkscape:rounded="-0.16"
inkscape:flatsided="true"
sodipodi:arg2="1.3125441"
sodipodi:arg1="0.52714594"
sodipodi:r2="34.365295"
sodipodi:r1="67.105957"
sodipodi:cy="413.5397"
sodipodi:cx="365.28876"
sodipodi:sides="4"
id="path5631"
style="opacity:1;fill:none;fill-opacity:1;stroke:#0026ff;stroke-width:6;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
sodipodi:type="star"
transform="matrix(0.99987664,-0.01570731,0.01570731,0.99987664,-309.89111,582.58709)" />
<circle
r="10.387358"
cy="991.18738"
cx="46.285023"
id="path5633"
style="opacity:1;fill:#0026ff;fill-opacity:1;stroke:#0026ff;stroke-width:6;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(0.99987663,-0.01570731,0.01570731,0.99987663,0,0)" />
<path
inkscape:connector-curvature="0"
id="path5635"
d="m 7.8998124,959.82946 103.0679076,58.11564 0,0"
style="fill:none;fill-rule:evenodd;stroke:#0026ff;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5635-8"
d="m 89.387832,941.50106 -58.11557,103.06794 0,0"
style="fill:none;fill-rule:evenodd;stroke:#0026ff;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,218 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font: normal 16px sans-serif;
background-color: #f3f3f3;
color: #000;
}
a {
text-decoration: none;
color: inherit;
cursor: pointer;
opacity: 0.9;
}
a:hover {
text-decoration: underline;
opacity: 1;
}
.button {
background-color: blue;
border: none;
color: white;
padding: 4px 2px;
width: 110px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
margin: 2px 0px;
}
.button:hover {
background-color: #002db3;
color: white;
}
.buttonhighlight {
background-color: #ff7700;
}
.inputtext {
background-color: white;
border: 1px solid gray;
color: black;
padding: 4px 5px;
width: 95%;
text-align: left;
text-decoration: none;
display: inline-block;
font-size: 12px;
}
.loginform {
margin: auto;
margin-top: 100px;
width: 382px;
border: 2px solid lightblue;
padding: 10px 10px;
font-size: 16px;
align-items: center;
}
.loginerror {
width: 100%;
text-align: center;
color: red;
margin: 5px 10px 10px 10px;
font-size: 16px;
}
.logininput {
border: 1px solid gray;
color: black;
padding: 3px 5px;
margin: 1px 0;
text-align: left;
text-decoration: none;
font-size: 12px;
}
.loginmargin {
margin-top: 10px;
margin-bottom: 8px;
}
header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
padding: 10px 250px;
background-color: #dddddd;
}
header h2 {
font-family: Arial, Helvetica, sans-serif;
font-size: 32px;
color: blue;
}
header h2 img {
height: 35px;
float: left;
margin-right: 10px;
}
.user {
color: blue;
font-size: 14px;
}
section {
width: 100%;
display: flex;
padding: 10px 250px;
background-color: #f3f3f3;
}
.content {
width: 100%;
}
.buttonrow {
text-align: right;
padding: 0px 0px 5px 0px;
}
.table {
display: table;
width: 100%;
}
.table-row {
display: table-row;
width: 100%;
}
.table-cell {
display: table-cell;
padding: 5px 0px;
}
.bordertop{
border-top: 1px solid gray;
}
.bordertop2{
border-top: 1px solid gray;
}
.twobuttoncell {
width: 225px;
text-align: right;
}
.qrcode {
padding: 5px 5px;
}
.instructions {
font-size: 14px;
}
@media screen and (max-width: 1000px) {
.table, .table-row {
display: block;
}
.table-cell {
display: block;
}
.bordertop2{
border-top: 0;
}
}
footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #dddddd;
color: blue;
font-size: 12px;
text-align: center;
padding: 3px 5px;
}
@media screen and (max-width: 1200px) {
header {
padding: 10px 100px;
}
section {
padding: 10px 100px;
}
}
@media screen and (max-width: 500px) {
header {
flex-direction: column;
padding: 10px 10px;
}
header h2 {
margin-bottom: 15px;
}
section {
flex-direction: column;
padding: 10px 10px;
}
}

View File

@ -1 +0,0 @@
TEST

View File

@ -50,6 +50,8 @@ class WGCfg():
def get_peer(self, peer):
"""Get data of the given WireGuard peer"""
if peer is None:
return None
return self.transform_to_clientdata(peer, self.wc.peers[peer])
def get_peers(self):
@ -58,11 +60,16 @@ class WGCfg():
def get_peer_byid(self, id):
"""Get data WireGuard peer with the given id"""
peer = next(peer for peer, peerdata in self.get_peers().items() if peerdata['Id'] == id)
try:
peer = next(peer for peer, peerdata in self.get_peers().items() if peerdata['Id'] == id)
except StopIteration:
peer = None
return peer, self.get_peer(peer)
def get_peerconfig(self, peer):
"""Get config for the given WireGuard peer"""
if peer is None:
return None, None
peerdata = self.get_peer(peer)
for item in self.get_interface()['_rawdata']:
if item.startswith('# Endpoint = '):