From ba5711aefeb1227505f00a88f3b8f333c711397a Mon Sep 17 00:00:00 2001 From: Thomas L Date: Thu, 22 Mar 2018 02:23:31 +0100 Subject: [PATCH] start refactoring web-frontend. --- db.py | 105 ++++++++++++++++++++++++++++++++++++ static/css/style.css | 69 ++++-------------------- template/login-plain.tpl | 9 ++++ template/login.tpl | 2 + template/propaganda.tpl | 42 +++++++++++++++ template/register-plain.tpl | 13 +++++ template/register.tpl | 10 ++++ template/settings.tpl | 88 ++++++++++++++++++++++++++++++ template/wrapper.tpl | 26 +++++++++ ticketfrei-web.py | 65 ++++++++++++++++++++++ 10 files changed, 371 insertions(+), 58 deletions(-) create mode 100644 db.py create mode 100644 template/login-plain.tpl create mode 100644 template/login.tpl create mode 100644 template/propaganda.tpl create mode 100644 template/register-plain.tpl create mode 100644 template/register.tpl create mode 100644 template/settings.tpl create mode 100644 template/wrapper.tpl create mode 100644 ticketfrei-web.py diff --git a/db.py b/db.py new file mode 100644 index 0000000..f5a49b8 --- /dev/null +++ b/db.py @@ -0,0 +1,105 @@ +from bottle import redirect, request, response +from functools import wraps +from inspect import Signature +import jwt +from os import path, urandom +from pylibscrypt import scrypt_mcf, scrypt_mcf_check +import sqlite3 + + +class DB(object): + def __init__(self): + dbfile = path.join(path.dirname(path.abspath(__file__)), + 'ticketfrei.sqlite') + dbfile = ':memory:' + self.conn = sqlite3.connect(dbfile) + self.cur = self.conn.cursor() + self.secret = urandom(32) + self.create() + + def create(self): + # init db + self.cur.executescript(''' + CREATE TABLE user ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + email TEXT, + passhash TEXT, + enabled INTEGER DEFAULT 1 + ); + ''') + + def token(self, email, password): + return jwt.encode({ + 'email': email, + 'passhash': scrypt_mcf(password.encode('utf-8')).decode('ascii') + }, self.secret).decode('ascii') + + def register(self, token): + json = jwt.decode(token, self.secret) + # create user + self.cur.execute("INSERT INTO user (email, passhash) VALUES(?, ?);", + (json['email'], json['passhash'])) + return User(self, self.cur.lastrowid) + + def authenticate(self, email, password): + # check email/password + self.cur.execute("SELECT id, passhash FROM user WHERE email=?;", + (email, )) + row = self.cur.fetchone() + if not row: + return None + if not scrypt_mcf_check(row[1].encode('ascii'), + password.encode('utf-8')): + return None + return User(self, row[0]) + + def by_email(self, email): + self.cur.execute("SELECT id FROM user WHERE email=?;", (email, )) + row = self.cur.fetchone() + if not row: + return None + return User(self, row[0]) + + def close(self): + self.conn.close() + + +class User(object): + def __init__(self, db, uid): + # set cookie + response.set_cookie('uid', uid, secret=db.secret, path='/') + self.db = db + self.uid = uid + + def state(self): + return dict(foo='bar') + + +class DBPlugin(object): + name = 'DBPlugin' + api = 2 + + def __init__(self, loginpage): + self.db = DB() + self.loginpage = loginpage + + def close(self): + self.db.close() + + def apply(self, callback, route): + uservar = route.config.get('user', None) + dbvar = route.config.get('db', None) + signature = Signature.from_callable(route.callback) + + @wraps(callback) + def wrapper(*args, **kwargs): + if uservar and uservar in signature.parameters: + uid = request.get_cookie('uid', secret=self.db.secret) + if uid is None: + return redirect(self.loginpage) + kwargs[uservar] = User(self.db, uid) + if dbvar and dbvar in signature.parameters: + kwargs[dbvar] = self.db + return callback(*args, **kwargs) + + return wrapper diff --git a/static/css/style.css b/static/css/style.css index e143570..4ecc40a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,57 +1,29 @@ body { background-image: url(/static/img/wallpaper.png); font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 15px; + font-size: 12pt; line-height: 1.5em; background-position: center top; margin: 0; - display: flex; - flex-direction: column; } -.area { +#logo { + height: 9em; +} + +#content { background-color: #FFF; - max-width: 600px; + max-width: 37em; margin-left: auto; margin-right: auto; - flex: 1 0 auto; -/* - min-height: 100%; - height: auto !important; - height: 600px; - text-align: center; -*/ -} - -.text { padding: 2em; } -h1, h2 { - text-align: left; - margin-top: 0; -} - -p { - text-align: left; -} - -/* Set a style for all buttons */ button { background-color: #4CAF50; color: white; - padding: 14px 20px; - margin: 8px 0; - border: none; - cursor: pointer; - font-size: 120%; -} - -a.button { - background-color: #1da1f2; - color: white; - padding: 14px 20px; - margin: 8px 0; + padding: 0.7em 1em; + margin: 0.5em 0; border: none; cursor: pointer; font-size: 120%; @@ -63,27 +35,8 @@ button:hover { input[type=text], input[type=password] { width: 100%; - padding: 12px 20px; - margin: 8px 0; + padding: 0.8em 1em; + margin: 0.5em 0; display: inline-block; border: 1px solid #ccc; - box-sizing: border-box; } - -.container { - text-align: center; - padding: 4em; - padding-top: 0; - padding-bottom: 0; - float: none; -} - -.footer { - padding: 2em; - bottom: 0; - background-color: #fff; - float: center; - width: 540px; - height: 30px; - flex-shrink: 0; -} \ No newline at end of file diff --git a/template/login-plain.tpl b/template/login-plain.tpl new file mode 100644 index 0000000..18a01b9 --- /dev/null +++ b/template/login-plain.tpl @@ -0,0 +1,9 @@ +
+ + + + + + + +
diff --git a/template/login.tpl b/template/login.tpl new file mode 100644 index 0000000..6d1e3d9 --- /dev/null +++ b/template/login.tpl @@ -0,0 +1,2 @@ +% rebase('template/wrapper.tpl', title='Login') +% include('template/login-plain.tpl') diff --git a/template/propaganda.tpl b/template/propaganda.tpl new file mode 100644 index 0000000..479fcf6 --- /dev/null +++ b/template/propaganda.tpl @@ -0,0 +1,42 @@ +% rebase('template/wrapper.tpl') +% include('template/login-plain.tpl') +

