511 lines
21 KiB
Python
511 lines
21 KiB
Python
import stripe
|
|
import datetime
|
|
|
|
from flask import request, flash, url_for, render_template, \
|
|
render_template_string, redirect, g
|
|
from flask_login import login_user, logout_user, \
|
|
current_user, login_required
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from formspree import settings
|
|
from formspree.stuff import DB, TEMPLATES
|
|
from formspree.utils import send_email
|
|
from .models import User, Email, Plan
|
|
from .helpers import check_password, hash_pwd, send_downgrade_email, \
|
|
send_downgrade_reason_email
|
|
|
|
|
|
def register():
|
|
if request.method == 'GET':
|
|
return render_template('users/register.html')
|
|
|
|
g.log = g.log.bind(email=request.form.get('email'))
|
|
|
|
try:
|
|
user = User(request.form['email'], request.form['password'])
|
|
DB.session.add(user)
|
|
DB.session.commit()
|
|
g.log.info('User account created.', ip=request.headers.get('X-Forwarded-For'))
|
|
except ValueError:
|
|
DB.session.rollback()
|
|
flash(u"{} is not a valid email address.".format(
|
|
request.form['email']), "error")
|
|
g.log.info('Account creation failed. Invalid address.', ip=request.headers.get('X-Forwarded-For'))
|
|
return render_template('users/register.html')
|
|
except IntegrityError:
|
|
DB.session.rollback()
|
|
flash(u"An account with this email already exists.", "error")
|
|
g.log.info('Account creation failed. Address is already registered.', ip=request.headers.get('X-Forwarded-For'))
|
|
return render_template('users/register.html')
|
|
|
|
login_user(user, remember=True)
|
|
|
|
sent = Email.send_confirmation(user.email, user.id)
|
|
res = redirect(request.args.get('next', url_for('account')))
|
|
if sent:
|
|
res.set_cookie('pending-emails', user.email, max_age=10800)
|
|
flash(u"Your {SERVICE_NAME} account was created successfully!".format(
|
|
**settings.__dict__), 'success')
|
|
flash(u"We've sent an email confirmation to {addr}. Please go there "
|
|
"and click on the confirmation link before you can use your "
|
|
"{SERVICE_NAME} account.".format(
|
|
addr=current_user.email,
|
|
**settings.__dict__), 'info')
|
|
else:
|
|
flash(u"Your account was set up, but we couldn't send a verification "
|
|
"email to your address, please try doing it again manually "
|
|
"later.", 'warning')
|
|
return res
|
|
|
|
|
|
@login_required
|
|
def add_email():
|
|
address = request.form['address'].lower().strip()
|
|
res = redirect(url_for('account'))
|
|
|
|
g.log = g.log.bind(address=address, account=current_user.email)
|
|
|
|
if Email.query.get([address, current_user.id]):
|
|
g.log.info('Failed to add email to account. Already registered.')
|
|
flash(u'{} is already registered for your account.'.format(
|
|
address), 'warning')
|
|
return res
|
|
try:
|
|
g.log.info('Adding new email address to account.')
|
|
sent = Email.send_confirmation(address, current_user.id)
|
|
if sent:
|
|
pending = request.cookies.get('pending-emails', '').split(',')
|
|
pending.append(address)
|
|
res.set_cookie('pending-emails', ','.join(pending), max_age=10800)
|
|
flash(u"We've sent a message with a verification link to {}."
|
|
.format(address), 'info')
|
|
else:
|
|
flash(u"We couldn't sent you the verification email at {}. "
|
|
"Please try again later.".format(
|
|
address), 'error')
|
|
except ValueError:
|
|
flash(u'{} is not a valid email address.'.format(
|
|
request.form['address']), 'warning')
|
|
return res
|
|
|
|
|
|
@login_required
|
|
def confirm_email(digest):
|
|
res = redirect(url_for('account'))
|
|
email = Email.create_with_digest(addr=request.args.get('email', ''),
|
|
user_id=current_user.id,
|
|
digest=digest)
|
|
if email:
|
|
try:
|
|
DB.session.add(email)
|
|
DB.session.commit()
|
|
pending = request.cookies.get('pending-emails', '').split(',')
|
|
try:
|
|
pending.remove(email.address)
|
|
except ValueError:
|
|
pass # when not in list, means nothing serious.
|
|
res.set_cookie('pending-emails', ','.join(pending), max_age=10800)
|
|
flash(u'{} confirmed.'.format(
|
|
email.address), 'success')
|
|
except IntegrityError as e:
|
|
g.log.error('Failed to save new email address to account.',
|
|
exception=repr(e))
|
|
flash(u'A unexpected error has ocurred while we were trying '
|
|
'to confirm the email. Please contact us if this continues '
|
|
'to happen.', 'error')
|
|
return res
|
|
else:
|
|
flash(u"Couldn't confirm {}. Wrong link.".format(email), 'error')
|
|
return res
|
|
|
|
|
|
def login():
|
|
if request.method == 'GET':
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for('dashboard'))
|
|
return render_template('users/login.html')
|
|
email = request.form['email'].lower().strip()
|
|
password = request.form['password']
|
|
user = User.query.filter_by(email=email).first()
|
|
if user is None or not check_password(user.password, password):
|
|
flash(u"Invalid username or password.",
|
|
'warning')
|
|
return redirect(url_for('login'))
|
|
login_user(user, remember=True)
|
|
g.log.info('Logged user in', user=user.email, ip=request.headers.get('X-Forwarded-For'))
|
|
flash(u'Logged in successfully!', 'success')
|
|
return redirect(request.args.get('next') or url_for('dashboard'))
|
|
|
|
|
|
def logout():
|
|
logout_user()
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
def forgot_password():
|
|
if request.method == 'GET':
|
|
return render_template('users/forgot.html')
|
|
elif request.method == 'POST':
|
|
email = request.form['email'].lower().strip()
|
|
user = User.query.filter_by(email=email).first()
|
|
if not user or user.send_password_reset():
|
|
return render_template(
|
|
'info.html',
|
|
title='Reset email sent',
|
|
text=u"We've sent you a password reset link. Please check your email."
|
|
)
|
|
else:
|
|
flash(u"Something is wrong, please report this to us.", 'error')
|
|
return redirect(url_for('login', next=request.args.get('next')))
|
|
|
|
|
|
def reset_password(digest):
|
|
if request.method == 'GET':
|
|
email = request.args['email'].lower().strip()
|
|
user = User.from_password_reset(email, digest)
|
|
if user:
|
|
login_user(user, remember=True)
|
|
return render_template('users/reset.html', digest=digest)
|
|
else:
|
|
flash(u'The link you used to come to this screen has expired. '
|
|
'Please try the reset process again.', 'error')
|
|
return redirect(url_for('login', next=request.args.get('next')))
|
|
|
|
elif request.method == 'POST':
|
|
user = User.from_password_reset(current_user.email, digest)
|
|
if user and user.id == current_user.id:
|
|
if request.form['password1'] == request.form['password2']:
|
|
user.password = hash_pwd(request.form['password1'])
|
|
DB.session.add(user)
|
|
DB.session.commit()
|
|
flash(u'Changed password successfully!', 'success')
|
|
return redirect(request.args.get('next') or url_for('dashboard'))
|
|
else:
|
|
flash(u"The passwords don't match!", 'warning')
|
|
return redirect(url_for('reset-password', digest=digest, next=request.args.get('next')))
|
|
else:
|
|
flash(u'<b>Failed to reset password</b>. The link you used '
|
|
'to come to this screen has expired. Please try the reset '
|
|
'process again.', 'error')
|
|
return redirect(url_for('login', next=request.args.get('next')))
|
|
|
|
|
|
def upgrade():
|
|
token = request.form['stripeToken']
|
|
|
|
g.log = g.log.bind(account=current_user.email)
|
|
g.log.info('Upgrading account.')
|
|
|
|
if not current_user:
|
|
g.log.info('User is not logged, using email received from Stripe.', email=request.form.get('stripeEmail'))
|
|
user = User.query.filter_by(email=request.form['stripeEmail']).first()
|
|
login_user(user, remember=True)
|
|
|
|
sub = None
|
|
try:
|
|
if current_user.stripe_id:
|
|
g.log.info('User already has a subscription. Will change it.')
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
sub = customer.subscriptions.data[0] if customer.subscriptions.data else None
|
|
else:
|
|
g.log.info('Will create a new subscription.')
|
|
customer = stripe.Customer.create(
|
|
email=current_user.email,
|
|
metadata={'formspree_id': current_user.id},
|
|
)
|
|
current_user.stripe_id = customer.id
|
|
|
|
if sub:
|
|
sub.plan = 'gold'
|
|
sub.source = token
|
|
sub.save()
|
|
else:
|
|
customer.subscriptions.create(
|
|
plan='gold',
|
|
source=token
|
|
)
|
|
except stripe.CardError as e:
|
|
g.log.warning("Couldn't charge card.", reason=e.json_body,
|
|
status=e.http_status)
|
|
flash(u"Sorry. Your card could not be charged. Please contact us.",
|
|
"error")
|
|
return redirect(url_for('dashboard'))
|
|
|
|
current_user.plan = Plan.gold
|
|
DB.session.add(current_user)
|
|
DB.session.commit()
|
|
flash(u"Congratulations! You are now a {SERVICE_NAME} "
|
|
"{UPGRADED_PLAN_NAME} user!".format(**settings.__dict__),
|
|
'success')
|
|
g.log.info('Subscription created.')
|
|
|
|
return redirect(url_for('dashboard'))
|
|
|
|
|
|
@login_required
|
|
def resubscribe():
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
sub = customer.subscriptions.data[0] if customer.subscriptions.data else None
|
|
|
|
if not sub:
|
|
flash(u"You can't do this. You are not subscribed to any plan.",
|
|
"warning")
|
|
return redirect(url_for('account'))
|
|
|
|
sub.plan = 'gold'
|
|
sub.save()
|
|
|
|
g.log.info('Resubscribed user.', account=current_user.email)
|
|
at = datetime.datetime.fromtimestamp(sub.current_period_end)
|
|
flash(u'Glad to have you back! Your subscription will now automatically '
|
|
'renew on {date}'.format(date=at.strftime('%A, %B %d, %Y')),
|
|
'success')
|
|
|
|
return redirect(url_for('account'))
|
|
|
|
|
|
@login_required
|
|
def downgrade():
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
sub = customer.subscriptions.data[0] if customer.subscriptions.data else None
|
|
|
|
if not sub:
|
|
flash(u"You can't do this. You are not subscribed to any plan.",
|
|
"warning")
|
|
return redirect(url_for('account'))
|
|
|
|
reason = request.form.get('why')
|
|
if reason:
|
|
send_downgrade_reason_email.delay(current_user.email, reason)
|
|
|
|
sub.cancel_at_period_end = True
|
|
sub.save()
|
|
flash(u"You were unregistered from the {SERVICE_NAME} "
|
|
"{UPGRADED_PLAN_NAME} plan.".format(**settings.__dict__),
|
|
'success')
|
|
at = datetime.datetime.fromtimestamp(sub.current_period_end)
|
|
flash(u"Your card will not be charged anymore, but your plan will "
|
|
"remain active until {date}.".format(
|
|
date=at.strftime('%A, %B %d, %Y')
|
|
), 'info')
|
|
|
|
g.log.info('Subscription canceled from dashboard.', account=current_user.email)
|
|
return redirect(url_for('account'))
|
|
|
|
|
|
def stripe_webhook():
|
|
payload = request.data.decode('utf-8')
|
|
g.log.info('Received webhook from Stripe')
|
|
|
|
sig_header = request.headers.get('STRIPE_SIGNATURE')
|
|
event = None
|
|
|
|
try:
|
|
if settings.TESTING:
|
|
event = request.get_json()
|
|
else:
|
|
event = stripe.Webhook.construct_event(
|
|
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET)
|
|
if event['type'] == 'customer.subscription.deleted': # User subscription has expired
|
|
customer_id = event['data']['object']['customer']
|
|
customer = stripe.Customer.retrieve(customer_id)
|
|
if len(customer.subscriptions.data) == 0:
|
|
user = User.query.filter_by(stripe_id=customer_id).first()
|
|
user.plan = Plan.free
|
|
DB.session.add(user)
|
|
DB.session.commit()
|
|
g.log.info('Downgraded user from webhook.', account=user.email)
|
|
send_downgrade_email.delay(user.email)
|
|
elif event['type'] == 'invoice.payment_failed': # User payment failed
|
|
customer_id = event['data']['object']['customer']
|
|
customer = stripe.Customer.retrieve(customer_id)
|
|
g.log.info('User payment failed', account=customer.email)
|
|
send_email(to=customer.email,
|
|
subject='[ACTION REQUIRED] Failed Payment for {} {}'.format(
|
|
settings.SERVICE_NAME,
|
|
settings.UPGRADED_PLAN_NAME
|
|
),
|
|
text=render_template('email/payment-failed.txt'),
|
|
html=render_template_string(TEMPLATES.get('payment-failed.html')),
|
|
sender=settings.DEFAULT_SENDER)
|
|
return 'ok'
|
|
except ValueError as e:
|
|
g.log.error('Webhook failed for customer', json=event, error=e)
|
|
return 'Failure, developer please check logs', 500
|
|
except stripe.error.SignatureVerificationError as e:
|
|
g.log.error('Webhook failed Stripe signature verification', json=event, error=e)
|
|
return '', 400
|
|
except Exception as e:
|
|
g.log.error('Webhook failed for unknown reason', json=event, error=e)
|
|
return '', 500
|
|
|
|
|
|
@login_required
|
|
def add_card():
|
|
token = request.form['stripeToken']
|
|
|
|
g.log = g.log.bind(account=current_user.email)
|
|
|
|
try:
|
|
if current_user.stripe_id:
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
else:
|
|
customer = stripe.Customer.create(
|
|
email=current_user.email,
|
|
metadata={'formspree_id': current_user.id},
|
|
)
|
|
current_user.stripe_id = customer.id
|
|
# Make sure this card doesn't already exist
|
|
new_fingerprint = stripe.Token.retrieve(token).card.fingerprint
|
|
if new_fingerprint in (card.fingerprint for card in customer.sources.all(object='card').data):
|
|
flash(u'That card already exists in your wallet', 'error')
|
|
else:
|
|
customer.sources.create(source=token)
|
|
flash(u"You've successfully added a new card!", 'success')
|
|
g.log.info('Added card to stripe account.')
|
|
except stripe.CardError as e:
|
|
flash(u"Sorry, there was an error in adding your card. If this "
|
|
"persists, please contact us.", "error")
|
|
g.log.warning("Couldn't add card to Stripe account.",
|
|
reason=e.json_body, status=e.http_status)
|
|
except stripe.error.APIConnectionError:
|
|
flash(u"We're unable to establish a connection with our payment "
|
|
"processor. For your security, we haven't added this "
|
|
"card to your account. Please try again later.", 'error')
|
|
g.log.warning("Couldn't add card to Stripe account. Failed to "
|
|
"communicate with Stripe API.")
|
|
except stripe.error.StripeError:
|
|
flash(u'Sorry, an unknown error occured. Please try again '
|
|
'later. If this problem persists, please contact us.', 'error')
|
|
g.log.warning("Couldn't add card to Stripe account. Unknown error.")
|
|
|
|
return redirect(url_for('billing-dashboard'))
|
|
|
|
@login_required
|
|
def change_default_card(cardid):
|
|
try:
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
customer.default_source = cardid
|
|
customer.save()
|
|
card = customer.sources.retrieve(cardid)
|
|
flash("Successfully changed default payment source to your {} ending in {}".format(card.brand, card.last4), "success")
|
|
except Exception as e:
|
|
flash(u"Sorry something went wrong. If this error persists, please contact support", 'error')
|
|
g.log.warning("Failed to change default card", account=current_user.email, card=cardid)
|
|
return redirect(url_for('billing-dashboard'))
|
|
|
|
@login_required
|
|
def delete_card(cardid):
|
|
if current_user.stripe_id:
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
customer.sources.retrieve(cardid).delete()
|
|
flash(u'Successfully deleted card', 'success')
|
|
g.log.info('Deleted card from account.', account=current_user.email)
|
|
else:
|
|
flash(u"That's an invalid operation", 'error')
|
|
return redirect(url_for('billing-dashboard'))
|
|
|
|
|
|
@login_required
|
|
def account():
|
|
emails = {
|
|
'verified': (e.address for e in current_user.emails.order_by(Email.registered_on.desc())),
|
|
'pending': filter(bool, request.cookies.get('pending-emails', '').split(',')),
|
|
}
|
|
sub = None
|
|
cards = {}
|
|
if current_user.stripe_id:
|
|
try:
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
card_mappings = {
|
|
'Visa': 'cc-visa',
|
|
'American Express': 'cc-amex',
|
|
'MasterCard': 'cc-mastercard',
|
|
'Discover': 'cc-discover',
|
|
'JCB': 'cc-jcb',
|
|
'Diners Club': 'cc-diners-club',
|
|
'Unknown': 'credit-card'
|
|
}
|
|
cards = customer.sources.all(object='card').data
|
|
for card in cards:
|
|
if customer.default_source == card.id:
|
|
card.default = True
|
|
card.css_name = card_mappings[card.brand]
|
|
sub = customer.subscriptions.data[0] if customer.subscriptions.data else None
|
|
if sub:
|
|
sub.current_period_end = datetime.datetime.fromtimestamp(sub.current_period_end).strftime('%A, %B %d, %Y')
|
|
except stripe.error.StripeError:
|
|
return render_template('error.html', title='Unable to connect', text="We're unable to make a secure connection to verify your account details. Please try again in a little bit. If this problem persists, please contact <strong>%s</strong>" % settings.CONTACT_EMAIL)
|
|
|
|
return render_template('users/account.html', emails=emails, cards=cards, sub=sub)
|
|
|
|
@login_required
|
|
def billing():
|
|
if current_user.stripe_id:
|
|
try:
|
|
customer = stripe.Customer.retrieve(current_user.stripe_id)
|
|
card_mappings = {
|
|
'Visa': 'cc-visa',
|
|
'American Express': 'cc-amex',
|
|
'MasterCard': 'cc-mastercard',
|
|
'Discover': 'cc-discover',
|
|
'JCB': 'cc-jcb',
|
|
'Diners Club': 'cc-diners-club',
|
|
'Unknown': 'credit-card'
|
|
}
|
|
cards = customer.sources.all(object='card').data
|
|
for card in cards:
|
|
if customer.default_source == card.id:
|
|
card.default = True
|
|
card.css_name = card_mappings[card.brand]
|
|
sub = customer.subscriptions.data[0] if customer.subscriptions.data else None
|
|
if sub:
|
|
sub.current_period_end = datetime.datetime.fromtimestamp(sub.current_period_end).strftime('%A, %B %d, %Y')
|
|
except stripe.error.StripeError:
|
|
return render_template('error.html', title='Unable to connect', text="We're unable to make a secure connection to verify your account details. Please try again in a little bit. If this problem persists, please contact <strong>%s</strong>" % settings.CONTACT_EMAIL)
|
|
|
|
invoices = stripe.Invoice.list(customer=customer, limit=12)
|
|
return render_template('users/billing.html', cards=cards, sub=sub, invoices=invoices)
|
|
else:
|
|
flash('You have not added your billing information to your Formspree account. '
|
|
'Please contact us if you believe this is an error', 'error')
|
|
return redirect(url_for('account'))
|
|
|
|
@login_required
|
|
def update_invoice_address():
|
|
new_address = request.form.get('invoice-address')
|
|
if len(new_address) == 0:
|
|
new_address = None
|
|
user = User.query.filter_by(email=current_user.email).first()
|
|
user.invoice_address = new_address
|
|
|
|
DB.session.add(user)
|
|
DB.session.commit()
|
|
flash('Successfully updated invoicing address', 'success')
|
|
return redirect(url_for('billing-dashboard'))
|
|
|
|
|
|
@login_required
|
|
def invoice(invoice_id):
|
|
invoice = stripe.Invoice.retrieve('in_' + invoice_id)
|
|
if invoice.customer != current_user.stripe_id:
|
|
return render_template(
|
|
'error.html',
|
|
title='Unauthorized Invoice',
|
|
text='Only the account owner can open this invoice'
|
|
), 403
|
|
if invoice.charge:
|
|
charge = stripe.Charge.retrieve(invoice.charge)
|
|
card_mappings = {
|
|
'Visa': 'cc-visa',
|
|
'American Express': 'cc-amex',
|
|
'MasterCard': 'cc-mastercard',
|
|
'Discover': 'cc-discover',
|
|
'JCB': 'cc-jcb',
|
|
'Diners Club': 'cc-diners-club',
|
|
'Unknown': 'credit-card'
|
|
}
|
|
charge.source.css_name = card_mappings[charge.source.brand]
|
|
return render_template('users/invoice.html', invoice=invoice, charge=charge)
|
|
return render_template('users/invoice.html', invoice=invoice)
|