Skip to content

Commit 0408503

Browse files
committed
flask backend application code
1 parent 8b6a146 commit 0408503

File tree

15 files changed

+792
-0
lines changed

15 files changed

+792
-0
lines changed

api/__init__.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from cmath import log
2+
import os
3+
4+
from flask import Flask
5+
from flask_sqlalchemy import SQLAlchemy
6+
from flask_socketio import SocketIO
7+
from celery import Celery
8+
9+
from .config import flask_config
10+
11+
# Flask extensions
12+
db = SQLAlchemy()
13+
socketio = SocketIO(cors_allowed_origins="*", engineio_logger=True)
14+
celery = Celery(__name__,
15+
broker=os.environ.get('CELERY_BROKER_URL', 'redis://'),
16+
backend=os.environ.get('CELERY_BROKER_URL', 'redis://'))
17+
celery.config_from_object('api.celery_config')
18+
19+
# Import models so that they are registered with SQLAlchemy
20+
from . import models # noqa
21+
22+
# Import celery task so that it is registered with the Celery workers
23+
from .tasks import run_flask_request # noqa
24+
25+
# Import Socket.IO events so that they are registered with Flask-SocketIO
26+
from . import events # noqa
27+
28+
29+
def create_app(config_name=None, main=True):
30+
if config_name is None:
31+
config_name = os.environ.get('FLASK_ENV', 'development')
32+
app = Flask(__name__, static_folder='../build', static_url_path='/')
33+
app.config.from_object(flask_config[config_name])
34+
35+
# Initialize flask extensions
36+
db.init_app(app)
37+
38+
@app.cli.command('createdb')
39+
def createdb():
40+
db.create_all()
41+
42+
if main:
43+
# Initialize socketio server and attach it to the message queue, so
44+
# that everything works even when there are multiple servers or
45+
# additional processes such as Celery workers wanting to access
46+
# Socket.IO
47+
socketio.init_app(app,
48+
message_queue=app.config['SOCKETIO_MESSAGE_QUEUE'])
49+
else:
50+
# Initialize socketio to emit events through through the message queue
51+
# Note that since Celery does not use eventlet, we have to be explicit
52+
# in setting the async mode to not use it.
53+
socketio.init_app(None,
54+
message_queue=app.config['SOCKETIO_MESSAGE_QUEUE'],
55+
async_mode='threading')
56+
celery.conf.update(flask_config[config_name].CELERY_CONFIG)
57+
58+
# Register web application routes
59+
from .main import main as main_blueprint
60+
app.register_blueprint(main_blueprint)
61+
62+
# Register API routes
63+
from .blueprints import api as api_blueprint
64+
app.register_blueprint(api_blueprint, url_prefix='/api')
65+
66+
# Register async tasks support
67+
from .tasks import tasks_bp as tasks_blueprint
68+
app.register_blueprint(tasks_blueprint, url_prefix='/tasks')
69+
70+
return app

api/app_aux.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
3+
from api import create_app
4+
5+
# Create an application instance that web servers can use. We store it as
6+
# "application" (the wsgi default) and also the much shorter and convenient
7+
# "app".
8+
application = app = create_app(os.environ.get('FLASK_ENV', 'production'), main=False)

api/auth.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from flask import g, jsonify, session
2+
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
3+
4+
from . import db
5+
from .models import User
6+
7+
8+
# Authentication objects for username/password auth, token auth, and a
9+
# token optional auth that is used for open endpoints.
10+
basic_auth = HTTPBasicAuth()
11+
token_auth = HTTPTokenAuth('Bearer')
12+
token_optional_auth = HTTPTokenAuth('Bearer')
13+
14+
15+
@basic_auth.verify_password
16+
def verify_password(nickname, password):
17+
"""Password verification callback."""
18+
if not nickname or not password:
19+
return False
20+
user = User.query.filter_by(nickname=nickname).first()
21+
if user is None or not user.verify_password(password):
22+
return False
23+
if user.ping():
24+
from .events import push_model
25+
push_model(user)
26+
db.session.add(user)
27+
db.session.commit()
28+
g.current_user = user
29+
return True
30+
31+
32+
@basic_auth.error_handler
33+
def password_error():
34+
"""Return a 401 error to the client."""
35+
# To avoid login prompts in the browser, use the "Bearer" realm.
36+
return (jsonify({'error': 'authentication required'}), 401,
37+
{'WWW-Authenticate': 'Bearer realm="Authentication Required"'})
38+
39+
40+
@token_auth.verify_token
41+
def verify_token(token, add_to_session=False):
42+
"""Token verification callback."""
43+
if add_to_session:
44+
# clear the session in case auth fails
45+
if 'nickname' in session:
46+
del session['nickname']
47+
user = User.query.filter_by(token=token).first()
48+
if user is None:
49+
return False
50+
if user.ping():
51+
from .events import push_model
52+
push_model(user)
53+
db.session.add(user)
54+
db.session.commit()
55+
g.current_user = user
56+
if add_to_session:
57+
session['nickname'] = user.nickname
58+
return True
59+
60+
61+
@token_auth.error_handler
62+
def token_error():
63+
"""Return a 401 error to the client."""
64+
return (jsonify({'error': 'authentication required'}), 401,
65+
{'WWW-Authenticate': 'Bearer realm="Authentication Required"'})
66+
67+
68+
@token_optional_auth.verify_token
69+
def verify_optional_token(token):
70+
"""Alternative token authentication that allows anonymous logins."""
71+
if token == '':
72+
# no token provided, mark the logged in users as None and continue
73+
g.current_user = None
74+
return True
75+
# but if a token was provided, make sure it is valid
76+
return verify_token(token)

