Initial commit
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.pyc
|
||||
*.db
|
||||
*.log
|
||||
venv/
|
||||
|
4
app.py
Executable file
@ -0,0 +1,4 @@
|
||||
#!venv/bin/python
|
||||
from app import app
|
||||
|
||||
app.run(host='0.0.0.0', debug=True)
|
66
app/__init__.py
Normal file
@ -0,0 +1,66 @@
|
||||
from flask import Flask
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask_mail import Mail
|
||||
from flask.ext.security import SQLAlchemyUserDatastore, Security, user_registered
|
||||
from flask.ext.admin import Admin
|
||||
from flask.ext.admin.contrib.sqla import ModelView
|
||||
from config import ADMINS, MAILCONF, SECURITY_EMAIL_SENDER
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('config')
|
||||
|
||||
# Setup SQL database and ORM
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Initialize Flask-Security
|
||||
from models import User, Role, Ticket, Invoice
|
||||
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
||||
security = Security(app, user_datastore)
|
||||
mail = Mail(app)
|
||||
|
||||
# Initialize Flask-Admin
|
||||
from app import admin
|
||||
admin = Admin(app, name='PacketCrypt', index_view=admin.AdminIndex())
|
||||
admin.add_view(ModelView(User, db.session))
|
||||
admin.add_view(ModelView(Ticket, db.session))
|
||||
admin.add_view(ModelView(Invoice, db.session))
|
||||
|
||||
@app.before_first_request
|
||||
def initialize():
|
||||
try:
|
||||
db.create_all()
|
||||
user = user_datastore.find_user(email='br4n@atr0phy.net')
|
||||
if not user:
|
||||
user = user_datastore.create_user(email='br4n@atr0phy.net', password='packetcrypt')
|
||||
user_datastore.add_role_to_user(user, 'Admin')
|
||||
app.logger.info("First run, create default admin user")
|
||||
for role in ('Admin', 'User'):
|
||||
user_datastore.create_role(name=role)
|
||||
db.session.commit()
|
||||
except Exception, e:
|
||||
app.logger.error(str(e))
|
||||
|
||||
@user_registered.connect_via(app)
|
||||
def on_user_registered(sender, **extra):
|
||||
default_role = user_datastore.find_role("User")
|
||||
user_datastore.add_role_to_user(user, default_role)
|
||||
db.session.commit()
|
||||
|
||||
# Import views
|
||||
from app import views
|
||||
|
||||
if not app.debug:
|
||||
import logging
|
||||
from logging.handlers import SMTPHandler, RotatingFileHandler
|
||||
credentials = None
|
||||
if MAILCONF['MAIL_USERNAME'] or MAILCONF['MAIL_PASSWORD']:
|
||||
credentials = (MAILCONF['MAIL_USERNAME'], MAILCONF['MAIL_PASSWORD'])
|
||||
mail_handler = SMTPHandler((MAILCONF['MAIL_SERVER'], MAILCONF['MAIL_PORT']), SECURITY_EMAIL_SENDER, ADMINS, 'PacketCrypt failure', credentials)
|
||||
mail_handler.setLevel(logging.ERROR)
|
||||
app.logger.addHandler(mail_handler)
|
||||
file_handler = RotatingFileHandler('app.log', 'a', 1 * 1024 * 1024, 10)
|
||||
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
|
||||
app.logger.setLevel(logging.INFO)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.info('PacketCrypt startup')
|
8
app/admin.py
Normal file
@ -0,0 +1,8 @@
|
||||
from flask.ext.security import current_user
|
||||
from flask.ext.admin import AdminIndexView, BaseView, expose
|
||||
|
||||
class AdminIndex(AdminIndexView):
|
||||
def is_accessible(self):
|
||||
return current_user.has_role('Admin')
|
||||
|
||||
|
8
app/forms.py
Normal file
@ -0,0 +1,8 @@
|
||||
from flask.ext.wtf import Form
|
||||
from wtforms import TextField, SubmitField, TextAreaField
|
||||
from wtforms.validators import Required
|
||||
|
||||
class TicketForm(Form):
|
||||
subject = TextField('Subject', validators = [Required()])
|
||||
body = TextAreaField('Message', validators = [Required()])
|
||||
|
55
app/models.py
Normal file
@ -0,0 +1,55 @@
|
||||
from app import db
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask.ext.security import UserMixin, RoleMixin
|
||||
|
||||
roles_users = db.Table('roles_users',
|
||||
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
|
||||
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
|
||||
|
||||
class Role(db.Model, RoleMixin):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
name = db.Column(db.String(80), unique=True)
|
||||
description = db.Column(db.String(255))
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(255), unique=True)
|
||||
password = db.Column(db.String(255))
|
||||
active = db.Column(db.Boolean())
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
|
||||
tickets = db.relationship('Ticket', backref='creator', lazy='dynamic')
|
||||
invoices = db.relationship('Invoice', backref='customer', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return '<User %r>' % (self.email)
|
||||
|
||||
class Ticket(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
subject = db.Column(db.String(140), unique=True)
|
||||
body = db.Column(db.String(2000))
|
||||
created = db.Column(db.DateTime)
|
||||
timestamp = db.Column(db.DateTime)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<Ticket %r>' % (self.subject)
|
||||
|
||||
class Invoice(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
is_confirmed = db.Column(db.Boolean())
|
||||
paid = db.Column(db.Boolean())
|
||||
datepaid = db.Column(db.DateTime)
|
||||
datecreated = db.Column(db.DateTime)
|
||||
dateends = db.Column(db.DateTime)
|
||||
total_btc = db.Column(db.Float)
|
||||
exchange_rate_when_paid = db.Column(db.Float)
|
||||
address = db.Column(db.String(34))
|
||||
confirmations = db.Column(db.Integer)
|
||||
transaction_hash = db.Column(db.String)
|
||||
input_transaction_hash = db.Column(db.String)
|
||||
value_paid = db.Column(db.Float)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<Invoice %r>' % (self.id)
|
96
app/static/css/app.css
Normal file
@ -0,0 +1,96 @@
|
||||
body {
|
||||
}
|
||||
|
||||
#top-bar {
|
||||
position: absolute;
|
||||
width:100%;
|
||||
top:0;
|
||||
left:0;
|
||||
background-color: rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
#top-bar div:last-child {
|
||||
margin: 1.0%;
|
||||
}
|
||||
|
||||
#big-top-bar {
|
||||
background: url('/static/img/binary.jpg') no-repeat;
|
||||
min-height: 400px;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.pc-market-block {
|
||||
max-width: 60%;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
#tech-images > img {
|
||||
border-radius: 1.0;
|
||||
padding: 40px 30px;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#pc-dashboard-wrapper {
|
||||
max-width:60%;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding-top:4em;
|
||||
}
|
||||
|
||||
#footer-social > img {
|
||||
border-radius: 100px;;
|
||||
padding: 0 8px;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
}
|
||||
#three-fold-container {
|
||||
max-width: 80%;
|
||||
}
|
||||
#two-fold-container {
|
||||
padding-top:6em;
|
||||
max-width:80%;
|
||||
}
|
||||
|
||||
#container {
|
||||
}
|
||||
|
||||
#form-container {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
#form-container .uk-form-row > textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#bitcoin-logo {
|
||||
bottom: 10px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#btc_qr {
|
||||
margin: 0 auto;
|
||||
display:block
|
||||
}
|
||||
|
||||
.pc-form-row {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
#form-info {
|
||||
width:55%;
|
||||
}
|
||||
#form-info > .uk-button {
|
||||
margin-top:2em;
|
||||
}
|
||||
#form-container > form {
|
||||
width:40%;
|
||||
display:inline-block;
|
||||
}
|
||||
.pc-form-row > input {
|
||||
width:100%;
|
||||
}
|
BIN
app/static/img/android.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
app/static/img/binary.jpg
Normal file
After Width: | Height: | Size: 470 KiB |
BIN
app/static/img/bitcoins.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
app/static/img/facebook.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
app/static/img/gnupg.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
app/static/img/ios.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
app/static/img/openvpn.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
app/static/img/osx.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
app/static/img/reddit.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
app/static/img/twitter.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
app/static/img/windows.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
7643
app/static/lib/uikit/css/uikit.almost-flat.css
Normal file
3
app/static/lib/uikit/css/uikit.almost-flat.min.css
vendored
Normal file
6890
app/static/lib/uikit/css/uikit.css
Normal file
7759
app/static/lib/uikit/css/uikit.gradient.css
Normal file
3
app/static/lib/uikit/css/uikit.gradient.min.css
vendored
Normal file
3
app/static/lib/uikit/css/uikit.min.css
vendored
Normal file
BIN
app/static/lib/uikit/fonts/FontAwesome.otf
Normal file
BIN
app/static/lib/uikit/fonts/fontawesome-webfont.eot
Normal file
BIN
app/static/lib/uikit/fonts/fontawesome-webfont.ttf
Normal file
BIN
app/static/lib/uikit/fonts/fontawesome-webfont.woff
Normal file
1780
app/static/lib/uikit/js/uikit.js
Normal file
3
app/static/lib/uikit/js/uikit.min.js
vendored
Normal file
14
app/templates/_macros.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% macro render_field_with_label(field) %}
|
||||
{% set css_class=kwargs.pop('class', '') %}
|
||||
<span class="uk-text-small uk-text-bold">{{ field.label }}:</span>
|
||||
<div class="uk-form-row pc-form-row">{{ field(class=css_class, **kwargs)|safe }}</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_field_with_label_oneline(field) %}
|
||||
{% set css_class=kwargs.pop('class', '') %}
|
||||
<div class="uk-form-row">{{field.label}} {{field(class=css_class, **kwargs)|safe }}</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_field(field) %}
|
||||
<div class="uk-form-row">{{ field(class=css_class, **kwargs)|safe }}</div>
|
||||
{% endmacro %}
|
84
app/templates/confirm_purchase.html
Normal file
@ -0,0 +1,84 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}Confirming purchase - packetcrypt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-2 uk-panel uk-panel-box uk-container-center">
|
||||
<h2>Invoice Created</h2>
|
||||
Please send {{ invoice.total_btc - invoice.value_paid}} BTC to: <a href="bitcoin:{{invoice.address}}?amount={{invoice.total_btc}}">{{ invoice.address }}</a>
|
||||
<br/><br/>
|
||||
<img src="https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl=bitcoin:{{invoice.address}}?amount={{invoice.total_btc}}&callback=?" id="btc_qr" />
|
||||
<button class="uk-button uk-button-primary uk-button-expand" id="confirm">Confirm Payment <i class="uk-icon-spinner"></i></button>
|
||||
<form action="{{url_for('confirm_purchase', invoice_id=invoice.id)}}" method="POST" name="delete_ticket" style="width:100%;">
|
||||
<button type="submit" class="uk-button uk-button-expand" id="cancel">Cancel Payment</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="modal" class="uk-modal">
|
||||
<div class="uk-modal-dialog uk-modal-dialog-slide">
|
||||
<a class="uk-modal-close uk-close"></a>
|
||||
<h1>Thank you!</h1>
|
||||
<p>Your payment of <label id="valuepaid"></label> <i class="uk-icon-btc"></i> has been confirmed.</p>
|
||||
<p id="paid">Your VPN service is being configured and should be available from your dashboard in the next 5-10 minutes.</p>
|
||||
<p id="notpaid">
|
||||
However, this does not cover the total cost for your PacketCrypt service.
|
||||
Please send <label id="remaining"></label> <i class="uk-icon-btc"></i> to: <a id="btcaddy" >{{invoice.address}}</a>
|
||||
</p>
|
||||
<p>Transaction: <a id="hashlink"></a></p>
|
||||
<a class="uk-button uk-button-success uk-button-expand" href="{{url_for('dashboard')}}">Return to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block postscript %}
|
||||
<script>
|
||||
var amountPaidOnLoad = {{invoice.value_paid}};
|
||||
$(document).ready(function(){
|
||||
var confirmButton = $('#confirm');
|
||||
var cancelButton = $('#cancel');
|
||||
|
||||
confirmButton.click(checkPayment);
|
||||
|
||||
checkPayment();
|
||||
|
||||
});
|
||||
var checkPayment = function (arg){
|
||||
$('#confirm > i').addClass('uk-icon-spin');
|
||||
$.post('/invoice_status', {
|
||||
invoice_id: {{invoice.id}}
|
||||
}).done(function(d){
|
||||
var paidInFull = (d['value_paid'] >= d['total_btc']);
|
||||
var transactionHash = d['input_transaction_hash'];
|
||||
var confirmations = d['confirmations'];
|
||||
|
||||
window.setTimeout(stopSpin, 3500);
|
||||
|
||||
if(confirmations == null || amountPaidOnLoad == d['value_paid']){
|
||||
//$('#flash-wrapper').children(":first").append("<div class='uk-alert uk-alert-warning' data-uk-alert><a class='uk-alert-close uk-close'></a>Unable to confirm payment. Wait a few seconds and try again.</div>");
|
||||
window.setTimeout(checkPayment, 10000);
|
||||
} else {
|
||||
// Setup the modal before display
|
||||
var priceLeft = (d['total_btc'] - d['value_paid']);
|
||||
var btcLink = "bitcoin:{{invoice.address}}?amount="+priceLeft;
|
||||
var hashLink = "https://blockchain.info/tx/"+transactionHash;
|
||||
$('#valuepaid').html(d['value_paid']);
|
||||
$('#remaining').html(d['total_btc'] - d['value_paid']);
|
||||
(paidInFull) ? $('#notpaid').css('display', 'none') : $('#paid').css('display', 'none');
|
||||
$('#btcaddy').attr('href', btcLink);
|
||||
$('#hashlink').attr('href', hashLink).html(transactionHash);
|
||||
|
||||
// Display teh modal
|
||||
var modal = new $.UIkit.modal.Modal('#modal');
|
||||
modal.show();
|
||||
}
|
||||
}).fail(function(){
|
||||
$('#flash-wrapper').children(":first").append("<div class='uk-alert uk-alert-danger' data-uk-alert><a href='' class='uk-alert-close uk-close'></a>An error occured while trying to confirm your payment.</div>");
|
||||
});
|
||||
};
|
||||
|
||||
var stopSpin = function (arg){
|
||||
$('#confirm > i').removeClass('uk-icon-spin');
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
130
app/templates/dashboard.html
Normal file
@ -0,0 +1,130 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %} Dashboard - packetcrypt {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-container-center" id="pc-dashboard-wrapper">
|
||||
<div class="uk-grid">
|
||||
<div class="uk-width-1-1">
|
||||
<h4 class="uk-float-right">Hello, {{ user.email }}!</h4>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="uk-width-1-2">
|
||||
<div class="uk-panel">
|
||||
<h2 class="uk-panel-title">Account Information <i class="uk-icon-user"></i> </h2>
|
||||
<ul>
|
||||
<li>Email Address: {{ user.email }}</li>
|
||||
<li>Last Payment Date: {{ lastpaid }}</li>
|
||||
<li>Plan Expires: {{ expires }}</li>
|
||||
<li>Number of Support Tickets: {{ user.tickets.all() | length }} </li>
|
||||
<li class="uk-text-muted">Traffic This Month: 000Mb (00%)</li>
|
||||
<li class="uk-text-muted">Number of Referred Signups: 0</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-1-2">
|
||||
<div class="uk-panel uk-panel-box uk-panel-box-secondary">
|
||||
<h2 class="uk-panel-title">Controls <i class="uk-icon-cogs uk-float-right"></i> </h2>
|
||||
<hr class="uk-panel-divider">
|
||||
<ul class="uk-nav">
|
||||
<li><a href="">Logout</a></li>
|
||||
<li><a href="">Change Password</a></li>
|
||||
<li><a href="{{ url_for('newticket') }}">Open Support Ticket</a></li>
|
||||
</ul>
|
||||
<div class="uk-panel" style="margin-top:1em;">
|
||||
{% if latest %}
|
||||
<a href="{{url_for('purchase')}}" class="uk-button uk-button-success uk-align-left uk-button-small">Renew</a>
|
||||
{% else %}
|
||||
<a href="{{url_for('purchase')}}" class="uk-button uk-button-success uk-align-left uk-button-small">Purchase</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="uk-button-group uk-align-right" style="">
|
||||
<button class="uk-button uk-button-disabled uk-button-small" type="submit"><i class="uk-icon-folder-open-alt"></i> Download</button>
|
||||
<div data-uk-dropdown>
|
||||
<a href="" class="uk-button uk-button-small"><i class="uk-icon-caret-down"></i></a>
|
||||
<div class="uk-dropdown uk-dropdown-small">
|
||||
<ul class="uk-nav uk-nav-dropdown">
|
||||
<li><a href="">Windows</a></li>
|
||||
<li><a href="">OSX</a></li>
|
||||
<li><a href="">Android</a></li>
|
||||
<li><a href="">iOS</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end button-group -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-1-1">
|
||||
<table class="uk-table uk-table-hover uk-table-striped">
|
||||
<caption>Support Tickets <div class="uk-badge" style="margin-left:1em;">{{ g.user.tickets.all() | length }}</div></caption>
|
||||
<thead>
|
||||
<tr><th>Date</th><th>Subject</th><th>Status</th><th>Last Updated</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if g.user.tickets.all() %}
|
||||
{% for ticket in g.user.tickets.all() %}
|
||||
<tr class="ticket-row">
|
||||
<td>{{ ticket.timestamp |date }}</td>
|
||||
<td><a href="{{ url_for('viewticket', tid=ticket.id) }}"></a>{{ ticket.subject }}</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr><td>You have no support ticket history</td><td></td><td></td><td></td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br/><br/>
|
||||
<table class="uk-table uk-table-hover uk-table-striped">
|
||||
<caption>Invoices <div class="uk-badge uk-badge-danger" style="margin-left:1em;">{{ g.user.invoices.all() | length }}</div></caption>
|
||||
<thead>
|
||||
<tr><th>Date Paid</th><th>Amount Paid</th><th>Payment Address</th><th>Confirmed</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if g.user.invoices.all() %}
|
||||
{% for invoice in g.user.invoices.all() %}
|
||||
<tr class="invoice-row">
|
||||
{% if invoice.datepaid %}
|
||||
<td>{{invoice.datepaid | date}}</td>
|
||||
{% else %}
|
||||
<td>Unpaid</td>
|
||||
{% endif %}
|
||||
<td>{{invoice.value_paid}}</td>
|
||||
<td>{{invoice.address}}</td>
|
||||
{% if invoice.is_confirmed %}
|
||||
<td><a href="{{url_for('confirm_purchase', invoice_id=invoice.id)}}"><i class="uk-icon-ok"></i></a></td>
|
||||
{% else %}
|
||||
{% if invoice.paid %}
|
||||
<td><a href="{{url_for('confirm_purchase', invoice_id=invoice.id)}}"><i class="uk-icon-ban-circle"></i></a></td>
|
||||
{% else %}
|
||||
<td><a href="{{url_for('confirm_purchase', invoice_id=invoice.id)}}" class="uk-button">Pay <i class="uk-icon-btc"></i></a></td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block postscript %}
|
||||
<script type="text/javascript">
|
||||
$(window).load(function(){
|
||||
$(".ticket-row").click(function(){
|
||||
var tid = $(this).find("a").attr("href");
|
||||
window.location = tid
|
||||
});
|
||||
$(".invoice-row").click(function(){
|
||||
var invoice_id = $(this).find("a").attr("href");
|
||||
window.location = invoice_id
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
26
app/templates/editticket.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field %}
|
||||
|
||||
{% block title %}Update Support Ticket - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in form.errors %}
|
||||
{% for error in form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-2 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for('editticket', tid=ticket.id) }}" method="POST" name="form" class="uk-form" style="width:100%">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_label(form.subject) }}
|
||||
{{ render_field_with_label(form.body) }}
|
||||
<div class="uk-form-row">
|
||||
<button class="uk-button uk-button-success" type="submit">Update Ticket</button>
|
||||
<a href="{{url_for('dashboard')}}" class="uk-button uk-button-primary">Close</a>
|
||||
<a href="{{url_for('deleteticket', tid=ticket.id)}}" class="uk-button uk-button-danger">Delete Ticket</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
9
app/templates/error.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-4 uk-panel uk-panel-box uk-container-center">
|
||||
<h1> The file was not found and/or an unexpected error occurred.</h1>
|
||||
<p>The administrator has been notified. Sorry for the inconvenience!</p>
|
||||
<p><a href="{{url_for('index')}}" class="uk-button">Back</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
88
app/templates/index.html
Normal file
@ -0,0 +1,88 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}PacketCrypt - Secure, anonymous VPN service for humans{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="uk-grid">
|
||||
<div class="uk-width-1-1">
|
||||
<div class="uk-panel uk-container-center pc-market-block uk-text-center">
|
||||
<h1>We handle the security, you browse the web.</h1>
|
||||
<span class="uk-text-large">
|
||||
PacketCrypt offers the the quickest way available to keep your web traffic safe.
|
||||
We follow financial industry standards to ensure your data is protected no matter where you're grabbing a connection.
|
||||
PacketCrypt makes it simple to encrypt your internet trafic and go about your day.
|
||||
</span>
|
||||
<div id="tech-images">
|
||||
<img src={{ url_for('static', filename='img/windows.png') }} />
|
||||
<img src={{ url_for('static', filename='img/openvpn.png') }} />
|
||||
<img src={{ url_for('static', filename='img/android.png') }} />
|
||||
<img src={{ url_for('static', filename='img/ios.png') }} />
|
||||
<img src={{ url_for('static', filename='img/osx.png') }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-1-1">
|
||||
<div id="three-fold-container" class="uk-container-center">
|
||||
<div class="uk-grid" data-uk-grid-match="{target:'.uk-panel'}">
|
||||
<div class="uk-width-large-1-3 uk-width-medium-1-1">
|
||||
<div class="uk-panel uk-panel-box uk-panel-box-primary">
|
||||
<h3 class="uk-panel-title uk-text-center"><i class="uk-icon-lock uk-icon-large"></i> Secure</h3>
|
||||
We don't take chances with your data or ours.
|
||||
All of PacketCrypt's services rely on OpenVPN, open source security software trusted by over 5 million users.
|
||||
Run on the safest platform in the world: FreeBSD. Our servers are stable and secured to make sure we're always there for you.
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-large-1-3 uk-width-medium-1-1">
|
||||
<div class="uk-panel uk-panel-box uk-panel-box-primary">
|
||||
<h3 class="uk-panel-title uk-text-center"><i class="uk-icon-download-alt uk-icon-large"></i> Fast</h3>
|
||||
Unlike other anonymous VPN providers, you don't have to sacrifice speed for safety with PacketCrypt.
|
||||
All of our servers worldwide are on a Gigabit pipeline, so you can download as fast as your local connection can handle.
|
||||
Stream movies or download large files fast, and without leaving a trace.
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-large-1-3 uk-width-medium-1-1">
|
||||
<div class="uk-panel uk-panel-box uk-panel-box-primary">
|
||||
<h3 class="uk-panel-title uk-text-center"><i class="uk-icon-group uk-icon-large"></i> Anonymous</h3>
|
||||
The first rule of security is <em>Trust No One</em>, and that includes us.
|
||||
PacketCrypt keeps no logs, and doesn't track your login sessions.
|
||||
You pay us in Bitcoin and all we ask for is an email address.
|
||||
Not only does PacketCrypt protect you from the rest of the internet, we protect you from ourselves.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-1-1">
|
||||
<div id="two-fold-container" class="uk-container-center">
|
||||
<div class="uk-grid" data-uk-grid-match="{target:'.uk-panel'}">
|
||||
<div class="uk-width-1-3">
|
||||
<div class="uk-panel uk-align-left uk-text-center">
|
||||
<h2>Sign Up Now!</h2>
|
||||
<ul class="uk-list uk-list-striped">
|
||||
<li>No personal information required</li>
|
||||
<li>Only <strong id="btcprice">0</strong><i class="uk-icon-btc"></i> BTC</li>
|
||||
<li>Supports all major platforms</li>
|
||||
<li>Unthrottled Gigabit connection</li>
|
||||
<li>1000Gb of transfer per month!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-width-2-3">
|
||||
<div class="uk-panel">
|
||||
<h2>Getting started is easy:</h2>
|
||||
<p>
|
||||
All you need to register is an email address! Bitcoin transactions are processed instantly,
|
||||
and your access will be available within 10 minutes of purchase. Use our custom VPN client
|
||||
for an install-and-go experience or download your client key and use the tools you're
|
||||
familiar with! Click below to sign up for an account and let Packetcrypt secure your web traffic.
|
||||
</p>
|
||||
</span>
|
||||
<a href="{{url_for('purchase')}}" class="uk-button uk-button-primary uk-button-large uk-align-right">Buy Now!</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
66
app/templates/layout.html
Normal file
@ -0,0 +1,66 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='lib/uikit/css/uikit.gradient.min.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}" />
|
||||
|
||||
<!-- Javascript -->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='lib/uikit/js/uikit.min.js') }}"></script>
|
||||
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" class="uk-grid">
|
||||
<div id="big-top-bar" class="uk-width-1-1">
|
||||
<div id="top-bar" class="">
|
||||
<div class="uk-float-left"><img src="http://placehold.it/250x65" /></div>
|
||||
<div class="uk-float-right uk-button-group">
|
||||
<a class="uk-button" href="{{ url_for('index') }}">Home</a>
|
||||
<a class="uk-button" href=" {{ url_for('blog') }}">Blog</a>
|
||||
{% if g.user.is_authenticated() %}
|
||||
<a class="uk-button uk-button-success" href="{{ url_for_security('logout') }}">Logoff</a>
|
||||
<a class="uk-button uk-button-primary" href=" {{ url_for('dashboard') }}">Dashboard</a>
|
||||
{% else %}
|
||||
<a class="uk-button uk-button-success" href="{{ url_for_security('login') }}">Login</a>
|
||||
<a class="uk-button uk-button-primary" href="{{ url_for_security('register') }}">Signup</a>
|
||||
{% endif %}
|
||||
{% if g.user.has_role('Admin') %}
|
||||
<a class="uk-button uk-button-danger" href="/admin/">Admin Panel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="http://bitcoin.org"><img id="bitcoin-logo" src="{{ url_for('static', filename='img/bitcoins.png') }}" /></a>
|
||||
</div>
|
||||
<div id="flash-wrapper" class="uk-width-1-1">
|
||||
<div class="uk-panel uk-width-1-2 uk-container-center">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="uk-alert uk-alert-success" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% block flash %}{% endblock %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="content" class="uk-width-1-1">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div id="footer" class="uk-width-1-1">
|
||||
<hr/>
|
||||
<div class="uk-float-left">
|
||||
© 2013 PacketCrypt, Inc. All Rights Reserved. PacketCrypt™ is property of PacketCrypt, Inc.<br/> All other marks are the property of their respective owners.
|
||||
<br />
|
||||
<a href="">Terms</a> | <a href="">Privacy Policy</a> | <a href="">Contact us</a>
|
||||
</div>
|
||||
<div class="uk-float-right" id="footer-social">
|
||||
<img src="{{ url_for('static', filename='img/reddit.png') }}"/>
|
||||
<img src="{{ url_for('static', filename='img/twitter.png') }}"/>
|
||||
<img src="{{ url_for('static', filename='img/facebook.png') }}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block postscript %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
26
app/templates/newticket.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field %}
|
||||
|
||||
{% block title %}Create Support Ticket - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in form.errors %}
|
||||
{% for error in form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-2 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for('newticket') }}" method="POST" name="form" class="uk-form" style="width:100%;">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_label(form.subject) }}
|
||||
{{ render_field_with_label(form.body) }}
|
||||
<div class="uk-form-row">
|
||||
<button class="uk-button uk-button-primary" type="Submit">Create Ticket</button>
|
||||
<button class="uk-button uk-button-danger" type="reset">Reset</button>
|
||||
<a class="uk-button" href="{{ url_for('dashboard') }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
24
app/templates/purchase.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}Purchase VPN Service Now - packetcrypt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-2 uk-panel uk-panel-box uk-container-center">
|
||||
<h4>Purchase PacketCrypt VPN Service</h4>
|
||||
<ul>
|
||||
<li>Service in 5 different countries</li>
|
||||
<li>4 weeks of access</li>
|
||||
<li>Price (USD): ${{service_price}}</li>
|
||||
{% if price %}
|
||||
<li>Price (BTC): {{ price }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if g.user.is_authenticated() %}
|
||||
<a href="{{url_for('confirm_purchase')}}" class="uk-button uk-button-success">Purchase</a>
|
||||
<a href="{{url_for('dashboard')}}" class="uk-button uk-button-danger">Cancel</a>
|
||||
{% else %}
|
||||
<a href="{{url_for_security('login')}}" class="uk-button uk-button-primary">Login to Purchase</a>
|
||||
<a href="{{url_for_security('register')}}" class="uk-button uk-button-success">Register an Account</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
23
app/templates/security/change_password.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field %}
|
||||
|
||||
{% block title %}Change Password - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in change_password_form.errors %}
|
||||
{% for error in change_password_form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-4 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form" class="uk-form">
|
||||
{{ change_password_form.hidden_tag() }}
|
||||
{{ render_field_with_label(change_password_form.password) }}
|
||||
{{ render_field_with_label(change_password_form.new_password) }}
|
||||
{{ render_field_with_label(change_password_form.new_password_confirm) }}
|
||||
{{ render_field(change_password_form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
25
app/templates/security/forgot_password.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field %}
|
||||
|
||||
{% block title %}Forgot Password - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in forgot_password_form.errors %}
|
||||
{% for error in forgot_password_form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-3-5 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form" class="uk-form">
|
||||
{{ forgot_password_form.hidden_tag() }}
|
||||
{{ render_field_with_label(forgot_password_form.email) }}
|
||||
{{ render_field(forgot_password_form.submit, class="uk-button") }}
|
||||
</form>
|
||||
<div id="form-info" class="uk-align-right uk-text-info">
|
||||
Enter the email-address associated with your account to the left.
|
||||
Instructions on resetting your password will be emailed to you.
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
31
app/templates/security/login_user.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field_with_label_oneline, render_field %}
|
||||
|
||||
{% block title %}Login - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in login_user_form.errors %}
|
||||
{% for error in login_user_form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-3-5 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form" class="uk-form">
|
||||
{{ login_user_form.hidden_tag() }}
|
||||
{{ render_field_with_label(login_user_form.email) }}
|
||||
{{ render_field_with_label(login_user_form.password) }}
|
||||
{{ render_field_with_label_oneline(login_user_form.remember) }}
|
||||
{{ render_field(login_user_form.next) }}
|
||||
{{ render_field(login_user_form.submit, class="uk-button uk-button-primary") }}
|
||||
</form>
|
||||
<div id="form-info" class="uk-align-right uk-text-info">
|
||||
Log in with your Packetcrypt email and password to access your dashboard, download the VPN client, and purchase extended service!
|
||||
If you are unable to remember your password, please use the button below to reset it.
|
||||
|
||||
<a href="{{url_for_security('forgot_password')}}" class="uk-button">Forgot Password?</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
31
app/templates/security/register_user.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field %}
|
||||
|
||||
{% block title %}Register - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in register_user_form.errors %}
|
||||
{% for error in register_user_form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-3-5 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form" class="uk-form" style="display:inline-block;">
|
||||
{{ register_user_form.hidden_tag() }}
|
||||
{{ render_field_with_label(register_user_form.email) }}
|
||||
{{ render_field_with_label(register_user_form.password) }}
|
||||
{% if register_user_form.password_confirm %}
|
||||
{{ render_field_with_label(register_user_form.password_confirm) }}
|
||||
{% endif %}
|
||||
{{ render_field(register_user_form.submit, class='uk-button uk-button-primary') }}
|
||||
</form>
|
||||
<div id="form-info" class="uk-align-right uk-text-info">
|
||||
All we need is an email address and a password to create an account, and you'll be clicks away from browsing securely.
|
||||
After registering please check your inbox for a verification email.
|
||||
|
||||
<br/><p><a href="{{url_for_security('login')}}" class="uk-text-bold uk-text-muted">Click here to sign in to an existing account.</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
22
app/templates/security/reset_password.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_field_with_label, render_field %}
|
||||
|
||||
{% block title %}Reset Password - packetcrypt{% endblock %}
|
||||
{% block flash %}
|
||||
{% for field in reset_password_form.errors %}
|
||||
{% for error in reset_password_form.errors[field] %}
|
||||
<div class="uk-alert uk-alert-danger" data-uk-alert><a href="" class="uk-alert-close uk-close"></a>{{ field }}: {{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-4 uk-panel uk-panel-box uk-container-center">
|
||||
<form action="{{ url_for_security('reset_password') }}" method="POST" name="reset_password_form" class="uk-form">
|
||||
{{ reset_password_form.hidden_tag() }}
|
||||
{{ render_field_with_label(reset_password_form.password) }}
|
||||
{{ render_field_with_label(reset_password_form.password_confirm) }}
|
||||
{{ render_field(reset_password_form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
17
app/templates/viewticket.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}View Support Ticket - packetcrypt{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-container" class="uk-width-1-2 uk-panel uk-panel-box uk-container-center">
|
||||
<h4 class="uk-panel-title">{{ ticket.subject }}</h4>
|
||||
<hr class="uk-panel-divider"/>
|
||||
<span class="ticket-text">{{ ticket.body }}</span>
|
||||
<br/>
|
||||
<span class="uk-article-meta">{{ ticket.timestamp }}</span>
|
||||
<br/><br/>
|
||||
<a href="{{url_for('dashboard')}}" class="uk-button uk-button-primary" type="submit">Close</a>
|
||||
<a href="{{url_for('editticket', tid=ticket.id)}}" class="uk-button uk-button-success">Edit</a>
|
||||
<a href="{{url_for('deleteticket', tid=ticket.id)}}" class="uk-button uk-button-danger">Delete</a>
|
||||
</div>
|
||||
{% endblock %}
|
201
app/views.py
Normal file
@ -0,0 +1,201 @@
|
||||
from flask import render_template, flash, redirect, g, request, url_for, jsonify
|
||||
from app import app, db
|
||||
from models import Ticket, Invoice
|
||||
from forms import TicketForm
|
||||
from flask.ext.security import login_required, current_user
|
||||
from config import BLOCKCHAIN_URL, SECRET_KEY, STASH_WALLET, PRICE_OF_SERVICE, CONFIRMATION_CAP
|
||||
import simplejson as json
|
||||
import urllib2
|
||||
|
||||
# Page routes
|
||||
@app.route('/')
|
||||
@app.route('/index')
|
||||
def index():
|
||||
user = g.user
|
||||
return render_template("index.html", user=user)
|
||||
|
||||
@app.route('/blog')
|
||||
def blog():
|
||||
return "Da Blog!"
|
||||
|
||||
@app.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
user = g.user
|
||||
latest_invoice = user.invoices.order_by(Invoice.datepaid.desc()).first()
|
||||
lastpaid = latest_invoice.datepaid if latest_invoice else "Unpurchased"
|
||||
expires = latest_invoice.dateends if latest_invoice else "No service enabled"
|
||||
return render_template("dashboard.html", user=user, latest=latest_invoice, lastpaid=lastpaid, expires=expires)
|
||||
|
||||
@app.route('/newticket', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def newticket():
|
||||
user = g.user
|
||||
form = TicketForm()
|
||||
if form.validate_on_submit():
|
||||
import datetime
|
||||
t = Ticket()
|
||||
form.populate_obj(t)
|
||||
t.timestamp = datetime.datetime.utcnow()
|
||||
t.created = datetime.datetime.utcnow()
|
||||
t.user_id = user.id
|
||||
db.session.add(t);
|
||||
db.session.commit()
|
||||
|
||||
flash('New ticket submitted: ' + form.subject.data)
|
||||
return redirect('/dashboard')
|
||||
return render_template('newticket.html', form=form, user=user)
|
||||
|
||||
@app.route('/viewticket/<int:tid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def viewticket(tid):
|
||||
user = g.user
|
||||
t = Ticket.query.get(tid)
|
||||
return render_template('viewticket.html', user=user, ticket=t)
|
||||
|
||||
@app.route('/editticket/<int:tid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def editticket(tid):
|
||||
user = g.user
|
||||
t = Ticket.query.get(tid)
|
||||
form = TicketForm(subject=t.subject, body=t.body)
|
||||
if form.validate_on_submit():
|
||||
import datetime
|
||||
form.populate_obj(t)
|
||||
t.timestamp = datetime.datetime.utcnow()
|
||||
flash("Updated ticket: " + t.subject)
|
||||
db.session.commit()
|
||||
return redirect('/dashboard')
|
||||
return render_template('editticket.html', user=user, ticket=t, form=form)
|
||||
|
||||
@app.route('/deleteticket/<int:tid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def deleteticket(tid):
|
||||
user = g.user
|
||||
t = Ticket.query.get(tid)
|
||||
return render_template('viewticket.html', user=user, ticket=t)
|
||||
|
||||
# Start Bitcoin stuff -- blockchain.info api
|
||||
@app.route('/purchase')
|
||||
def purchase():
|
||||
user = g.user
|
||||
try:
|
||||
exchange_data = json.load(urllib2.build_opener().open(urllib2.Request("http://blockchain.info/tobtc?currency=USD&value=30")))
|
||||
except urllib2.URLError as e:
|
||||
flash('Unable to fetch current BTC exchange rate.')
|
||||
return render_template('purchase.html', user=user, price=0, service_price=PRICE_OF_SERVICE)
|
||||
return render_template('purchase.html', user=user, price=str(exchange_data), service_price=PRICE_OF_SERVICE)
|
||||
|
||||
@app.route('/confirm_purchase/', defaults={'invoice_id': None})
|
||||
@app.route('/confirm_purchase/<int:invoice_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def confirm_purchase(invoice_id):
|
||||
import datetime
|
||||
user = g.user
|
||||
if invoice_id is None:
|
||||
i = Invoice()
|
||||
i.paid = False
|
||||
i.datecreated = datetime.datetime.utcnow()
|
||||
i.user_id = user.id
|
||||
db.session.add(i)
|
||||
db.session.commit()
|
||||
try:
|
||||
callback_url = url_for('pay_invoice', _external=True)+'?secret='+SECRET_KEY+'%26invoice_id='+str(i.id)
|
||||
url = BLOCKCHAIN_URL+'?method=create&address='+STASH_WALLET+'&callback='+callback_url
|
||||
xhr = urllib2.Request(url)
|
||||
data = json.load(urllib2.build_opener().open(xhr))
|
||||
price_data = json.load(urllib2.build_opener().open(urllib2.Request("http://blockchain.info/tobtc?currency=USD&value="+str(PRICE_OF_SERVICE))))
|
||||
exchange_data = json.load(urllib2.build_opener().open(urllib2.Request("http://blockchain.info/ticker")))
|
||||
app.logger.info("Sent to blockchain api: " + url)
|
||||
except urllib2.URLError as e:
|
||||
app.logger.error('Unable to access the blockchain.info api: ' + url)
|
||||
flash('There was an error creating a new invoice. Please try again later.')
|
||||
return redirect('/dashboard')
|
||||
i.address = data['input_address']
|
||||
i.total_btc = price_data
|
||||
i.exchange_rate_when_paid = exchange_data['USD']['last']
|
||||
db.session.commit()
|
||||
# TODO: Generate a QR code and/or other e-z payment options for BTC services
|
||||
return redirect(url_for('confirm_purchase', invoice_id=i.id))
|
||||
else:
|
||||
i = Invoice.query.get(invoice_id)
|
||||
if request.method == 'POST':
|
||||
flash('Invoice ('+i.address+') was deleted succesfully.')
|
||||
db.session.delete(i)
|
||||
db.session.commit()
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('confirm_purchase.html', user=user, invoice=i, min_confirm=CONFIRMATION_CAP)
|
||||
|
||||
|
||||
# AJAX Callbacks
|
||||
@app.route('/invoice_status', methods=['POST'])
|
||||
def invoice_status():
|
||||
data = request.form
|
||||
i = Invoice.query.get(data['invoice_id'])
|
||||
if i is None:
|
||||
return 0
|
||||
return jsonify({
|
||||
'confirmations': i.confirmations,
|
||||
'value_paid': i.value_paid,
|
||||
'total_btc': i.total_btc,
|
||||
'input_transaction_hash': i.input_transaction_hash
|
||||
})
|
||||
|
||||
@app.route('/pay_invoice', methods=['GET'])
|
||||
def pay_invoice():
|
||||
data = request.args
|
||||
if 'test' in data:
|
||||
app.logger.info('Test response recieved from Blockchain.info. return: *test*')
|
||||
return "*test*"
|
||||
if 'secret' in data and data['secret'] == SECRET_KEY:
|
||||
import datetime
|
||||
i = Invoice.query.get(data['invoice_id'])
|
||||
if i is None:
|
||||
# could not find invoice - do we ignore or create?
|
||||
app.logger.info("Callback received for non-existant invoice. return: *error*")
|
||||
return "*error*"
|
||||
if not i.paid:
|
||||
i.value_paid = float(data['value']) / 100000000
|
||||
else:
|
||||
i.value_paid += float(data['value']) / 100000000
|
||||
i.datepaid = datetime.datetime.utcnow()
|
||||
i.confirmations = data['confirmations']
|
||||
i.transaction_hash = data['transaction_hash']
|
||||
i.input_transaction_hash = data['input_transaction_hash']
|
||||
if i.value_paid == i.total_btc:
|
||||
app.logger.info("Invoice {} paid on {} for {} BTC.".format(i.id, i.datepaid, i.value_paid))
|
||||
i.paid = True
|
||||
db.session.commit()
|
||||
if i.paid and i.confirmations > CONFIRMATION_CAP:
|
||||
app.logger.info("Invoice {} was confirmed at {}. return: *ok*".format(i.id, i.datepaid))
|
||||
i.is_confirmed = True
|
||||
i.dateends = i.datepaid + datetime.timedelta(weeks=4)
|
||||
return "*ok*"
|
||||
app.logger.info("Callback received for invoice {}: awaiting confirmation (current: {}). return: *unconfirmed*".format(i.id, i.confirmations))
|
||||
return "*unconfirmed*"
|
||||
else:
|
||||
app.logger.info('Payment callback with invalid secret key recieved. return: *error*')
|
||||
return "*error*"
|
||||
|
||||
# Not routes
|
||||
@app.errorhandler(404)
|
||||
def internal_error(error):
|
||||
return render_template('error.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
db.session.rollback()
|
||||
return render_template('error.html'), 500
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.user = current_user
|
||||
|
||||
@app.template_filter('date')
|
||||
def _jinja2_filter_datetime(date, fmt=None):
|
||||
if not date:
|
||||
return None
|
||||
if fmt:
|
||||
return date.strftime(fmt)
|
||||
else:
|
||||
return date.strftime("%m/%d/%y (%I:%M%p)")
|