diff --git a/buckler.py b/buckler.py index 96c1bdf..1766703 100644 --- a/buckler.py +++ b/buckler.py @@ -16,6 +16,7 @@ import asyncpg import uvloop import auth +import forms import tools import config @@ -23,6 +24,7 @@ uvloop.install() routes = web.RouteTableDef() @routes.get(config.url_prefix + '/', name='index') +@routes.post(config.url_prefix + '/', name='index') @auth.auth_required async def index(request): """The index page.""" @@ -55,38 +57,26 @@ async def index(request): if index != -1: users[user_perm['username']][apps[index]['name']] = True users_json = json.dumps(users) + + if request.method == 'POST': + data = await request.post() + form = data.get('form_name') + + forms_ = { + 'invite_user': forms.invite_user, + 'change_password': forms.change_password, + 'delete_key': forms.delete_key, + 'delele_session': forms.delete_session, + } + + if not forms_[form]: + errors = defaultdict['main'].append("Unknown form id: {form}") + else: + errors = await forms_[form](request) + return render_template("index.html", request, locals()) -@routes.post(config.url_prefix + '/change_password', name='change_password') -@auth.auth_required -async def change_password(request): - """Allows a user to change their password.""" - data = await request.post() - current_pw = data.get('current_password') - new_pw = data.get('new_password') - verify_pw = data.get('verify_password') - - if not all((current_pw, new_pw, verify_pw)): - return - if not new_pw == verify_pw: - return - - async with request.app['pool'].acquire() as conn: - pw_hash = conn.fetchrow( - "SELECT password_hash FROM user_info WHERE id = $1", - request['session']['id']) - if not argon2.verify(current_pw, pw_hash['password_hash']): - return - h = argon2.hash(new_pw) - conn.execute( - "UPDATE user_info SET password_hash = $1 WHERE id = $2", - h, request['session']['id']) - index_url = request.app.router['index'].url_for() - raise web.HTTPFound(location=index_url) - - - @routes.get(config.url_prefix + '/login', name='login') @routes.post(config.url_prefix + '/login', name='login') async def login(request): @@ -337,50 +327,6 @@ async def set_session(request): return web.json_response(error) -@routes.post(config.url_prefix + '/delete_key', name='delete_key') -@auth.auth_required -async def delete_key(request): - """Allows a user to delete a security key.""" - data = await request.post() - async with request.app['pool'].acquire() as conn: - for key, value in data.items(): - key_id = key.replace('fido-', '') - if not key_id: - continue - try: - key_id = int(key_id) - except ValueError: - continue - if value != 'on': - continue - await conn.execute( - "DELETE FROM user_credential " - "WHERE id = $1 AND user_id = $2", - key_id, request['session']['id']) - index_url = request.app.router['index'].url_for() - raise web.HTTPFound(location=index_url) - - -@routes.post(config.url_prefix + '/delete_session', name='delete_session') -@auth.auth_required -async def delete_session(request): - """Allows a user to delete a session .""" - data = await request.post() - async with request.app['pool'].acquire() as conn: - for key, value in data.items(): - session_id = key.replace('session-', '', 1) - if not session_id: - continue - if value != 'on': - continue - await conn.execute( - "DELETE FROM user_session " - "WHERE id = $1 AND user_id = $2", - session_id, request['session']['id']) - index_url = request.app.router['index'].url_for() - raise web.HTTPFound(location=index_url) - - async def init_app(): """Initializes the application.""" app = web.Application() diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..728b192 --- /dev/null +++ b/forms.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Co-routines for handling various forms in Buckler. +""" +import tools + + +async def invite_user(request): + """Allows an admin to invite a new user.""" + if not request['session']['admin']: + return {'main': "You do not have permission to do that."} + + data = await request.post() + email = data.get('email') + + if not email: + return {'invite_user': "This field is required."} + # TODO: validate email address + + await tools.send_invite(request, email) + return {} + + +async def change_password(request): + """Allows a user to change their password.""" + errors = {} + data = await request.post() + current_pw = data.get('current_password') + new_pw = data.get('new_password') + verify_pw = data.get('verify_password') + + if not all((current_pw, new_pw, verify_pw)): + errors['change_password'] = "All fields are required." + return errors + if not new_pw == verify_pw: + errors['change_password'] = "Passwords do not match." + return errors + + async with request.app['pool'].acquire() as conn: + pw_hash = conn.fetchrow( + "SELECT password_hash FROM user_info WHERE id = $1", + request['session']['id']) + if not argon2.verify(current_pw, pw_hash['password_hash']): + errors['change_password'] = "Invalid password." + return errors + h = argon2.hash(new_pw) + conn.execute( + "UPDATE user_info SET password_hash = $1 WHERE id = $2", + h, request['session']['id']) + return errors + + +async def delete_key(request): + """Allows a user to delete a security key.""" + data = await request.post() + async with request.app['pool'].acquire() as conn: + for key, value in data.items(): + key_id = key.replace('fido-', '') + if not key_id: + continue + try: + key_id = int(key_id) + except ValueError: + continue + if value != 'on': + continue + await conn.execute( + "DELETE FROM user_credential " + "WHERE id = $1 AND user_id = $2", + key_id, request['session']['id']) + return {} + + +async def delete_session(request): + """Allows a user to delete a session .""" + data = await request.post() + async with request.app['pool'].acquire() as conn: + for key, value in data.items(): + session_id = key.replace('session-', '', 1) + if not session_id: + continue + if value != 'on': + continue + await conn.execute( + "DELETE FROM user_session " + "WHERE id = $1 AND user_id = $2", + session_id, request['session']['id']) + return {} diff --git a/static/buckler.css b/static/buckler.css index b5e5c02..7228ac4 100644 --- a/static/buckler.css +++ b/static/buckler.css @@ -65,16 +65,16 @@ td { text-align: center; } -#change_password { +.no_borders { border: none; border-collapse: separate; width: auto; } -#change_password tr { +.no_borders tr { border: none; } -#change_password td { +.no_borders td { text-align: left; } diff --git a/templates/index.html b/templates/index.html index 4451747..614948d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -60,6 +60,18 @@ +
+

Invite New User

+
+
+
+ + + +

+

+
+
{% endif %} @@ -67,8 +79,9 @@

Change Password


-
- + + +
@@ -91,7 +104,8 @@

{% if fido2_keys %} - + +
@@ -120,7 +134,8 @@

Active Sessions


- + +
diff --git a/tools.py b/tools.py index a632ff5..b419264 100644 --- a/tools.py +++ b/tools.py @@ -35,7 +35,7 @@ async def send_invite(request, to_addr): to_addr, token) d = {'invite': token} invite_url = request.app.router['register'].url_for().with_query(d) - invite_url = config.server_domain + config.url_prefix + invite_url + invite_url = 'https://' + config.server_domain + str(invite_url) body = "Buckle up.\n" + invite_url send_mail(to_addr, "Buckler Invite", body) @@ -52,7 +52,7 @@ async def send_confirmation(request, user_id, to_addr): user_id, token) d = {'confirm': token} confirm_url = request.app.router['register'].url_for().with_query(d) - confirm_url = config.server_domain + str(confirm_url) + confirm_url = 'https://' + config.server_domain + str(confirm_url) body = "Buckle up.\n" + confirm_url