start refactoring web-frontend.

master
Thomas L 2018-03-22 02:23:31 +01:00
parent 205097b87f
commit ba5711aefe
10 changed files with 371 additions and 58 deletions

105
db.py Normal file
View File

@ -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

View File

@ -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;
}

9
template/login-plain.tpl Normal file
View File

@ -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>

2
template/login.tpl Normal file
View File

@ -0,0 +1,2 @@
% rebase('template/wrapper.tpl', title='Login')
% include('template/login-plain.tpl')

42
template/propaganda.tpl Normal file
View File

@ -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>

View File

@ -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>

10
template/register.tpl Normal file
View File

@ -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

88
template/settings.tpl Normal file
View File

@ -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>

26
template/wrapper.tpl Normal file
View File

@ -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>

65
ticketfrei-web.py Normal file
View File

@ -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)