From 3f4ec83abe4b3ae70466650c65f2a0f981a1bba0 Mon Sep 17 00:00:00 2001 From: Thomas L <tom@dl6tom.de> 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 @@ +<form action="login" method="POST"> + <label for="email">Email</label> + <input type="text" placeholder="Enter Email" name="email" id="email" required> + + <label for="pass">Password</label> + <input type="password" placeholder="Enter Password" name="pass" id="pass" required> + + <button type="submit">Login</button> +</form> 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') +<h1>Features</h1> +<p>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.</p> +<h2>How to get Ticketfrei to my city?</h2> +<p>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.</p> +% include('template/register-plain.tpl') +<h2>Our Mission</h2> +<p>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.</p> 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 @@ +<form action="register" method="post"> + <label for="email">Email</label> + <input type="text" placeholder="Enter Email" name="email" id="email" required> + + <label for="pass">Password</label> + <input type="password" placeholder="Enter Password" name="pass" id="pass" required> + + <label for="pass-repeat">Repeat Password</label> + <input type="password" placeholder="Repeat Password" name="pass-repeat" id="pass-repeat" required> + + <button type="submit">Sign Up</button> +</form> + 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'): +<div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;"> + <p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>{{!info}}</p> + </div> +</div> +% 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') +<div id="enablebutton" style="float: right; padding: 2em;">asdf</div> + +<a class='button' style="padding: 1.5em;" href="/login/twitter"> + <picture> + <source type='image/webp' sizes='20px' srcset="/static-cb/1517673283/twitter-20.webp 20w,/static-cb/1517673283/twitter-40.webp 40w,/static-cb/1517673283/twitter-80.webp 80w,"/> + <source type='image/png' sizes='20px' srcset="/static-cb/1517673283/twitter-20.png 20w,/static-cb/1517673283/twitter-40.png 40w,/static-cb/1517673283/twitter-80.png 80w,"/> + <img src="https://codl.forget.fr/static-cb/1517673283/twitter-20.png" alt="" /> + </picture> + Log in with Twitter +</a> + +<section style="padding: 1.5em;"> + <h2>Log in with Mastodon</h2> + <p> + <form action="/login/mastodon" method='post'> + <label>Mastodon instance: + <input type='text' name='instance_url' list='instances' placeholder='social.example.net'/> + </label> + <datalist id='instances'> + <option value=''> + <option value='anticapitalist.party'> + <option value='awoo.space'> + <option value='cybre.space'> + <option value='mastodon.social'> + <option value='glitch.social'> + <option value='botsin.space'> + <option value='witches.town'> + <option value='social.wxcafe.net'> + <option value='monsterpit.net'> + <option value='mastodon.xyz'> + <option value='a.weirder.earth'> + <option value='chitter.xyz'> + <option value='sins.center'> + <option value='dev.glitch.social'> + <option value='computerfairi.es'> + <option value='niu.moe'> + <option value='icosahedron.website'> + <option value='hostux.social'> + <option value='hyenas.space'> + <option value='instance.business'> + <option value='mastodon.sdf.org'> + <option value='pawoo.net'> + <option value='pouet.it'> + <option value='scalie.business'> + <option value='sleeping.town'> + <option value='social.koyu.space'> + <option value='sunshinegardens.org'> + <option value='vcity.network'> + <option value='octodon.social'> + <option value='soc.ialis.me'> + </datalist> + <input name='confirm' value='Log in' type='submit'/> + </form> + </p> +</section> + +<!-- offer mailing list creation button --> + +<div style="float: left; padding: 1.5em;"> + <!-- good list entry field --> + <p> + 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. + </p> + <form action="/settings/goodlist" method="post"> + <!-- find a way to display current good list. js which reads from a cookie? template? --> + <textarea id="goodlist" rows="8" cols="70" name="goodlist" wrap="physical"></textarea> + <input name='confirm' value='Submit' type='submit'/> + </form> +</div> + +<!-- blacklist entry field --> +<div style="float:right; padding: 1.5em;"> + <p> + 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. + <!-- There are words which you can't exclude from the blacklist, e.g. certain racist, sexist, or antisemitic slurs. (to be implemented) --> + </p> + <form action="/settings/blacklist" method="post"> + <!-- find a way to display current blacklist. js which reads from a cookie? template? --> + <textarea id="blacklist" rows="8" cols="70" name="blacklist" wrap="physical"></textarea> + <input name='confirm' value='Submit' type='submit'/> + </form> +</div> + + 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 @@ +<head> + <title>Ticketfrei - {{get('title', 'A bot against control society!')}}</title> + <meta name='og:title' content='Ticketfrei'/> + <meta name='og:description' content='A bot against control society! Nobody should have to pay for public transport. Find out where ticket controllers are!'/> + <meta name='og:image' content="https://ticketfrei.links-tech.org/static/img/ticketfrei-og-image.png"/> + <meta name='og:image:alt' content='Ticketfrei'/> + <meta name='og:type' content='website' /> + <link rel='stylesheet' href='/static/css/style.css'> + <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> + <script src="https://code.jquery.com/jquery-1.12.4.js"></script> + <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> +</head> +<body> + <div id="content"> + <img src="/static/img/ticketfrei_logo.png" alt="Ticketfrei" id="logo"> + % if defined('error'): + <div class="ui-widget"> + <div class="ui-state-error ui-corner-all" style="padding: 0.7em;"> + <p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>{{error}}</p> + </div> + </div> + % end + {{!base}} + <p>Contribute on <a href="https://github.com/b3yond/ticketfrei">GitHub!</a></p> + </div> +</body> 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='<a href="%s/../confirm/%s">Confirmation mail sent.</a>' % + (request.url, db.token(email, password))) + + +@get('/confirm/<token>', 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/<filename:path>') +def static(filename): + return bottle.static_file(filename, root='static') + + +bottle.install(DBPlugin('/')) +bottle.run(host='localhost', port=8080)