api/blueprints/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from flask import Blueprint
2+
3+
api = Blueprint('api', __name__)
4+
5+
from . import tokens, users, messages # noqa

api/blueprints/messages.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from flask import request, abort, jsonify, g, url_for
2+
3+
from .. import db
4+
from ..auth import token_auth, token_optional_auth
5+
from ..models import Message
6+
from ..utils import timestamp
7+
from ..tasks import async_task
8+
from . import api
9+
10+
11+
@api.route('/messages', methods=['POST'])
12+
@token_auth.login_required
13+
@async_task
14+
def new_message():
15+
"""
16+
Post a new message.
17+
This endpoint is requires a valid user token.
18+
"""
19+
msg = Message.create(request.get_json() or {})
20+
db.session.add(msg)
21+
db.session.commit()
22+
r = jsonify(msg.to_dict())
23+
r.status_code = 201
24+
r.headers['Location'] = url_for('api.get_message', id=msg.id)
25+
return r
26+
27+
28+
@api.route('/messages', methods=['GET'])
29+
@token_optional_auth.login_required
30+
def get_messages():
31+
"""
32+
Return list of messages.
33+
This endpoint is publicly available, but if the client has a token it
34+
should send it, as that indicates to the server that the user is online.
35+
"""
36+
since = int(request.args.get('updated_since', '0'))
37+
day_ago = timestamp() - 24 * 60 * 60
38+
if since < day_ago:
39+
# do not return more than a day worth of messages
40+
since = day_ago
41+
msgs = Message.query.filter(Message.updated_at > since).order_by(
42+
Message.updated_at)
43+
return jsonify({'messages': [msg.to_dict() for msg in msgs.all()]})
44+
45+
46+
@api.route('/messages/<id>', methods=['GET'])
47+
@token_optional_auth.login_required
48+
def get_message(id):
49+
"""
50+
Return a message.
51+
This endpoint is publicly available, but if the client has a token it
52+
should send it, as that indicates to the server that the user is online.
53+
"""
54+
return jsonify(Message.query.get_or_404(id).to_dict())
55+
56+
57+
@api.route('/messages/<id>', methods=['PUT'])
58+
@token_auth.login_required
59+
@async_task
60+
def edit_message(id):
61+
"""
62+
Modify an existing message.
63+
This endpoint is requires a valid user token.
64+
Note: users are only allowed to modify their own messages.
65+
"""
66+
msg = Message.query.get_or_404(id)
67+
if msg.user != g.current_user:
68+
abort(403)
69+
msg.from_dict(request.get_json() or {})
70+
db.session.add(msg)
71+
db.session.commit()
72+
return '', 204

api/blueprints/tokens.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from flask import jsonify, g
2+
3+
from .. import db
4+
from ..auth import basic_auth, token_auth
5+
6+
from . import api
7+
8+
9+
@api.route('/tokens', methods=['POST'])
10+
@basic_auth.login_required
11+
def new_token():
12+
"""
13+
Request a user token.
14+
This endpoint is requires basic auth with nickname and password.
15+
"""
16+
if g.current_user.token is None:
17+
g.current_user.generate_token()
18+
db.session.add(g.current_user)
19+
db.session.commit()
20+
return jsonify(g.current_user.to_dict())
21+
22+
23+
@api.route('/tokens', methods=['DELETE'])
24+
@token_auth.login_required
25+
def revoke_token():
26+
"""
27+
Revoke a user token.
28+
This endpoint is requires a valid user token.
29+
"""
30+
g.current_user.token = None
31+
db.session.add(g.current_user)
32+
db.session.commit()
33+
return '', 204

