start refactoring web-frontend.
This commit is contained in:
parent
f9033a009f
commit
3f4ec83abe
105
db.py
Normal file
105
db.py
Normal 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
|
|
@ -1,57 +1,29 @@
|
||||||
body {
|
body {
|
||||||
background-image: url(/static/img/wallpaper.png);
|
background-image: url(/static/img/wallpaper.png);
|
||||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||||
font-size: 15px;
|
font-size: 12pt;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
background-position: center top;
|
background-position: center top;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.area {
|
#logo {
|
||||||
|
height: 9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
max-width: 600px;
|
max-width: 37em;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
flex: 1 0 auto;
|
|
||||||
/*
|
|
||||||
min-height: 100%;
|
|
||||||
height: auto !important;
|
|
||||||
height: 600px;
|
|
||||||
text-align: center;
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set a style for all buttons */
|
|
||||||
button {
|
button {
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 14px 20px;
|
padding: 0.7em 1em;
|
||||||
margin: 8px 0;
|
margin: 0.5em 0;
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.button {
|
|
||||||
background-color: #1da1f2;
|
|
||||||
color: white;
|
|
||||||
padding: 14px 20px;
|
|
||||||
margin: 8px 0;
|
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
|
@ -63,27 +35,8 @@ button:hover {
|
||||||
|
|
||||||
input[type=text], input[type=password] {
|
input[type=text], input[type=password] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 20px;
|
padding: 0.8em 1em;
|
||||||
margin: 8px 0;
|
margin: 0.5em 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid #ccc;
|
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
9
template/login-plain.tpl
Normal 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
2
template/login.tpl
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
% rebase('template/wrapper.tpl', title='Login')
|
||||||
|
% include('template/login-plain.tpl')
|
42
template/propaganda.tpl
Normal file
42
template/propaganda.tpl
Normal 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>
|
13
template/register-plain.tpl
Normal file
13
template/register-plain.tpl
Normal 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
10
template/register.tpl
Normal 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
88
template/settings.tpl
Normal 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
26
template/wrapper.tpl
Normal 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
65
ticketfrei-web.py
Normal 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)
|
Loading…
Reference in a new issue