Prototype website for VPN service, Bitcoin payments via the Blockchain.info API
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
7.6 KiB

10 years ago
  1. from flask import render_template, flash, redirect, g, request, url_for, jsonify
  2. from app import app, db
  3. from models import Ticket, Invoice
  4. from forms import TicketForm
  5. from flask.ext.security import login_required, current_user
  6. from config import BLOCKCHAIN_URL, SECRET_KEY, STASH_WALLET, PRICE_OF_SERVICE, CONFIRMATION_CAP
  7. import simplejson as json
  8. import urllib2
  9. # Page routes
  10. @app.route('/')
  11. @app.route('/index')
  12. def index():
  13. user = g.user
  14. return render_template("index.html", user=user)
  15. @app.route('/blog')
  16. def blog():
  17. return "Da Blog!"
  18. @app.route('/dashboard')
  19. @login_required
  20. def dashboard():
  21. user = g.user
  22. latest_invoice = user.invoices.order_by(Invoice.datepaid.desc()).first()
  23. lastpaid = latest_invoice.datepaid if latest_invoice else "Unpurchased"
  24. expires = latest_invoice.dateends if latest_invoice else "No service enabled"
  25. return render_template("dashboard.html", user=user, latest=latest_invoice, lastpaid=lastpaid, expires=expires)
  26. @app.route('/newticket', methods=['GET', 'POST'])
  27. @login_required
  28. def newticket():
  29. user = g.user
  30. form = TicketForm()
  31. if form.validate_on_submit():
  32. import datetime
  33. t = Ticket()
  34. form.populate_obj(t)
  35. t.timestamp = datetime.datetime.utcnow()
  36. t.created = datetime.datetime.utcnow()
  37. t.user_id = user.id
  38. db.session.add(t);
  39. db.session.commit()
  40. flash('New ticket submitted: ' + form.subject.data)
  41. return redirect('/dashboard')
  42. return render_template('newticket.html', form=form, user=user)
  43. @app.route('/viewticket/<int:tid>', methods=['GET', 'POST'])
  44. @login_required
  45. def viewticket(tid):
  46. user = g.user
  47. t = Ticket.query.get(tid)
  48. return render_template('viewticket.html', user=user, ticket=t)
  49. @app.route('/editticket/<int:tid>', methods=['GET', 'POST'])
  50. @login_required
  51. def editticket(tid):
  52. user = g.user
  53. t = Ticket.query.get(tid)
  54. form = TicketForm(subject=t.subject, body=t.body)
  55. if form.validate_on_submit():
  56. import datetime
  57. form.populate_obj(t)
  58. t.timestamp = datetime.datetime.utcnow()
  59. flash("Updated ticket: " + t.subject)
  60. db.session.commit()
  61. return redirect('/dashboard')
  62. return render_template('editticket.html', user=user, ticket=t, form=form)
  63. @app.route('/deleteticket/<int:tid>', methods=['GET', 'POST'])
  64. @login_required
  65. def deleteticket(tid):
  66. user = g.user
  67. t = Ticket.query.get(tid)
  68. return render_template('viewticket.html', user=user, ticket=t)
  69. # Start Bitcoin stuff -- blockchain.info api
  70. @app.route('/purchase')
  71. def purchase():
  72. user = g.user
  73. try:
  74. exchange_data = json.load(urllib2.build_opener().open(urllib2.Request("http://blockchain.info/tobtc?currency=USD&value=30")))
  75. except urllib2.URLError as e:
  76. flash('Unable to fetch current BTC exchange rate.')
  77. return render_template('purchase.html', user=user, price=0, service_price=PRICE_OF_SERVICE)
  78. return render_template('purchase.html', user=user, price=str(exchange_data), service_price=PRICE_OF_SERVICE)
  79. @app.route('/confirm_purchase/', defaults={'invoice_id': None})
  80. @app.route('/confirm_purchase/<int:invoice_id>', methods=['GET', 'POST'])
  81. @login_required
  82. def confirm_purchase(invoice_id):
  83. import datetime
  84. user = g.user
  85. if invoice_id is None:
  86. i = Invoice()
  87. i.paid = False
  88. i.datecreated = datetime.datetime.utcnow()
  89. i.user_id = user.id
  90. db.session.add(i)
  91. db.session.commit()
  92. try:
  93. callback_url = url_for('pay_invoice', _external=True)+'?secret='+SECRET_KEY+'%26invoice_id='+str(i.id)
  94. url = BLOCKCHAIN_URL+'?method=create&address='+STASH_WALLET+'&callback='+callback_url
  95. xhr = urllib2.Request(url)
  96. data = json.load(urllib2.build_opener().open(xhr))
  97. price_data = json.load(urllib2.build_opener().open(urllib2.Request("http://blockchain.info/tobtc?currency=USD&value="+str(PRICE_OF_SERVICE))))
  98. exchange_data = json.load(urllib2.build_opener().open(urllib2.Request("http://blockchain.info/ticker")))
  99. app.logger.info("Sent to blockchain api: " + url)
  100. except urllib2.URLError as e:
  101. app.logger.error('Unable to access the blockchain.info api: ' + url)
  102. flash('There was an error creating a new invoice. Please try again later.')
  103. return redirect('/dashboard')
  104. i.address = data['input_address']
  105. i.total_btc = price_data
  106. i.exchange_rate_when_paid = exchange_data['USD']['last']
  107. db.session.commit()
  108. # TODO: Generate a QR code and/or other e-z payment options for BTC services
  109. return redirect(url_for('confirm_purchase', invoice_id=i.id))
  110. else:
  111. i = Invoice.query.get(invoice_id)
  112. if request.method == 'POST':
  113. flash('Invoice ('+i.address+') was deleted succesfully.')
  114. db.session.delete(i)
  115. db.session.commit()
  116. return redirect(url_for('dashboard'))
  117. return render_template('confirm_purchase.html', user=user, invoice=i, min_confirm=CONFIRMATION_CAP)
  118. # AJAX Callbacks
  119. @app.route('/invoice_status', methods=['POST'])
  120. def invoice_status():
  121. data = request.form
  122. i = Invoice.query.get(data['invoice_id'])
  123. if i is None:
  124. return 0
  125. return jsonify({
  126. 'confirmations': i.confirmations,
  127. 'value_paid': i.value_paid,
  128. 'total_btc': i.total_btc,
  129. 'input_transaction_hash': i.input_transaction_hash
  130. })
  131. @app.route('/pay_invoice', methods=['GET'])
  132. def pay_invoice():
  133. data = request.args
  134. if 'test' in data:
  135. app.logger.info('Test response recieved from Blockchain.info. return: *test*')
  136. return "*test*"
  137. if 'secret' in data and data['secret'] == SECRET_KEY:
  138. import datetime
  139. i = Invoice.query.get(data['invoice_id'])
  140. if i is None:
  141. # could not find invoice - do we ignore or create?
  142. app.logger.info("Callback received for non-existant invoice. return: *error*")
  143. return "*error*"
  144. if not i.paid:
  145. i.value_paid = float(data['value']) / 100000000
  146. else:
  147. i.value_paid += float(data['value']) / 100000000
  148. i.datepaid = datetime.datetime.utcnow()
  149. i.confirmations = data['confirmations']
  150. i.transaction_hash = data['transaction_hash']
  151. i.input_transaction_hash = data['input_transaction_hash']
  152. if i.value_paid == i.total_btc:
  153. app.logger.info("Invoice {} paid on {} for {} BTC.".format(i.id, i.datepaid, i.value_paid))
  154. i.paid = True
  155. db.session.commit()
  156. if i.paid and i.confirmations > CONFIRMATION_CAP:
  157. app.logger.info("Invoice {} was confirmed at {}. return: *ok*".format(i.id, i.datepaid))
  158. i.is_confirmed = True
  159. i.dateends = i.datepaid + datetime.timedelta(weeks=4)
  160. return "*ok*"
  161. app.logger.info("Callback received for invoice {}: awaiting confirmation (current: {}). return: *unconfirmed*".format(i.id, i.confirmations))
  162. return "*unconfirmed*"
  163. else:
  164. app.logger.info('Payment callback with invalid secret key recieved. return: *error*')
  165. return "*error*"
  166. # Not routes
  167. @app.errorhandler(404)
  168. def internal_error(error):
  169. return render_template('error.html'), 404
  170. @app.errorhandler(500)
  171. def internal_error(error):
  172. db.session.rollback()
  173. return render_template('error.html'), 500
  174. @app.before_request
  175. def before_request():
  176. g.user = current_user
  177. @app.template_filter('date')
  178. def _jinja2_filter_datetime(date, fmt=None):
  179. if not date:
  180. return None
  181. if fmt:
  182. return date.strftime(fmt)
  183. else:
  184. return date.strftime("%m/%d/%y (%I:%M%p)")