Compare commits
5 Commits
2356c50fba
...
9d1a720ab7
Author | SHA1 | Date | |
---|---|---|---|
9d1a720ab7 | |||
4f01847b03 | |||
c66efb985a | |||
a93a178b57 | |||
e3c33a7492 |
55
buckler.py
55
buckler.py
|
@ -44,6 +44,10 @@ async def index(request):
|
|||
fido2_keys = await conn.fetch(
|
||||
"SELECT * FROM user_credential WHERE user_id = $1",
|
||||
request['session']['id'])
|
||||
active_sessions = await conn.fetch(
|
||||
"SELECT ip_address FROM user_session "
|
||||
"WHERE user_id = $1",
|
||||
request['session']['id'])
|
||||
|
||||
if request['session']['admin']:
|
||||
apps = [app['name'] for app in apps]
|
||||
|
@ -229,7 +233,8 @@ async def get_session(request):
|
|||
session = session[0]
|
||||
data = await conn.fetchrow(
|
||||
"SELECT user_info.username, app_user.session_data "
|
||||
"FROM user_info, app_user "
|
||||
"FROM user_info LEFT JOIN app_user "
|
||||
"ON (user_info.id = app_user.user_id) "
|
||||
"WHERE user_info.id = $1 AND app_user.app_id = $2",
|
||||
session['user_id'], app['id'])
|
||||
|
||||
|
@ -244,11 +249,14 @@ async def get_session(request):
|
|||
await conn.close()
|
||||
|
||||
data_meta = dict(data)
|
||||
data_meta.update(
|
||||
{'last_used': session['last_used'].isoformat()})
|
||||
data_meta['last_used'] = session['last_used'].isoformat()
|
||||
data_meta['user_sid'] = user_sid
|
||||
data_meta['user_id'] = user_id
|
||||
session_data = data_meta.pop('session_data')
|
||||
data = {
|
||||
'meta': data_meta,
|
||||
'session_data': json.loads(data_meta.pop('session_data'))}
|
||||
'session_data': json.loads(session_data)
|
||||
}
|
||||
|
||||
return web.json_response(data)
|
||||
else:
|
||||
|
@ -264,7 +272,44 @@ async def get_session(request):
|
|||
@routes.post(config.url_prefix + '/set_session', name='set_session')
|
||||
async def set_session(request):
|
||||
"""Allows an application to set a user app session."""
|
||||
pass
|
||||
# TODO: only allow LAN IPs
|
||||
app_id = request.query.get('app_id')
|
||||
app_key = request.query.get('app_key')
|
||||
user_id = request.query.get('userid')
|
||||
user_sid = request.query.get('session')
|
||||
|
||||
try:
|
||||
app_id = int(app_id)
|
||||
user_id = int(user_id)
|
||||
assert all((app_id, app_key, user_id, user_sid))
|
||||
except (ValueError, AssertionError):
|
||||
return web.json_response({'error': "Invalid credentials."})
|
||||
|
||||
conn = await request.app['pool'].acquire()
|
||||
app = await conn.fetchrow("SELECT * FROM app_info WHERE id = $1", app_id)
|
||||
if app:
|
||||
if argon2.verify(app_key, app['key_hash']):
|
||||
session = await conn.fetchrow(
|
||||
"SELECT * FROM user_session "
|
||||
"WHERE user_id = $1 AND id = $2 AND expires > NOW()",
|
||||
user_id, user_sid)
|
||||
if session:
|
||||
session_data = await request.text()
|
||||
# TODO: error handling, verify json
|
||||
await conn.execute(
|
||||
"UPDATE app_user SET session_data = $1 "
|
||||
"WHERE user_id = $2 AND app_id = $3",
|
||||
session_data, user_id, app_id)
|
||||
await conn.close()
|
||||
return web.json_response({'success': True})
|
||||
else:
|
||||
error = {'error': "User ID or Session ID invalid."}
|
||||
else:
|
||||
error = {'error': "App ID or Key invalid."}
|
||||
else:
|
||||
error = {'error': "App ID or Key invalid."}
|
||||
await conn.close()
|
||||
return web.json_response(error)
|
||||
|
||||
|
||||
async def init_app():
|
||||
|
|
|
@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS app_info (
|
|||
CREATE TABLE IF NOT EXISTS app_user (
|
||||
user_id INTEGER references user_info(id) ON DELETE CASCADE,
|
||||
app_id INTEGER references app_info(id) ON DELETE CASCADE,
|
||||
session_data JSON,
|
||||
session_data JSON DEFAULT '{}',
|
||||
PRIMARY KEY (user_id, app_id)
|
||||
);
|
||||
|
||||
|
|
107
buckler_flask.py
Normal file
107
buckler_flask.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Session interface middlewares to integrate the flask app with Buckler.
|
||||
"""
|
||||
import json
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
from flask.sessions import SessionInterface, SessionMixin
|
||||
from flask import session, redirect, request
|
||||
|
||||
import config
|
||||
|
||||
class BucklerSessionInterface(SessionInterface):
|
||||
"""
|
||||
Queries the Buckler server for session data to the current user and
|
||||
application.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.url = config.buckler['url']
|
||||
self.app_id = config.buckler['app_id']
|
||||
self.app_key = config.buckler['app_key']
|
||||
|
||||
def open_session(self, app, request):
|
||||
"""Called when a request is initiated."""
|
||||
user_id = request.cookies.get('userid')
|
||||
user_sid = request.cookies.get('session')
|
||||
|
||||
params = {
|
||||
'app_id': self.app_id,
|
||||
'app_key': self.app_key,
|
||||
'userid': user_id,
|
||||
'session': user_sid
|
||||
}
|
||||
params = urllib.parse.urlencode(params)
|
||||
req = urllib.request.Request(self.url + f"/get_session?{params}")
|
||||
res = urllib.request.urlopen(req)
|
||||
data = json.loads(res.read())
|
||||
if data.get('error'):
|
||||
return None
|
||||
session = BucklerSession()
|
||||
session.update(data['session_data'])
|
||||
session.meta = data['meta']
|
||||
session.cookies = request.cookies
|
||||
return session
|
||||
|
||||
def save_session(self, app, session, response):
|
||||
"""Called at the end of a request."""
|
||||
if not session.modified:
|
||||
return
|
||||
user_id = session.meta.get('user_id')
|
||||
user_sid = session.meta.get('user_sid')
|
||||
|
||||
params = {
|
||||
'app_id': self.app_id,
|
||||
'app_key': self.app_key,
|
||||
'userid': user_id,
|
||||
'session': user_sid
|
||||
}
|
||||
params = urllib.parse.urlencode(params)
|
||||
data = json.dumps(session)
|
||||
req = urllib.request.Request(
|
||||
self.url + f"/set_session?{params}",
|
||||
data=data.encode('utf8'),
|
||||
method='POST')
|
||||
res = urllib.request.urlopen(req)
|
||||
|
||||
last_used = datetime.fromisoformat(session.meta['last_used'])
|
||||
now = datetime.now(last_used.tzinfo)
|
||||
delta = now - last_used
|
||||
if delta.seconds > 600:
|
||||
response.set_cookie(
|
||||
'userid',
|
||||
session.cookies['userid'],
|
||||
max_age=30*24*60*60,
|
||||
secure=True,
|
||||
httponly=True)
|
||||
response.set_cookie(
|
||||
'session',
|
||||
session.cookies['session'],
|
||||
max_age=30*24*60*60,
|
||||
secure=True,
|
||||
httponly=True)
|
||||
|
||||
|
||||
class BucklerSession(dict, SessionMixin):
|
||||
"""A server side session class based on the Buckler security shield."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.modified = False
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__setitem__(key, value)
|
||||
self.modified = True
|
||||
|
||||
|
||||
def require_auth():
|
||||
"""
|
||||
Requires the user to be properly authenticated with Buckler before
|
||||
accessing any views on the application.
|
||||
"""
|
||||
if not hasattr(session, 'meta'):
|
||||
resp = redirect(config.buckler['login_url'])
|
||||
resp.set_cookie('redirect', request.url)
|
||||
return resp
|
|
@ -22,13 +22,19 @@ main {
|
|||
|
||||
main section {
|
||||
background-color: whitesmoke;
|
||||
padding: 5%;
|
||||
padding: 2% 5%;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#avail_sites {
|
||||
margin: 0;
|
||||
padding-left: 1em;
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
function load() {
|
||||
let headers = document.querySelectorAll('h2');
|
||||
headers.forEach(function(header) {
|
||||
header.addEventListener('click', function() {
|
||||
let article = this.nextElementSibling;
|
||||
if (article.style.display === 'none') {
|
||||
article.style.display = '';
|
||||
} else {
|
||||
article.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function perm_change(row) {
|
||||
let user_perms = users_perms[row.children[0].textContent];
|
||||
let row_perms = [];
|
||||
|
|
|
@ -2,85 +2,125 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>Buckler</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
|
||||
<script>
|
||||
var users_perms = {{ users_json|safe }};
|
||||
</script>
|
||||
<script type="text/javascript" src="/static/buckler.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
|
||||
<script>window.onload = load;</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="A small shield for web applications.">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<object id="logo" data="/static/buckler_icon.svg"></object>
|
||||
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
|
||||
<h1>Buckler</h1>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
Available sites
|
||||
<ul id="avail_sites">
|
||||
{% for site in avail_sites %}
|
||||
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h2>Available Sites</h2>
|
||||
<article>
|
||||
<hr>
|
||||
<ul id="avail_sites">
|
||||
{% for site in avail_sites %}
|
||||
<li><a href="{{ site['url'] }}">{{ site['name'] }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
{% if request['session']['admin'] %}
|
||||
<section>
|
||||
<table id="users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
{% for app in apps %}
|
||||
<th>{{ app }}</th>
|
||||
<h2>User Permissions</h2>
|
||||
<article style="display: none;">
|
||||
<hr>
|
||||
<table id="users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
{% for app in apps %}
|
||||
<th>{{ app }}</th>
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for username, values in users.items() %}
|
||||
<tr>
|
||||
<td>{{ username }}</td>
|
||||
{% for value in values %}
|
||||
<td><input type="checkbox" onchange="perm_change(this.parentElement.parentElement)"{% if value %} checked{% endif %}></td>
|
||||
{% endfor %}
|
||||
<td><input type="submit"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for username, values in users.items() %}
|
||||
<tr>
|
||||
<td>{{ username }}</td>
|
||||
{% for value in values %}
|
||||
<td><input type="checkbox" onchange="perm_change(this.parentElement.parentElement)"{% if value %} checked{% endif %}></td>
|
||||
{% endfor %}
|
||||
<td><input type="submit"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</section>
|
||||
{% endif %}
|
||||
<section>
|
||||
<form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<label for="current_password">Current password</label>
|
||||
<input id="current_password" name="current_password" type="password"><br>
|
||||
<label for="new_password">New password</label>
|
||||
<input id="new_password" name="new_password" type="password"><br>
|
||||
<label for="verify_password">Verify password</label>
|
||||
<input id="verify_password" name="verify_password" type="password"><br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
<h2>Change Password</h2>
|
||||
<article style="display: none;">
|
||||
<hr>
|
||||
<form action="{{ request.app.router['change_password'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<label for="current_password">Current password</label>
|
||||
<input id="current_password" name="current_password" type="password"><br>
|
||||
<label for="new_password">New password</label>
|
||||
<input id="new_password" name="new_password" type="password"><br>
|
||||
<label for="verify_password">Verify password</label>
|
||||
<input id="verify_password" name="verify_password" type="password"><br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
<section>
|
||||
{% if fido2_keys %}
|
||||
<table id="security_keys">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nick</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in fido2_keys %}
|
||||
<tr>
|
||||
<td>{{ key['nick'] }}</td>
|
||||
<td><input type="checkbox"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<span>No registered keys.</span>
|
||||
{% endif %}
|
||||
<br><a href="./add_key">Add key</a>
|
||||
<h2>Security Keys</h2>
|
||||
<article style="display: none;">
|
||||
<hr>
|
||||
{% if fido2_keys %}
|
||||
<table id="security_keys">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nick</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in fido2_keys %}
|
||||
<tr>
|
||||
<td>{{ key['nick'] }}</td>
|
||||
<td><input type="checkbox"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<span>No registered keys.</span>
|
||||
{% endif %}
|
||||
<br><a href="./add_key">Add key</a>
|
||||
</article>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Active Sessions</h2>
|
||||
<article style="display: none;">
|
||||
<hr>
|
||||
<table id="active_sessions">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in active_sessions %}
|
||||
<tr>
|
||||
<td>{{ session['ip_address'] }}</td>
|
||||
<td><input type="checkbox"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
|
|
@ -2,22 +2,28 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>Buckler - Login</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="A small shield for web applications.">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
|
||||
<h1>Buckler Login</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text"><br>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password"><br>
|
||||
{% if login_failed %}
|
||||
<ul><li>Username and/or password incorrect</li></ul>
|
||||
{% endif %}
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<section>
|
||||
<form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text"><br>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password"><br>
|
||||
{% if login_failed %}
|
||||
<ul><li>Username and/or password incorrect</li></ul>
|
||||
{% endif %}
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -7,9 +7,12 @@
|
|||
<script type="text/javascript" src="/static/buckler-auth.js"></script>
|
||||
<script>const url_prefix = '{{ url_prefix }}';</script>
|
||||
<script>window.onload = login;</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="A small shield for web applications.">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
|
||||
<h1>Buckler Login</h1>
|
||||
</header>
|
||||
<main>
|
||||
|
|
|
@ -2,40 +2,50 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>Buckler - Register</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="A small shield for web applications.">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Buckler Register</h1>
|
||||
<form method="POST" enctype="application/x-www-form-urlencoded">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
|
||||
{% if errors['username'] %}
|
||||
<ul>
|
||||
{% for error in errors['username'] %}
|
||||
<li class="error">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email"><br>
|
||||
{% if errors['email'] %}
|
||||
<ul>
|
||||
{% for error in errors['email'] %}
|
||||
<li class="error">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
|
||||
<label for="password_verify">Verify Password</label>
|
||||
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
|
||||
{% if errors['password'] %}
|
||||
<ul>
|
||||
{% for error in errors['password'] %}
|
||||
<li class="error">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<input type="submit" value="Register">
|
||||
</form>
|
||||
<header>
|
||||
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
|
||||
<h1>Buckler Register</h1>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<form method="POST" enctype="application/x-www-form-urlencoded">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
|
||||
{% if errors['username'] %}
|
||||
<ul>
|
||||
{% for error in errors['username'] %}
|
||||
<li class="error">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email"><br>
|
||||
{% if errors['email'] %}
|
||||
<ul>
|
||||
{% for error in errors['email'] %}
|
||||
<li class="error">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
|
||||
<label for="password_verify">Verify Password</label>
|
||||
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
|
||||
{% if errors['password'] %}
|
||||
<ul>
|
||||
{% for error in errors['password'] %}
|
||||
<li class="error">{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<input type="submit" value="Register">
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,17 +6,21 @@
|
|||
<script type="text/javascript" src="/static/cbor.js"></script>
|
||||
<script type="text/javascript" src="/static/buckler-auth.js"></script>
|
||||
<script>const url_prefix = '{{ url_prefix }}';</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="A small shield for web applications.">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
|
||||
<h1>Register Security Key</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div id="devices">
|
||||
<div class="device">
|
||||
<h1>Register Security Key</h1>
|
||||
<p>Upon clicking submit, your security key will begin flashing. Have it ready.
|
||||
<p><label for="security_key_nick">Security key nick</label> <input id="security_key_nick" type="text" minlength="1" maxlength="64" required>
|
||||
<p><input type="button" value="Submit" onclick="register()">
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<p>Upon clicking submit, your security key will begin flashing. Have it ready.
|
||||
<p><label for="security_key_nick">Security key nick</label>
|
||||
<input id="security_key_nick" type="text" minlength="1" maxlength="64" required>
|
||||
<p><input type="button" value="Submit" onclick="register()">
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,8 +2,19 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>Buckler - Register</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/buckler.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="A small shield for web applications.">
|
||||
</head>
|
||||
<body>
|
||||
{{ message }}
|
||||
<header>
|
||||
<object id="logo" title="Buckler logo" data="/static/buckler_icon.svg"></object>
|
||||
<h1>Buckler Register</h1>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
{{ message }}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue
Block a user