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)