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