Rewrite, user authentication

v2
informatic 2017-04-04 23:56:42 +02:00
parent f2eb24aeac
commit 32caeed871
19 changed files with 366 additions and 85 deletions

View File

@ -1,80 +1,16 @@
import logging
logging.basicConfig(level=logging.DEBUG) # noqa
import threading
import urllib.parse
import flask
import qrcode
import qrcode.image.svg
import six
from bitvend.mdb import BitvendCashlessMDBDevice
from bitvend.utils import to_local_currency, from_local_currency, format_btc, \
sat_to_btc
from bitvend.processor import PaymentProcessor
from bitvend.models import db, Transaction
logging.basicConfig(level=logging.INFO)
from prometheus_client import start_http_server
start_http_server(8000)
app = flask.Flask(__name__)
app.config.from_object('bitvend.default_settings')
db.init_app(app)
dev = BitvendCashlessMDBDevice()
dev.init_app(app)
proc = PaymentProcessor(dev)
proc.init_app(app)
@app.route('/')
def index():
return flask.render_template(
'index.html', items=app.config['ITEMS'],
mdb_online=dev.online,
proc_online=proc.online)
@app.route('/log')
def log():
return flask.render_template(
'log.html', transactions=Transaction.query.all())
@app.route('/reclaim/<tx_hash>')
def reclaim(tx_hash):
tx = Transaction.query.filter_by(tx_hash=tx_hash).first()
if tx and tx.product_id is None:
dev.begin_session(tx.value, tx_hash)
dev.begin_session(tx.value, tx_hash)
dev.begin_session(tx.value, tx_hash)
return flask.redirect('/log')
flask.abort(404)
@app.route('/qrcode/<path:data>')
def qrcode_gen(data):
bio = six.BytesIO()
qr = qrcode.QRCode(border=0, box_size=50)
qr.add_data(data)
img = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
img.save(bio)
return bio.getvalue(), 200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public,max-age=3600',
}
@app.context_processor
def ctx_utils():
return {
'from_local_currency': from_local_currency,
'to_local_currency': to_local_currency,
'format_btc': format_btc,
'sat_to_btc': sat_to_btc,
'qrcode': lambda data: flask.url_for('qrcode_gen', data=data),
}
from bitvend import create_app, dev, proc, db
if __name__ == "__main__":
from prometheus_client import start_http_server
start_http_server(8000)
app = create_app()
with app.app_context():
db.create_all()

View File

@ -0,0 +1,40 @@
import flask
from bitvend.processor import PaymentProcessor
from bitvend.mdb import BitvendCashlessMDBDevice
dev = BitvendCashlessMDBDevice()
proc = PaymentProcessor(dev)
from bitvend.utils import to_local_currency, from_local_currency, format_btc, \
sat_to_btc
from bitvend.models import db, Transaction
from bitvend.auth import login_manager
import bitvend.views
def create_app():
app = flask.Flask(__name__)
app.config.from_object('bitvend.default_settings')
db.init_app(app)
login_manager.init_app(app)
dev.init_app(app)
proc.init_app(app)
app.register_blueprint(bitvend.views.bp)
@app.context_processor
def ctx_utils():
return {
'from_local_currency': from_local_currency,
'to_local_currency': to_local_currency,
'format_btc': format_btc,
'sat_to_btc': sat_to_btc,
'qrcode': lambda data: flask.url_for('bitvend.qrcode_gen', data=data),
'current_transaction': Transaction.query.filter(Transaction.finished == False).first(),
}
return app

3
bitvend/app.py Normal file
View File

@ -0,0 +1,3 @@
from bitvend import create_app
app = create_app()

31
bitvend/auth.py Normal file
View File

@ -0,0 +1,31 @@
import requests
import functools
from flask import session, flash, redirect, request
from flask_login import login_user, LoginManager, logout_user
from bitvend.models import User, db
login_manager = LoginManager()
@login_manager.user_loader
def load_user(user_id):
return User.query.filter(User.uid == user_id).first()
def try_login(username, password):
resp = requests.post('https://auth.hackerspace.pl/', data={
'login': username,
'password': password
})
if resp.status_code == 200:
u = User.query.get(username)
if not u:
u = User(uid=username)
db.session.add(u)
db.session.commit()
login_user(u)
return True
return False

View File

