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 ### Changed
- n/a - Improve button naming when adding new client
### Fixed ### Fixed
- n/a - 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 ## [0.1.0] - 2020-11-19
### Added ### 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. 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). 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 can be downloaded
- Config files for WireGuard peers are shown as QR Code - Config files for WireGuard peers are shown as QR Code
- Assistant for initial set-up - Assistant for initial set-up
- Web frontend has responsive design
- Web frontend does not run with root privileges - Web frontend does not run with root privileges
- Simple installation - Simple installation

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,35 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<h3>Configured Clients</h3> <h3>Configured Clients</h3>
<div class='span-18 last'> <div class='form'>
<form method="get" action="edit"> <form method="get" action="edit">
<button type="submit" name="action" value="new">Add Client</button> <div class="buttonrow">
<table> <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') %} {%- for peer, peerdata in peers.items()|sort(attribute='1.Description') %}
<tr> <div class="line"></div>
<td> <div class="table-row">
<div class="table-cell bordertop">
{{ peerdata['Description'] }}<br> {{ peerdata['Description'] }}<br>
{{ peerdata['Address'] }} <small>{{ peerdata['Address'] }}</small>
</td> </div>
<td> <div class="table-cell twobuttoncell bordertop2">
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Edit Client</button> <button class="button" type="submit" name="id" value="{{ peerdata['Id'] }}">Edit Client</button>
<button type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="config">Get Config</button> <button class="button" type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="config">Get Config</button>
</td> </div>
</tr> </div>
{%- endfor %} {%- endfor %}
</table> {% 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> </form>
</div> </div>
{% endblock %} {% 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'> <header>
<h2><a href='/'>Towalink WireGuard Frontend</a></h2> <h2><a href="/"><img src="static/logo.svg" alt="Towalink logo"><span>Towalink WireGuard Frontend</span></a></h2>
</div> <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 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(): class WebApp():
def __init__(self, cfg): def __init__(self, cfg):
"""Instance initialization""" """Instance initialization"""
self.cfg = cfg 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) self.wg = wgcfg.WGCfg(self.cfg.wg_configfile, self.cfg.libdir)
@cherrypy.expose @cherrypy.expose
@ -64,13 +27,21 @@ class WebApp():
self.wg.delete_peer(peer) self.wg.delete_peer(peer)
peers = self.wg.get_peers() peers = self.wg.get_peers()
tmpl = self.jinja_env.get_template('index.html') tmpl = self.jinja_env.get_template('index.html')
return tmpl.render(peers=peers) return tmpl.render(sessiondata=cherrypy.session, peers=peers)
@cherrypy.expose @cherrypy.expose
def config(self, 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) peer, peerdata = self.wg.get_peer_byid(id)
tmpl = self.jinja_env.get_template('config.html') tmpl = self.jinja_env.get_template('config.html')
return tmpl.render(peerdata=peerdata) return tmpl.render(sessiondata=cherrypy.session, peerdata=peerdata)
@cherrypy.expose @cherrypy.expose
def edit(self, action='edit', id=None, description=None): def edit(self, action='edit', id=None, description=None):
@ -84,43 +55,52 @@ class WebApp():
if action == 'new': # default values for new client if action == 'new': # default values for new client
peerdata = { 'Description': description, 'Id': '' } peerdata = { 'Description': description, 'Id': '' }
else: # save changes else: # save changes
peer = self.wg.create_peer(description) raise ValueError()
peerdata = self.wg.get_peer(peer)
tmpl = self.jinja_env.get_template('edit.html') tmpl = self.jinja_env.get_template('edit.html')
return tmpl.render(peerdata=peerdata) return tmpl.render(sessiondata=cherrypy.session, peerdata=peerdata)
@cherrypy.expose @cherrypy.expose
def download(self, id): 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) peer, peerdata = self.wg.get_peer_byid(id)
config, peerdata = self.wg.get_peerconfig(peer) config, peerdata = self.wg.get_peerconfig(peer)
cherrypy.response.headers['Content-Disposition'] = f'attachment; filename=wg_{id}.conf' cherrypy.response.headers['Content-Disposition'] = f'attachment; filename=wg_{id}.conf'
cherrypy.response.headers['Content-Type'] = 'text/plain' # 'application/x-download' 'application/octet-stream' cherrypy.response.headers['Content-Type'] = 'text/plain' # 'application/x-download' 'application/octet-stream'
return config.encode('utf-8') return config.encode('utf-8')
def check_username_and_password(self, username, password):
"""Check whether provided username and password are valid when authenticating"""
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 @cherrypy.expose
def logout(self): def logout(self):
username = cherrypy.session['username'] username = cherrypy.session['username']
cherrypy.session.clear() 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) return '"{0}" has been logged out'.format(username)
def run_webapp(cfg): def run_webapp(cfg):
"""Runs the CherryPy web application with the provided configuration data"""
def check_username_and_password(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)):
return
return 'invalid username/password'
script_path = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.dirname(os.path.abspath(__file__))
conf = { app = WebApp(cfg)
app_conf = {
'/': { '/': {
'tools.sessions.on': True, 'tools.sessions.on': True,
'tools.staticdir.root': os.path.join(script_path, 'webroot'), 'tools.staticdir.root': os.path.join(script_path, 'webroot'),
# 'tools.session_auth.on': True, 'tools.session_auth.on': True,
'tools.session_auth.login_screen': login_screen, 'tools.session_auth.login_screen': app.login_screen,
'tools.session_auth.check_username_and_password': check_username_and_password, 'tools.session_auth.check_username_and_password': app.check_username_and_password,
}, },
'/configs': { '/configs': {
'tools.staticdir.on': True, 'tools.staticdir.on': True,
@ -128,11 +108,13 @@ def run_webapp(cfg):
'tools.staticdir.dir': cfg.libdir 'tools.staticdir.dir': cfg.libdir
}, },
'/static': { '/static': {
'tools.session_auth.on': False,
'tools.staticdir.on': True, 'tools.staticdir.on': True,
'tools.staticdir.dir': 'static' 'tools.staticdir.dir': 'static'
}, },
'/favicon.ico': '/favicon.ico':
{ {
'tools.session_auth.on': False,
'tools.staticfile.on': True, 'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.join(script_path, 'webroot', 'static', 'favicon.ico') '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', cherrypy.config.update({'server.socket_host': '0.0.0.0',
'server.socket_port': 8080, 'server.socket_port': 8080,
}) })
cherrypy.quickstart(WebApp(cfg), '/', conf) cherrypy.quickstart(app, '/', app_conf)
if __name__ == '__main__': 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): def get_peer(self, peer):
"""Get data of the given WireGuard peer""" """Get data of the given WireGuard peer"""
if peer is None:
return None
return self.transform_to_clientdata(peer, self.wc.peers[peer]) return self.transform_to_clientdata(peer, self.wc.peers[peer])
def get_peers(self): def get_peers(self):
@ -58,11 +60,16 @@ class WGCfg():
def get_peer_byid(self, id): def get_peer_byid(self, id):
"""Get data WireGuard peer with the given id""" """Get data WireGuard peer with the given id"""
try:
peer = next(peer for peer, peerdata in self.get_peers().items() if peerdata['Id'] == id) 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) return peer, self.get_peer(peer)
def get_peerconfig(self, peer): def get_peerconfig(self, peer):
"""Get config for the given WireGuard peer""" """Get config for the given WireGuard peer"""
if peer is None:
return None, None
peerdata = self.get_peer(peer) peerdata = self.get_peer(peer)
for item in self.get_interface()['_rawdata']: for item in self.get_interface()['_rawdata']:
if item.startswith('# Endpoint = '): if item.startswith('# Endpoint = '):