@@ -0,0 +1,5 @@ | |||
*.pyc | |||
*.db | |||
*.log | |||
venv/ | |||
@@ -0,0 +1,4 @@ | |||
#!venv/bin/python | |||
from app import app | |||
app.run(host='0.0.0.0', debug=True) |
@@ -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') |
@@ -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') | |||
@@ -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()]) | |||
@@ -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) |
@@ -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%; | |||
} |
@@ -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 %} |
@@ -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 %} | |||
@@ -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 %} |
@@ -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 %} |
@@ -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 %} |
@@ -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 %} |
@@ -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> |
@@ -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 %} |
@@ -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 %} |
@@ -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 %} |
@@ -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 %} |
@@ -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 %} | |||
@@ -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 %} |
@@ -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 %} |
@@ -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 %} |
@@ -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)") |