mirror of
https://github.com/towalink/wgfrontend.git
synced 2025-04-19 00:45:15 +00:00
Major improvements incl. CSS
This commit is contained in:
parent
3cde7d371d
commit
16ce040ab9
16
CHANGELOG.md
16
CHANGELOG.md
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
2
setup.py
2
setup.py
@ -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',
|
||||
|
@ -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 "Towalink WireGuard Frontend"</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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
39
src/templates/login.html
Normal 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:
|
||||
</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>
|
@ -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>
|
||||
|
106
src/webapp.py
106
src/webapp.py
@ -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
203
src/webroot/static/logo.svg
Normal 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 |
218
src/webroot/static/styles.css
Normal file
218
src/webroot/static/styles.css
Normal 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;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
TEST
|
@ -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 = '):
|
||||
|
Loading…
x
Reference in New Issue
Block a user