Features

+

sum is simply dummy text of the printing and typesetting + industry. Lorem Ipsum has been the industry's standard + dummy text ever since the 1500s, when an unknown printer + took a galley of type and scrambled it to make a type + specimen book. It has survived not only five centuries, + but also the leap into electronic typesetting, remaining + essentially unchanged. It was popularised in the 1960s + with the release of Letraset sheets containing Lorem + Ipsum passages, and more recently with desktop publishing + software like Aldus PageMaker including versions of Lorem + Ipsum.

+

How to get Ticketfrei to my city?

+

sum is simply dummy text of the printing and typesetting + industry. Lorem Ipsum has been the industry's standard + dummy text ever since the 1500s, when an unknown printer + took a galley of type and scrambled it to make a type + specimen book. It has survived not only five centuries, + but also the leap into electronic typesetting, remaining + essentially unchanged. It was popularised in the 1960s + with the release of Letraset sheets containing Lorem + Ipsum passages, and more recently with desktop publishing + software like Aldus PageMaker including versions of Lorem + Ipsum.

+% include('template/register-plain.tpl') +

Our Mission

+

Contrary to popular belief, Lorem Ipsum is not simply random + text. It has roots in a piece of classical Latin literature + from 45 BC, making it over 2000 years old. Richard + McClintock, a Latin professor at Hampden-Sydney College in + Virginia, looked up one of the more obscure Latin words, + consectetur, from a Lorem Ipsum passage, and going through + the cites of the word in classical literature, discovered + the undoubtable source. Lorem Ipsum comes from sections + 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" + (The Extremes of Good and Evil) by Cicero, written in 45 + BC. This book is a treatise on the theory of ethics, very + popular during the Renaissance. The first line of Lorem + Ipsum, "Lorem ipsum dolor sit amet..", comes from a line + in section 1.10.32.