api/blueprints/users.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from flask import request, abort, jsonify, g, url_for
2+
3+
from .. import db
4+
from ..auth import token_auth, token_optional_auth
5+
from ..models import User
6+
7+
from . import api
8+
9+
10+
@api.route('/users', methods=['POST'])
11+
def new_user():
12+
"""
13+
Register a new user.
14+
This endpoint is publicly available.
15+
"""
16+
user = User.create(request.get_json() or {})
17+
if User.query.filter_by(nickname=user.nickname).first() is not None:
18+
abort(400)
19+
db.session.add(user)
20+
db.session.commit()
21+
r = jsonify(user.to_dict())
22+
r.status_code = 201
23+
r.headers['Location'] = url_for('api.get_user', id=user.id)
24+
return r
25+
26+
27+
@api.route('/users', methods=['GET'])
28+
@token_optional_auth.login_required
29+
def get_users():
30+
"""
31+
Return list of users.
32+
This endpoint is publicly available, but if the client has a token it
33+
should send it, as that indicates to the server that the user is online.
34+
"""
35+
users = User.query.order_by(User.updated_at.asc(), User.nickname.asc())
36+
if request.args.get('online'):
37+
users = users.filter_by(online=(request.args.get('online') != '0'))
38+
if request.args.get('updated_since'):
39+
users = users.filter(
40+
User.updated_at > int(request.args.get('updated_since')))
41+
return jsonify({'users': [user.to_dict() for user in users.all()]})
42+
43+
44+
@api.route('/users/<id>', methods=['GET'])
45+
@token_optional_auth.login_required
46+
def get_user(id):
47+
"""
48+
Return a user.
49+
This endpoint is publicly available, but if the client has a token it
50+
should send it, as that indicates to the server that the user is online.
51+
"""
52+
return jsonify(User.query.get_or_404(id).to_dict())
53+
54+
55+
@api.route('/users/<id>', methods=['PUT'])
56+
@token_auth.login_required
57+
def edit_user(id):
58+
"""
59+
Modify an existing user.
60+
This endpoint is requires a valid user token.
61+
Note: users are only allowed to modify themselves.
62+
"""
63+
user = User.query.get_or_404(id)
64+
if user != g.current_user:
65+
abort(403)
66+
user.from_dict(request.get_json() or {})
67+
db.session.add(user)
68+
db.session.commit()
69+
return '', 204

api/celery_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# global Celery options that apply to all configurations
2+
3+
# enable the pickle serializer
4+
task_serializer = 'pickle'
5+
result_serializer = 'pickle'
6+
accept_content = ['pickle']
7+
broker_connection_retry_on_startup = True

api/config.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import os
2+
3+
4+
class Config(object):
5+
DEBUG = False
6+
TESTING = False
7+
SECRET_KEY = os.environ.get('SECRET_KEY', '51f52814-0071-11e6-a247-000ec6c2372c')
8+
SERVER_NAME = os.environ.get("SERVER_NAME", "127.0.0.1:5000")
9+
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///db.sqlite3')
10+
SQLALCHEMY_TRACK_MODIFICATIONS = False
11+
REQUEST_STATS_WINDOW = 15
12+
CELERY_CONFIG = {}
13+
SOCKETIO_MESSAGE_QUEUE = os.environ.get(
14+
'SOCKETIO_MESSAGE_QUEUE', os.environ.get('CELERY_BROKER_URL',
15+
'redis://'))
16+
17+
18+
class DevelopmentConfig(Config):
19+
DEBUG = True
20+
21+
22+
class ProductionConfig(Config):
23+
pass
24+
25+
26+
class TestingConfig(Config):
27+
TESTING = True
28+
SERVER_NAME = 'localhost'
29+
SQLALCHEMY_DATABASE_URI = 'sqlite://'
30+
CELERY_CONFIG = {'task_always_eager': True}
31+
SOCKETIO_MESSAGE_QUEUE = None
32+
33+
34+
flask_config = {
35+
'development': DevelopmentConfig,
36+
'production': ProductionConfig,
37+
'testing': TestingConfig
38+
}

0 commit comments

Comments
 (0)