2015-09-05 02:51:20 +00:00
|
|
|
from flask import Blueprint, render_template, abort, request, redirect, session, url_for, send_file, Response
|
2018-08-25 13:19:46 +00:00
|
|
|
from flask_login import current_user, login_user, logout_user
|
2015-09-06 16:42:30 +00:00
|
|
|
from datetime import datetime, timedelta
|
2015-09-05 02:51:20 +00:00
|
|
|
from fosspay.objects import *
|
|
|
|
from fosspay.database import db
|
|
|
|
from fosspay.common import *
|
|
|
|
from fosspay.config import _cfg, load_config
|
2015-09-06 21:35:30 +00:00
|
|
|
from fosspay.email import send_thank_you, send_password_reset
|
2019-01-19 00:39:14 +00:00
|
|
|
from fosspay.email import send_new_donation, send_cancellation_notice
|
2018-09-19 18:03:19 +00:00
|
|
|
from fosspay.currency import currency
|
2015-09-05 02:51:20 +00:00
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
import os
|
2015-09-05 02:51:20 +00:00
|
|
|
import locale
|
2015-09-05 21:27:41 +00:00
|
|
|
import bcrypt
|
2015-09-06 01:30:52 +00:00
|
|
|
import hashlib
|
2015-09-06 16:42:30 +00:00
|
|
|
import stripe
|
|
|
|
import binascii
|
2018-01-06 22:22:41 +00:00
|
|
|
import requests
|
2015-09-05 02:51:20 +00:00
|
|
|
|
|
|
|
encoding = locale.getdefaultlocale()[1]
|
|
|
|
html = Blueprint('html', __name__, template_folder='../../templates')
|
|
|
|
|
|
|
|
@html.route("/")
|
|
|
|
def index():
|
|
|
|
if User.query.count() == 0:
|
|
|
|
load_config()
|
|
|
|
return render_template("setup.html")
|
2015-09-05 21:27:41 +00:00
|
|
|
projects = sorted(Project.query.all(), key=lambda p: p.name)
|
2015-09-06 01:30:52 +00:00
|
|
|
avatar = "//www.gravatar.com/avatar/" + hashlib.md5(_cfg("your-email").encode("utf-8")).hexdigest()
|
2015-09-06 16:42:30 +00:00
|
|
|
selected_project = request.args.get("project")
|
|
|
|
if selected_project:
|
|
|
|
try:
|
|
|
|
selected_project = int(selected_project)
|
|
|
|
except:
|
|
|
|
selected_project = None
|
2017-12-09 16:43:32 +00:00
|
|
|
active_recurring = (Donation.query
|
|
|
|
.filter(Donation.type == DonationType.monthly)
|
2018-08-25 13:41:24 +00:00
|
|
|
.filter(Donation.active == True)
|
|
|
|
.filter(Donation.hidden == False))
|
2017-12-09 16:43:32 +00:00
|
|
|
recurring_count = active_recurring.count()
|
|
|
|
recurring_sum = sum([d.amount for d in active_recurring])
|
|
|
|
|
|
|
|
access_token = _cfg("patreon-access-token")
|
|
|
|
campaign = _cfg("patreon-campaign")
|
|
|
|
if access_token and campaign:
|
2018-08-25 13:19:46 +00:00
|
|
|
try:
|
|
|
|
import patreon
|
|
|
|
client = patreon.API(access_token)
|
|
|
|
campaign = client.fetch_campaign()
|
|
|
|
attrs = campaign.json_data["data"][0]["attributes"]
|
|
|
|
patreon_count = attrs["patron_count"]
|
|
|
|
patreon_sum = attrs["pledge_sum"]
|
|
|
|
except:
|
|
|
|
patreon_count = 0
|
|
|
|
patreon_sum = 0
|
2017-12-09 16:43:32 +00:00
|
|
|
else:
|
|
|
|
patreon_count = 0
|
|
|
|
patreon_sum = 0
|
|
|
|
|
2018-01-06 22:22:41 +00:00
|
|
|
liberapay = _cfg("liberapay-campaign")
|
|
|
|
if liberapay:
|
|
|
|
lp = (requests
|
|
|
|
.get("https://liberapay.com/{}/public.json".format(liberapay))
|
|
|
|
).json()
|
|
|
|
lp_count = lp['npatrons']
|
|
|
|
lp_sum = int(float(lp['receiving']['amount']) * 100)
|
|
|
|
# Convert from weekly to monthly
|
|
|
|
lp_sum = lp_sum * 52 // 12
|
|
|
|
else:
|
|
|
|
lp_count = 0
|
|
|
|
lp_sum = 0
|
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
return render_template("index.html", projects=projects,
|
2017-12-09 16:43:32 +00:00
|
|
|
avatar=avatar, selected_project=selected_project,
|
|
|
|
recurring_count=recurring_count,
|
|
|
|
recurring_sum=recurring_sum,
|
|
|
|
patreon_count=patreon_count,
|
2018-01-06 22:22:41 +00:00
|
|
|
patreon_sum=patreon_sum,
|
|
|
|
lp_count=lp_count,
|
2018-09-19 18:03:19 +00:00
|
|
|
lp_sum=lp_sum, currency=currency)
|
2015-09-05 02:51:20 +00:00
|
|
|
|
|
|
|
@html.route("/setup", methods=["POST"])
|
|
|
|
def setup():
|
|
|
|
if not User.query.count() == 0:
|
|
|
|
abort(400)
|
|
|
|
email = request.form.get("email")
|
|
|
|
password = request.form.get("password")
|
|
|
|
if not email or not password:
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("..") # TODO: Tell them what they did wrong (i.e. being stupid)
|
2015-09-05 02:51:20 +00:00
|
|
|
user = User(email, password)
|
|
|
|
user.admin = True
|
|
|
|
db.add(user)
|
|
|
|
db.commit()
|
|
|
|
login_user(user)
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("admin?first-run=1")
|
2015-09-05 02:51:20 +00:00
|
|
|
|
|
|
|
@html.route("/admin")
|
|
|
|
@adminrequired
|
|
|
|
def admin():
|
2015-09-05 19:33:30 +00:00
|
|
|
first = request.args.get("first-run") is not None
|
|
|
|
projects = Project.query.all()
|
2015-09-05 21:27:41 +00:00
|
|
|
unspecified = Donation.query.filter(Donation.project == None).all()
|
2015-09-06 19:45:23 +00:00
|
|
|
donations = Donation.query.order_by(Donation.created.desc()).limit(50).all()
|
2015-09-05 19:33:30 +00:00
|
|
|
return render_template("admin.html",
|
2015-09-05 21:27:41 +00:00
|
|
|
first=first,
|
|
|
|
projects=projects,
|
2015-09-06 19:45:23 +00:00
|
|
|
donations=donations,
|
2018-09-19 18:03:19 +00:00
|
|
|
currency=currency,
|
2015-09-05 21:27:41 +00:00
|
|
|
one_times=lambda p: sum([d.amount for d in p.donations if d.type == DonationType.one_time]),
|
2016-07-04 20:32:53 +00:00
|
|
|
recurring=lambda p: sum([d.amount for d in p.donations if d.type == DonationType.monthly and d.active]),
|
2016-07-04 20:37:52 +00:00
|
|
|
recurring_ever=lambda p: sum([d.amount * d.payments for d in p.donations if d.type == DonationType.monthly]),
|
2015-09-05 21:27:41 +00:00
|
|
|
unspecified_one_times=sum([d.amount for d in unspecified if d.type == DonationType.one_time]),
|
2016-07-04 20:32:53 +00:00
|
|
|
unspecified_recurring=sum([d.amount for d in unspecified if d.type == DonationType.monthly and d.active]),
|
2016-07-04 20:37:52 +00:00
|
|
|
unspecified_recurring_ever=sum([d.amount * d.payments for d in unspecified if d.type == DonationType.monthly]),
|
2016-07-04 20:20:04 +00:00
|
|
|
total_one_time=sum([d.amount for d in Donation.query.filter(Donation.type == DonationType.one_time)]),
|
2016-07-04 20:32:53 +00:00
|
|
|
total_recurring=sum([d.amount for d in Donation.query.filter(Donation.type == DonationType.monthly, Donation.active == True)]),
|
|
|
|
total_recurring_ever=sum([d.amount * d.payments for d in Donation.query.filter(Donation.type == DonationType.monthly)]),
|
2015-09-05 21:27:41 +00:00
|
|
|
)
|
2015-09-05 19:33:30 +00:00
|
|
|
|
|
|
|
@html.route("/create-project", methods=["POST"])
|
|
|
|
@adminrequired
|
|
|
|
def create_project():
|
|
|
|
name = request.form.get("name")
|
|
|
|
project = Project(name)
|
|
|
|
db.add(project)
|
|
|
|
db.commit()
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("admin")
|
2015-09-05 19:33:30 +00:00
|
|
|
|
2015-09-05 21:27:41 +00:00
|
|
|
@html.route("/login", methods=["GET", "POST"])
|
|
|
|
def login():
|
2015-09-06 17:39:50 +00:00
|
|
|
if current_user:
|
|
|
|
if current_user.admin:
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("admin")
|
|
|
|
return redirect("panel")
|
2015-09-05 21:27:41 +00:00
|
|
|
if request.method == "GET":
|
|
|
|
return render_template("login.html")
|
|
|
|
email = request.form.get("email")
|
|
|
|
password = request.form.get("password")
|
|
|
|
if not email or not password:
|
|
|
|
return render_template("login.html", errors=True)
|
|
|
|
user = User.query.filter(User.email == email).first()
|
|
|
|
if not user:
|
|
|
|
return render_template("login.html", errors=True)
|
|
|
|
if not bcrypt.hashpw(password.encode('UTF-8'), user.password.encode('UTF-8')) == user.password.encode('UTF-8'):
|
|
|
|
return render_template("login.html", errors=True)
|
|
|
|
login_user(user)
|
2015-09-06 17:39:50 +00:00
|
|
|
if user.admin:
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("admin")
|
|
|
|
return redirect("panel")
|
2015-09-05 21:27:41 +00:00
|
|
|
|
2015-09-05 19:33:30 +00:00
|
|
|
@html.route("/logout")
|
|
|
|
@loginrequired
|
|
|
|
def logout():
|
|
|
|
logout_user()
|
2015-09-06 22:04:44 +00:00
|
|
|
return redirect(_cfg("protocol") + "://" + _cfg("domain"))
|
2015-09-06 16:42:30 +00:00
|
|
|
|
|
|
|
@html.route("/donate", methods=["POST"])
|
|
|
|
@json_output
|
|
|
|
def donate():
|
|
|
|
email = request.form.get("email")
|
|
|
|
stripe_token = request.form.get("stripe_token")
|
|
|
|
amount = request.form.get("amount")
|
|
|
|
type = request.form.get("type")
|
|
|
|
comment = request.form.get("comment")
|
|
|
|
project_id = request.form.get("project")
|
2015-09-06 19:45:23 +00:00
|
|
|
|
|
|
|
# validate and rejigger the form inputs
|
2015-09-06 16:42:30 +00:00
|
|
|
if not email or not stripe_token or not amount or not type:
|
|
|
|
return { "success": False, "reason": "Invalid request" }, 400
|
|
|
|
try:
|
|
|
|
if project_id is None or project_id == "null":
|
|
|
|
project = None
|
|
|
|
else:
|
|
|
|
project_id = int(project_id)
|
|
|
|
project = Project.query.filter(Project.id == project_id).first()
|
2015-09-06 17:17:04 +00:00
|
|
|
|
|
|
|
if type == "once":
|
|
|
|
type = DonationType.one_time
|
|
|
|
else:
|
|
|
|
type = DonationType.monthly
|
2015-09-06 21:15:09 +00:00
|
|
|
|
|
|
|
amount = int(amount)
|
2015-09-06 16:42:30 +00:00
|
|
|
except:
|
|
|
|
return { "success": False, "reason": "Invalid request" }, 400
|
|
|
|
|
|
|
|
new_account = False
|
|
|
|
user = User.query.filter(User.email == email).first()
|
|
|
|
if not user:
|
|
|
|
new_account = True
|
|
|
|
user = User(email, binascii.b2a_hex(os.urandom(20)).decode("utf-8"))
|
2015-09-06 17:22:35 +00:00
|
|
|
user.password_reset = binascii.b2a_hex(os.urandom(20)).decode("utf-8")
|
|
|
|
user.password_reset_expires = datetime.now() + timedelta(days=1)
|
2015-09-06 16:42:30 +00:00
|
|
|
customer = stripe.Customer.create(email=user.email, card=stripe_token)
|
|
|
|
user.stripe_customer = customer.id
|
2015-09-06 17:17:04 +00:00
|
|
|
db.add(user)
|
2015-09-06 19:45:23 +00:00
|
|
|
else:
|
|
|
|
customer = stripe.Customer.retrieve(user.stripe_customer)
|
|
|
|
new_source = customer.sources.create(source=stripe_token)
|
|
|
|
customer.default_source = new_source.id
|
|
|
|
customer.save()
|
2015-09-06 17:17:04 +00:00
|
|
|
|
2015-09-06 19:45:23 +00:00
|
|
|
donation = Donation(user, type, amount, project, comment)
|
2015-09-06 17:17:04 +00:00
|
|
|
db.add(donation)
|
|
|
|
|
|
|
|
try:
|
|
|
|
charge = stripe.Charge.create(
|
|
|
|
amount=amount,
|
2018-09-19 18:03:19 +00:00
|
|
|
currency=_cfg("currency"),
|
2015-09-06 17:17:04 +00:00
|
|
|
customer=user.stripe_customer,
|
|
|
|
description="Donation to " + _cfg("your-name")
|
|
|
|
)
|
|
|
|
except stripe.error.CardError as e:
|
|
|
|
db.rollback()
|
|
|
|
db.close()
|
|
|
|
return { "success": False, "reason": "Your card was declined." }
|
2015-09-06 16:42:30 +00:00
|
|
|
|
|
|
|
db.commit()
|
|
|
|
|
2015-09-06 21:15:09 +00:00
|
|
|
send_thank_you(user, amount, type == DonationType.monthly)
|
2019-01-23 22:22:35 +00:00
|
|
|
send_new_donation(user, donation)
|
2015-09-06 21:15:09 +00:00
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
if new_account:
|
|
|
|
return { "success": True, "new_account": new_account, "password_reset": user.password_reset }
|
|
|
|
else:
|
|
|
|
return { "success": True, "new_account": new_account }
|
|
|
|
|
2015-09-06 21:35:30 +00:00
|
|
|
def issue_password_reset(email):
|
|
|
|
user = User.query.filter(User.email == email).first()
|
|
|
|
if not user:
|
|
|
|
return render_template("reset.html", errors="No one with that email found.")
|
|
|
|
user.password_reset = binascii.b2a_hex(os.urandom(20)).decode("utf-8")
|
|
|
|
user.password_reset_expires = datetime.now() + timedelta(days=1)
|
|
|
|
send_password_reset(user)
|
|
|
|
db.commit()
|
|
|
|
return render_template("reset.html", done=True)
|
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
@html.route("/password-reset", methods=['GET', 'POST'], defaults={'token': None})
|
|
|
|
@html.route("/password-reset/<token>", methods=['GET', 'POST'])
|
|
|
|
def reset_password(token):
|
2015-09-06 21:35:30 +00:00
|
|
|
if request.method == "GET" and not token:
|
|
|
|
return render_template("reset.html")
|
|
|
|
|
|
|
|
if request.method == "POST":
|
2015-09-06 16:42:30 +00:00
|
|
|
token = request.form.get("token")
|
2015-09-06 21:35:30 +00:00
|
|
|
email = request.form.get("email")
|
|
|
|
|
|
|
|
if email:
|
|
|
|
return issue_password_reset(email)
|
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
if not token:
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("..")
|
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
user = User.query.filter(User.password_reset == token).first()
|
|
|
|
if not user:
|
2015-09-06 21:35:30 +00:00
|
|
|
return render_template("reset.html", errors="This link has expired.")
|
|
|
|
|
2015-09-06 16:42:30 +00:00
|
|
|
if request.method == 'GET':
|
|
|
|
if user.password_reset_expires == None or user.password_reset_expires < datetime.now():
|
2015-09-06 21:35:30 +00:00
|
|
|
return render_template("reset.html", errors="This link has expired.")
|
2015-09-06 16:42:30 +00:00
|
|
|
if user.password_reset != token:
|
2015-09-06 21:35:30 +00:00
|
|
|
redirect("..")
|
2015-09-06 16:42:30 +00:00
|
|
|
return render_template("reset.html", token=token)
|
|
|
|
else:
|
|
|
|
if user.password_reset_expires == None or user.password_reset_expires < datetime.now():
|
|
|
|
abort(401)
|
|
|
|
if user.password_reset != token:
|
|
|
|
abort(401)
|
|
|
|
password = request.form.get('password')
|
|
|
|
if not password:
|
|
|
|
return render_template("reset.html", token=token, errors="You need to type a new password.")
|
|
|
|
user.set_password(password)
|
|
|
|
user.password_reset = None
|
|
|
|
user.password_reset_expires = None
|
|
|
|
db.commit()
|
|
|
|
login_user(user)
|
2015-09-06 21:35:30 +00:00
|
|
|
return redirect("panel")
|
2015-09-06 16:42:30 +00:00
|
|
|
|
|
|
|
@html.route("/panel")
|
|
|
|
@loginrequired
|
|
|
|
def panel():
|
2015-09-06 17:39:50 +00:00
|
|
|
return render_template("panel.html",
|
|
|
|
one_times=lambda u: [d for d in u.donations if d.type == DonationType.one_time],
|
2018-09-19 18:03:19 +00:00
|
|
|
recurring=lambda u: [d for d in u.donations if d.type == DonationType.monthly and d.active],
|
|
|
|
currency=currency)
|
2015-09-06 20:37:25 +00:00
|
|
|
|
|
|
|
@html.route("/cancel/<id>")
|
|
|
|
@loginrequired
|
|
|
|
def cancel(id):
|
|
|
|
donation = Donation.query.filter(Donation.id == id).first()
|
|
|
|
if donation.user != current_user:
|
|
|
|
abort(401)
|
|
|
|
if donation.type != DonationType.monthly:
|
|
|
|
abort(400)
|
|
|
|
donation.active = False
|
|
|
|
db.commit()
|
2019-01-23 22:22:35 +00:00
|
|
|
send_cancellation_notice(user, donation)
|
2015-09-06 20:37:25 +00:00
|
|
|
return redirect("/panel")
|
2019-02-06 14:05:20 +00:00
|
|
|
|
|
|
|
@html.route("/invoice/<id>")
|
|
|
|
def invoice(id):
|
|
|
|
invoice = Invoice.query.filter(Invoice.external_id == id).first()
|
|
|
|
if not invoice:
|
|
|
|
abort(404)
|
|
|
|
return render_template("invoice.html", invoice=invoice)
|