diff --git a/template/register-plain.tpl b/template/register-plain.tpl new file mode 100644 index 0000000..df26372 --- /dev/null +++ b/template/register-plain.tpl @@ -0,0 +1,13 @@ +
+ + + + + + + + + + +
+ diff --git a/template/register.tpl b/template/register.tpl new file mode 100644 index 0000000..43b9c08 --- /dev/null +++ b/template/register.tpl @@ -0,0 +1,10 @@ +% rebase('template/wrapper.tpl', title='Register') +% if defined('info'): +
+
+

{{!info}}

+
+
+% else: +% include('template/register-plain.tpl') +% end diff --git a/template/settings.tpl b/template/settings.tpl new file mode 100644 index 0000000..aeb87eb --- /dev/null +++ b/template/settings.tpl @@ -0,0 +1,88 @@ +% rebase('template/wrapper.tpl') +
asdf
+ + + + + + + + Log in with Twitter + + +
+

Log in with Mastodon

+

+

+ + + + +
+

+
+ + + +
+ +

+ These words have to be contained in a report. + If none of these expressions is in the report, it will be ignored by the bot. + You can use the defaults, or enter some expressions specific to your city and language. +

+
+ + + +
+
+ + +
+

+ These words are not allowed in reports. + If you encounter spam, you can add more here - the bot will ignore reports which use such words. + +

+
+ + + +
+
+ + diff --git a/template/wrapper.tpl b/template/wrapper.tpl new file mode 100644 index 0000000..cff6633 --- /dev/null +++ b/template/wrapper.tpl @@ -0,0 +1,26 @@ + + Ticketfrei - {{get('title', 'A bot against control society!')}} + + + + + + + + + + + +
+ + % if defined('error'): +
+
+

{{error}}

+
+
+ % end + {{!base}} +

Contribute on GitHub!

+
+ diff --git a/ticketfrei-web.py b/ticketfrei-web.py new file mode 100644 index 0000000..c03428d --- /dev/null +++ b/ticketfrei-web.py @@ -0,0 +1,65 @@ +import bottle +from bottle import get, post, redirect, request, response, view +from db import DBPlugin + + +@get('/') +@view('template/propaganda.tpl') +def propaganda(): + # clear auth cookie + response.set_cookie('uid', '', expires=0) + + +@post('/register', db='db') +@view('template/register.tpl') +def register_post(db): + email = request.forms.get('email', '') + password = request.forms.get('pass', '') + password_repeat = request.forms.get('pass-repeat', '') + if password != password_repeat: + return dict(error='Passwords do not match.') + if db.by_email(email): + return dict(error='Email address already in use.') + # send confirmation mail + # XXX + return dict(info='Confirmation mail sent.' % + (request.url, db.token(email, password))) + + +@get('/confirm/', db='db') +@view('template/propaganda.tpl') +def confirm(db, token): + # create db-entry + if db.register(token): + return redirect('/settings') + return dict(error='Account creation failed.') + + +@post('/login', db='db') +@view('template/login.tpl') +def login_post(db): + # check login + if db.authenticate(request.forms.get('email', ''), + request.forms.get('pass', '')): + return redirect('/settings') + return dict(error='Authentication failed.') + + +@get('/settings', user='user') +@view('template/settings.tpl') +def settings(user): + return user.state() + + +@get('/api/state', user='user') +def api_enable(user): + return user.state() + + +@get('/static/') +def static(filename): + return bottle.static_file(filename, root='static') + + +bottle.install(DBPlugin('/')) +bottle.run(host='localhost', port=8080)