@ -1,11 +1,16 @@
import platform
SECRET_KEY = 'testing'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///storage-%s.db' % (platform.node(),)
INPUT_ADDRESS = '12fkW5EBb3uBy1zD8pan4TcbabP5Fjato7'
BLOCKCYPHER_CHAIN = 'btc/main'
#INPUT_ADDRESS ='n2SYFMbgfG4LXkvB4VgF4SeSEqQE28NAuV'
#BLOCKCYPHER_CHAIN = 'btc/test3'
TEMPLATES_AUTO_RELOAD = True
ITEMS = [
{

View File

@ -8,14 +8,14 @@ from mdb.constants import *
class BitvendCashlessMDBDevice(CashlessMDBDevice):
current_tx_hash = None
current_tx_id = None
app = None
def init_app(self, app):
self.app = app
def begin_session(self, amount, tx_hash=None):
self.current_tx_hash = tx_hash
def begin_session(self, amount, tx_id=None):
self.current_tx_id = tx_id
super(BitvendCashlessMDBDevice, self).begin_session(amount)
@ -25,15 +25,21 @@ class BitvendCashlessMDBDevice(CashlessMDBDevice):
self.send([0x05, 0x00, 0xff])
self.current_request.processed = True
self.logger.info('got vend request: %r', self.current_tx_hash)
self.logger.info('got vend request: %r', self.current_tx_id)
if self.current_tx_hash:
if self.current_tx_id:
with self.app.app_context():
tx = Transaction.query.filter_by(tx_hash=self.current_tx_hash).first()
tx = Transaction.query.get(self.current_tx_id)
tx.product_id = product
tx.product_value = value
if tx.amount is None:
tx.amount = -value
db.session.commit()
self.current_tx_id = None
cashless_purchase_counter.inc()
return True

View File

@ -1,14 +1,91 @@
from flask import current_app as app
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql import func, select
db = SQLAlchemy()
class TransferException(Exception): pass
class NoFunds(TransferException): pass
class User(db.Model):
__tablename__ = 'users'
uid = db.Column(db.String(64), primary_key=True)
transactions = db.relationship('Transaction', backref='user', lazy='dynamic')
def __str__(self):
return self.uid
@hybrid_property
def balance(self):
return sum((_.amount or 0) for _ in self.transactions)
@balance.expression
def balance(self):
return (select([func.sum(Transaction.amount)]).
where(Transaction.uid == cls.uid).
label("balance")
)
def transfer(self, target, amount):
if self.balance - amount < -self.debt_limit:
raise NoFunds()
self.transactions.append(Transaction(
amount=-amount, type='transfer'
))
target.transactions.append(Transaction(
amount=amount, type='transfer'
))
@property
def debt_limit(self):
return app.config.get('DEBT_LIMIT', 5000)
@hybrid_property
def amount_available(self):
return self.balance + self.debt_limit
is_authenticated = True
is_active = True
is_anonymous = False
def get_id(self):
return self.uid
@property
def transaction_in_progress(self):
return self.transactions.filter(Transaction.finished == False).count()
class Transaction(db.Model):
__tablename__ = 'transactions'
id = db.Column(db.Integer, primary_key=True)
tx_hash = db.Column(db.String)
uid = db.Column(db.String(64), db.ForeignKey('users.uid'))
amount = db.Column(db.Integer)
type = db.Column(db.String(32), default='manual')
related = db.Column(db.String)
related_user = db.relationship('User', foreign_keys=[related], primaryjoin=related==User.uid)
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
value = db.Column(db.Integer)
#value = db.Column(db.Integer)
@hybrid_property
def value(self):
return self.amount
product_id = db.Column(db.Integer)
product_value = db.Column(db.Integer)
@hybrid_property
def finished(self):
return (self.type != 'purchase') | (self.product_id != None)

View File

@ -64,7 +64,9 @@ class PaymentProcessor(threading.Thread):
with self.app.app_context():
tx = Transaction(tx_hash=tx_hash)
tx.value = to_local_currency(tx_value)
tx.uid = '__bitcoin__'
tx.amount = -to_local_currency(tx_value)
tx.type = 'purchase'
db.session.add(tx)
db.session.commit()
@ -79,9 +81,9 @@ class PaymentProcessor(threading.Thread):
self.logger.info('Transaction ok, going to device...')
# FIXME we need better handling of ACK on POLL responses...
self.device.begin_session(to_local_currency(tx_value), tx_hash)
self.device.begin_session(to_local_currency(tx_value), tx_hash)
self.device.begin_session(to_local_currency(tx_value), tx_hash)
self.device.begin_session(to_local_currency(tx_value), tx.id)
self.device.begin_session(to_local_currency(tx_value), tx.id)
self.device.begin_session(to_local_currency(tx_value), tx.id)
def on_message(self, ws, message):
#print message

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 330 KiB

View File

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 369 KiB

View File

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -24,6 +24,9 @@ body {
footer {
padding-bottom: 20px;
}
.well h3 { padding: 0; }
.well h3 small { padding-top: 5px; }
</style>
</head>
<body>
@ -50,16 +53,37 @@ body {
<span class="label label-danger">payments: offline</span>
{% endif %}
</p>
{% if current_user.is_authenticated %}
<ul class="nav navbar-right navbar-nav">
<li><a href="{{ url_for('bitvend.logout') }}">Logout</a>
</ul>
<p class="navbar-text navbar-right">
<small>Logged in as:</small> <b>{{ current_user }}</b>
</p>
{% else %}
<ul class="nav navbar-right navbar-nav">
<li><a href="{{ url_for('bitvend.login') }}">Login</a></li>
</ul>
{% endif %}
<p class="navbar-text navbar-right">
1zł = {{ format_btc(from_local_currency(100)) }}
</p>
<p class="navbar-text navbar-right">
<b>Rate:</b> {{ to_local_currency(100000000) / 100 }}zł
</p>
</div>
</div>
</div>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{category}}">{{ message }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
<hr>
<footer class="row"><big>made by <a href="https://wiki.hackerspace.pl/people:informatic:start">inf</a></big>

View File

@ -1,10 +1,40 @@
{% extends "base.html" %}
{% block content %}
{% if not mdb_online or not proc_online %}
{% if false and not mdb_online or not proc_online %}
<div class="alert alert-warning">
<b>Some of the subsystems are misbehaving.</b> Please avoid making payments, unless in absolute need of Mate.
</div>
{% endif %}
{% if current_user.is_authenticated %}
<div class="row">
<div class="col-sm-4 col-sm-offset-2">
<div class="well text-right">
<h3><small class="pull-left">Balance</small> <small>{{ current_user.amount_available }} / </small> {{ current_user.balance }}</h3>
</div>
</div>
<div class="col-sm-4">
<div class="well text-right">
<h3><small class="pull-left">Purchases</small> {{ current_user.transactions.count() }}</h3>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
{% if current_transaction %}
<a href="{{ url_for('.cancel') }}" class="btn btn-danger btn-block btn-lg">Cancel transaction
{% if current_transaction.user != current_user %}
<small>(by {{ current_transaction.user }})</small>
{% endif %}
</a>
{% else %}
<a href="{{ url_for('.begin') }}" class="btn btn-primary btn-block btn-lg">Begin transaction</a>
{% endif %}
</div>
</div>
<hr>
{% endif %}
<div class="alert alert-info">
This is just a test deployment of Warsaw Hackerspace Vending Machine Bitcoin Payments System™.<br />
<b>Please report any issues to <a href="mailto:informatic@hackerspace.pl" class="alert-link">informatic@hackerspace.pl</a>.</b>

View File

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-md-4 col-md-offset-4 well">
<form action="{{ url_for('.login_submit') }}" class="loginform" method="POST">
{% if not logged_in %}
<input type="text" name="username" placeholder="username" class="form-control input-lg" autofocus />
<input type="password" name="password" placeholder="password" class="form-control input-lg" />
{% endif %}
<button class="btn btn-lg btn-block">Login</button>
</form>
</div>
</div>
{% endblock %}

97
bitvend/views.py Normal file
View File

@ -0,0 +1,97 @@
from flask import Blueprint, render_template, redirect, request, flash, url_for
from flask import current_app as app
import six
import qrcode
import qrcode.image.svg
from bitvend import dev, proc
from bitvend.models import db, Transaction
from bitvend.auth import try_login
from flask_login import login_required, current_user, logout_user
bp = Blueprint('bitvend', __name__, template_folder='templates')
@bp.route('/')
def index():
return render_template(
'index.html', items=app.config['ITEMS'],
mdb_online=dev.online,
proc_online=proc.online)
@bp.route('/login')
def login():
return render_template('login.html')
@bp.route('/login', methods=['POST'])
def login_submit():
if try_login(request.form.get('username'), request.form.get('password')):
flash('Login successful', 'success')
return redirect('/')
flash('Login failed', 'danger')
return redirect(url_for('.login'))
@bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('.index'))
@bp.route('/log')
@login_required
def log():
return render_template(
'log.html', transactions=Transaction.query.all())
@bp.route('/begin')
@login_required
def begin():
if Transaction.query.filter(Transaction.finished == False).count():
flash('Nope xD', 'danger')
return redirect(url_for('.index'))
tx = Transaction(type='purchase')
current_user.transactions.append(tx)
db.session.commit()
dev.begin_session(current_user.amount_available, tx.id)
return redirect(url_for('.index'))
@bp.route('/cancel')
@login_required
def cancel():
dev.cancel_session()
# FIXME racey
Transaction.query.filter(Transaction.finished == False).delete()
db.session.commit()
return redirect(url_for('.index'))
@bp.route('/reclaim/<tx_hash>')
@login_required
def reclaim(tx_hash):
tx = Transaction.query.filter_by(tx_hash=tx_hash).first()
if tx and tx.product_id is None:
dev.begin_session(tx.value, tx_hash)
dev.begin_session(tx.value, tx_hash)
dev.begin_session(tx.value, tx_hash)
return redirect('/log')
abort(404)
@bp.route('/qrcode/<path:data>')
def qrcode_gen(data):
bio = six.BytesIO()
qr = qrcode.QRCode(border=0, box_size=50)
qr.add_data(data)
img = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
img.save(bio)
return bio.getvalue(), 200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public,max-age=3600',
}

View File

@ -74,3 +74,17 @@ class RaspiBackend(Backend):
self.pi.wave_delete(wid)
self.pi.set_mode(self.tx_pin, pigpio.INPUT)
#
# Backend device based on STM32F1 MDB-USB adapter (to be actually designed...)
#
class SerialBackend(Backend):
def __init__(self, device='/dev/ttyACM0'):
self.ser = serial.Serial(device)
def read(self):
return self.ser.read()
def write(self, data):
self.ser.write(data)