Initial commit

This commit is contained in:
Drew DeVault 2015-09-04 22:51:20 -04:00
commit 1059a4d62d
17 changed files with 707 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
*.pyc
bin/
config.ini
alembic.ini
include/
local/
lib/
static/
*.swp
*.rdb
storage/
pip-selfcheck.json
.sass-cache/
overrides/

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2015 Drew DeVault
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
Makefile Normal file
View File

@ -0,0 +1,51 @@
# Builds static assets
# Depends on:
# - scss
# - coffeescript
# - inotify-tools
# Run `make` to compile static assets
# Run `make watch` to recompile whenever a change is made
.PHONY: all static watch clean
STYLES:=$(patsubst styles/%.scss,static/%.css,$(wildcard styles/*.scss))
STYLES+=$(patsubst styles/%.css,static/%.css,$(wildcard styles/*.css))
SCRIPTS:=$(patsubst scripts/%.coffee,static/%.js,$(wildcard scripts/*.coffee))
SCRIPTS+=$(patsubst scripts/%.js,static/%.js,$(wildcard scripts/*.js))
_STATIC:=$(patsubst _static/%,static/%,$(wildcard _static/*))
static/%: _static/%
@mkdir -p static/
cp $< $@
static/%.css: styles/%.css
@mkdir -p static/
cp $< $@
static/%.css: styles/%.scss
@mkdir -p static/
scss -I styles/ $< $@
static/%.js: scripts/%.js
@mkdir -p static/
cp $< $@
static/%.js: scripts/%.coffee
@mkdir -p static/
coffee -m -o static/ -c $<
static: $(STYLES) $(SCRIPTS) $(_STATIC)
all: static
echo $(STYLES)
echo $(SCRIPTS)
clean:
rm -rf static
watch:
while inotifywait \
-e close_write scripts/ \
-e close_write styles/ \
-e close_write _static/; \
do make; done

107
README.md Normal file
View File

@ -0,0 +1,107 @@
# fosspay
Helps you get paid for your open source work.
[![](https://img.shields.io/badge/Donations-fosspay-brightgreen.svg)](https://drewdevault.com/donate)
## Rationale
I write a ton of open source software, but almost none of it is on the scale
that I can expect reliable income from donations, or the sorts of projects that
a business would be likely to fund. It's very unlikely that I'd receive enough
donations from random folks to support full time open source work, but full time
is the best way to make serious progress on your projects.
So - here's how this works: supporters give you one-time or recurring donations,
and after a while you get enough to take a week off from work to spend on open
source work. Since I have several projects, I also ask supporters to tell me
what project they're donating towards, and I distribute the load based on which
projects receive the most support.
## Before you start
Talk to your employer. The way that this is designed to work is that you
continue working full-time at your job, and collect donations. After a while,
you should have enough donations to take some period of unpaid leave - a week, a
month, or whatever works.
* You keep your current job and job security
* You get paid to work on FOSS even with flaky or inconsistent donations
* Everyone wins
There are a few things you need to talk about with your employer:
1. Make sure you own the IP for the things you write during your open source
sprints.
1. Make sure that you have a job to come back to afterwards.
1. Research the tax implications of accepting these donations.
### Stripe
Payments are taken through Stripe, which is pretty headache-free for you to use.
You need to set up an approved Stripe account, which you can get from here:
https://stripe.com/
### Mandrill
You will need a mail server of some sort. If you don't want to go through the
trouble of setting one up, you can use Mandrill:
http://mandrill.com/
You can probably also use your existing mail server, which is what I do, which
makes it easy for people to email you questions and such.
### SSL
You will need an SSL certificate for your website (you also need a domain name).
You can get a free SSL certificate from [StartSSL](http://www.startssl.com/),
but they've always felt pretty... bad to me. You can pay for one instead at
[RapidSSL](https://www.rapidssl.com/), which is what I use personally. You can
also get one for free from [Let's Encrypt](https://letsencrypt.org/) if that
ever happens.
If you need a domain, you can use my referral link for
[Namecheap](http://www.namecheap.com/?aff=84838) and that'd be super nice of
you. Here's a link to Namecheap without the referral link:
[Namecheap](http://www.namecheap.com).
## Installation
Install these things (Arch Linux packages in parenthesis):
* Python 3 (python)
* PostgreSQL (postgresql)
* scss (ruby-sass)
* Flask (python-flask)
* SQLAlchemy (python-sqlalchemy)
* Flask-Login (python-flask-login)
* psycopg2 (python-psycopg2)
* bcrypt (python-bcrypt)
You'll have to configure PostgreSQL yourself and get a connection string that
fosspay can use. Then you can clone this repository to wherever you want to run
it from (I suggest making an unprivledged user account on the server you want to
host this on).
### Configuration
Copy config.ini.example to config.ini and edit it to your liking. Then, you can
run this command to try the site in development mode:
python app.py
[Click here](http://localhost:5000) to visit your donation site and further
instructions will be provided there.
### Production Deployment
To deploy this to production, copy the systemd unit from `contrib/` to your
server at `/etc/systemd/system/` (or whatever's appropriate for your distro).
Use `sytsemctl enable fosspay` and `systemctl start fosspay` to run the site on
`127.0.0.1:8000` (you can change this port by editing the unit file). You should
configure nginx to proxy through to fosspay from whatever other website you
already have. My nginx config is provided in `contrib/` for you to take a look
at - it proxies most requests to Github pages (my blog), and `/donate` to
fosspay.

11
app.py Normal file
View File

@ -0,0 +1,11 @@
from fosspay.app import app
from fosspay.config import _cfg, _cfgi
import os
app.static_folder = os.path.join(os.getcwd(), "static")
import os
if __name__ == '__main__':
app.run(host=_cfg("debug-host"), port=_cfgi('debug-port'), debug=True)

24
config.ini.example Normal file
View File

@ -0,0 +1,24 @@
[dev]
# Change this to the actual location of your site
protocol=http
domain=localhost:5000
# Change this value to something random and secret
secret-key=hello world
# On the debug server, this lets you choose what to bind to
debug-host=0.0.0.0
debug-port=5000
# Fill out these details with your mail server
smtp-host=mail.you.com
smtp-port=587
smtp-user=you
smtp-password=password
smtp-from=donate@you.com
# Your information
your_name=Joe Bloe
your_email=joe@bloe.com
# SQL connection string
connection-string=postgresql://postgres@localhost/fosspay

74
fosspay/app.py Normal file
View File

@ -0,0 +1,74 @@
from flask import Flask, render_template, request, g, Response, redirect, url_for
from flask.ext.login import LoginManager, current_user
from jinja2 import FileSystemLoader, ChoiceLoader
import sys
import os
import locale
from fosspay.config import _cfg, _cfgi
from fosspay.database import db, init_db
from fosspay.objects import User
from fosspay.common import *
from fosspay.network import *
from fosspay.blueprints.html import html
app = Flask(__name__)
app.secret_key = _cfg("secret-key")
app.jinja_env.cache = None
init_db()
login_manager = LoginManager()
login_manager.init_app(app)
app.jinja_loader = ChoiceLoader([
FileSystemLoader("overrides"),
FileSystemLoader("templates"),
])
@login_manager.user_loader
def load_user(email):
return User.query.filter(User.email == email).first()
login_manager.anonymous_user = lambda: None
app.register_blueprint(html)
try:
locale.setlocale(locale.LC_ALL, 'en_US')
except:
pass
if not app.debug:
@app.errorhandler(500)
def handle_500(e):
# shit
try:
db.rollback()
db.close()
except:
# shit shit
print("We're very borked, letting init system kick us back up")
sys.exit(1)
return render_template("internal_error.html"), 500
@app.errorhandler(404)
def handle_404(e):
return render_template("not_found.html"), 404
@app.context_processor
def inject():
return {
'root': _cfg("protocol") + "://" + _cfg("domain"),
'domain': _cfg("domain"),
'protocol': _cfg("protocol"),
'len': len,
'any': any,
'request': request,
'locale': locale,
'url_for': url_for,
'file_link': file_link,
'user': current_user,
'_cfg': _cfg,
'debug': app.debug
}

View File

@ -0,0 +1,39 @@
from flask import Blueprint, render_template, abort, request, redirect, session, url_for, send_file, Response
from flask.ext.login import current_user, login_user, logout_user
from fosspay.objects import *
from fosspay.database import db
from fosspay.common import *
from fosspay.config import _cfg, load_config
import locale
encoding = locale.getdefaultlocale()[1]
html = Blueprint('html', __name__, template_folder='../../templates')
@html.route("/")
def index():
if User.query.count() == 0:
load_config()
return render_template("setup.html")
return render_template("index.html")
@html.route("/setup", methods=["POST"])
def setup():
if not User.query.count() == 0:
abort(400)
email = request.form.get("email")
password = request.form.get("password")
if not email or not password:
return redirect("/") # TODO: Tell them what they did wrong (i.e. being stupid)
user = User(email, password)
user.admin = True
db.add(user)
db.commit()
login_user(user)
return redirect("/admin?first-run=1")
@html.route("/admin")
@adminrequired
def admin():
first=bool(request.args.get("first-run"))
return render_template("admin.html", first=first)

104
fosspay/common.py Normal file
View File

@ -0,0 +1,104 @@
from flask import session, jsonify, redirect, request, Response, abort
from flask.ext.login import current_user
from werkzeug.utils import secure_filename
from functools import wraps
from fosspay.objects import User
from fosspay.database import db, Base
from fosspay.config import _cfg
import json
import urllib
import requests
import xml.etree.ElementTree as ET
import hashlib
def firstparagraph(text):
try:
para = text.index("\n\n")
return text[:para + 2]
except:
try:
para = text.index("\r\n\r\n")
return text[:para + 4]
except:
return text
def with_session(f):
@wraps(f)
def go(*args, **kw):
try:
ret = f(*args, **kw)
db.commit()
return ret
except:
db.rollback()
db.close()
raise
return go
def loginrequired(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user:
return redirect("/login?return_to=" + urllib.parse.quote_plus(request.url))
else:
return f(*args, **kwargs)
return wrapper
def adminrequired(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user:
return redirect("/login?return_to=" + urllib.parse.quote_plus(request.url))
else:
if not current_user.admin:
abort(401)
return f(*args, **kwargs)
return wrapper
def json_output(f):
@wraps(f)
def wrapper(*args, **kwargs):
def jsonify_wrap(obj):
jsonification = json.dumps(obj)
return Response(jsonification, mimetype='application/json')
result = f(*args, **kwargs)
if isinstance(result, tuple):
return jsonify_wrap(result[0]), result[1]
if isinstance(result, dict):
return jsonify_wrap(result)
if isinstance(result, list):
return jsonify_wrap(result)
# This is a fully fleshed out response, return it immediately
return result
return wrapper
def cors(f):
@wraps(f)
def wrapper(*args, **kwargs):
res = f(*args, **kwargs)
if request.headers.get('x-cors-status', False):
if isinstance(res, tuple):
json_text = res[0].data
code = res[1]
else:
json_text = res.data
code = 200
o = json.loads(json_text)
o['x-status'] = code
return jsonify(o)
return res
return wrapper
def file_link(path):
return _cfg("protocol") + "://" + _cfg("domain") + "/" + path
def disown_link(path):
return _cfg("protocol") + "://" + _cfg("domain") + "/disown?filename=" + path

33
fosspay/config.py Normal file
View File

@ -0,0 +1,33 @@
import logging
try:
from configparser import ConfigParser
except ImportError:
# Python 2 support
from ConfigParser import ConfigParser
logger = logging.getLogger("fosspay")
logger.setLevel(logging.DEBUG)
sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
sh.setFormatter(formatter)
logger.addHandler(sh)
# scss logger
logging.getLogger("scss").addHandler(sh)
env = 'dev'
config = None
def load_config():
global config
config = ConfigParser()
config.readfp(open('config.ini'))
load_config()
_cfg = lambda k: config.get(env, k)
_cfgi = lambda k: int(_cfg(k))

14
fosspay/database.py Normal file
View File

@ -0,0 +1,14 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from .config import _cfg, _cfgi
engine = create_engine(_cfg('connection-string'))
db = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
Base = declarative_base()
Base.query = db.query_property()
def init_db():
import fosspay.objects
Base.metadata.create_all(bind=engine)

19
fosspay/network.py Normal file
View File

@ -0,0 +1,19 @@
def makeMask(n):
"return a mask of n bits as a long integer"
return (2 << n - 1) - 1
def dottedQuadToNum(ip):
"convert decimal dotted quad string to long integer"
parts = ip.split(".")
return int(parts[0]) | (int(parts[1]) << 8) | (int(parts[2]) << 16) | (int(parts[3]) << 24)
def networkMask(ip, bits):
"Convert a network address to a long integer"
return dottedQuadToNum(ip) & makeMask(bits)
def addressInNetwork(ip, net):
"Is an address in a network"
return ip & net == net

42
fosspay/objects.py Normal file
View File

@ -0,0 +1,42 @@
from sqlalchemy import Column, Integer, String, Unicode, Boolean, DateTime
from sqlalchemy import ForeignKey, Table, UnicodeText, Text, text
from sqlalchemy.orm import relationship, backref
from .database import Base
from datetime import datetime
import bcrypt
import os
import hashlib
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key = True)
email = Column(String(256), nullable = False, index = True)
admin = Column(Boolean())
password = Column(String)
created = Column(DateTime)
passwordReset = Column(String(128))
passwordResetExpiry = Column(DateTime)
def set_password(self, password):
self.password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def __init__(self, email, password):
self.email = email
self.admin = False
self.created = datetime.now()
self.set_password(password)
def __repr__(self):
return '<User %r>' % self.username
# Flask.Login stuff
# We don't use most of these features
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return self.email

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
stripe
Flask
Jinja2
Flask-Misaka
gunicorn

64
templates/admin.html Normal file
View File

@ -0,0 +1,64 @@
{% extends "layout.html" %}
{% block body %}
<h1>Fosspay Admin</h1>
{% if first %}
<div class="well">
<p>
You're set up and ready to go! This is your admin panel.
Yeah, it's not pretty. Next steps:
</p>
<ol>
<li>
Add some projects. Donors can tell you which project they want to support
when they donate.
</li>
<li>
Customize the look &amp; feel. Look at the contents of the <code>templates</code>
directory - you can copy and paste any of these templates into the
<code>overrides</code> directory and change it to suit your needs.
</li>
<li>
<a href="https://drewdevault.com/donate?project=fosspay">Donate to fosspay upstream?</a>
</li>
<li>
<a href="https://github.com/SirCmpwn/fosspay">Contribute code to fosspay upstream?</a>
</li>
</ol>
</div>
{% endif %}
<h2>Projects</h2>
<div class="row">
<div class="col-md-6">
<table class="table">
<thead>
<tr>
<th>Project Name</th>
<th>One-off donations</th>
<th>Recurring donations</th>
</tr>
</thead>
</table>
</div>
<div class="col-md-6">
<h4>Add Project</h4>
<form method="POST" action="/create-project">
<div class="form-group">
<input class="form-control" type="text" placeholder="Project name" name="name" />
</div>
<input type="submit" value="Add" />
</form>
</div>
</div>
<h2>Donation History</h2>
<table class="table">
<thead>
<tr>
<th>Email</th>
<th>Project</th>
<th>Comment</th>
<th>Amount</th>
<th style="width: 10%">Recurring</th>
</tr>
</thead>
</table>
{% endblock %}

14
templates/layout.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
{% block title %}
<title>Donate to {{_cfg("your-name")}}</title>
{% endblock %}
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
</div>
</body>
</html>

73
templates/setup.html Normal file
View File

@ -0,0 +1,73 @@
{% extends "layout.html" %}
{% block body %}
<h1>FossPay Setup</h1>
<p>Congrats! You have FossPay up and running.</p>
<h2>config.ini</h2>
<ul class="list-unstyled">
<li>
{% if _cfg("secret-key") == "hello world" %}
<span class="glyphicon glyphicon-remove text-danger"></span>
You need to change the secret key to something other than "hello world".
{% else %}
<span class="glyphicon glyphicon-ok text-success"></span>
Your secret key looks good.
{% endif %}
</li>
<li>
{% if _cfg("domain") == "localhost:5000" %}
<span class="glyphicon glyphicon-remove text-danger"></span>
You should change your domain to something other than localhost.
{% else %}
<span class="glyphicon glyphicon-ok text-success"></span>
Your domain is set to "{{_cfg("domain")}}".
{% endif %}
</li>
<li>
{% if _cfg("protocol") != "https" %}
<span class="glyphicon glyphicon-remove text-danger"></span>
Stripe requires your website to use HTTPS.
{% else %}
<span class="glyphicon glyphicon-ok text-success"></span>
Stripe requires your website to use HTTPS.
{% endif %}
</li>
<li>
{% if not _cfg("smtp-host") %}
<span class="glyphicon glyphicon-remove text-danger"></span>
You should configure an SMTP server to send emails with.
{% else %}
<span class="glyphicon glyphicon-ok text-success"></span>
Your email configuration looks good.
{% endif %}
</li>
<li>
{% if not _cfg("stripe-secret") or not _cfg("stripe-publish") %}
<span class="glyphicon glyphicon-remove text-danger"></span>
Your Stripe API keys are not in your config file.
{% else %}
<span class="glyphicon glyphicon-ok text-success"></span>
Your Stripe API keys look good.
{% endif %}
</li>
</ul>
<p>You can make changes and refresh this page if you like.</p>
<h2>Admin Account</h2>
<p>Enter your details for the admin account:</p>
<form class="form" action="/setup" method="POST">
<div class="form-group">
<input type="text" class="form-control" name="email"
placeholder="Email" value="{{_cfg("your-email")}}" />
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="Password" />
</div>
<input type="submit" value="Continue" class="btn btn-primary" />
</form>
{% endblock %}