Creating the database and webserver code

Set up MongoDB

If you already have a MongoDB instance and want to use it, simply make sure it is started. If you'd like to run a local, isolated copy, first install MongoDB version 3 or 4, then create an empty directory called "mongodb" inside your application's root directory. Then, start up Mongo using this folder to store its data and its logs, e.g. "mongod --dbpath ./mongodb --logpath ./mongodb/mongodb.log"

Install Python and required modules

If you don't yet have Python on your machine, install Python 2.7 or 3.x (the code here should work in either version, but we recommend 3.6 or 3.7 to stay current).

Next, we need to install the modules that Python will need. From the command line, run:

pip install "algorithmia" "flask >= 1.1" "pillow" "python-resize-image" "pyjwt" "pymongo"

Or simply "pip install -r requirements.txt" against the project's requirements file.

Add directories for file storage

Create a directory called "static" inside your app's root directory. Place a blank file called index.html inside it, as well as the files default_avatar.png and loading.gif. Then, create a subdirectory called "avatars" inside of "static"... this will be used to hold profile images uploaded by the app's users.

Write the Flask app

Create a new file app.py in your app's root directory, and add the following to the top (we'll talk about these libraries as we add more code below):

import Algorithmia
from datetime import datetime, timedelta
from flask import Flask, jsonify, request, send_file
from functools import wraps
import jwt
from os import environ, path
from PIL import Image
from resizeimage import resizeimage
from pymongo import MongoClient
from shutil import copyfile
from tempfile import NamedTemporaryFile
from uuid import uuid4
from werkzeug.security import generate_password_hash, check_password_hash


Next, we initialize the Flask server. By default, Flask will statically serve any files in the "static" directory under the path "/static"; we override this to serve at the root URL "/" by setting static_url_path="". Then, we add a rotating secret key; this effectively logs everyone out when the server is restarted, so if you wish to avoid this, generate a GUID manually and paste it in instead of using str(uuid4()).
And we avoid caching of any files by setting the max file age to 0 (remove this in production).

We'll also need a database connection, established here via MongoClient. We set the database name and connect to the collection called "users" (Mongo will automatically create it if id does not yet exist. Lastly, we guarantee that no two users can have the same ID by adding a uniqueness restriction if it doesn't yet exist. This also serves to verify that Mongo is running, so we can warn and exit if not.

# init flask app
app = Flask(__name__, static_url_path='')
app.secret_key = str(uuid4())
app.send_file_max_age_default = 0

# connect to db
db_client = MongoClient()
db = db_client.fullstack_demo
users = db.users

# init db
try:
if not db.users.list_indexes().alive:
db.users.create_index('id', unique=True)
except:
raise SystemExit('Unable to connect to database: please run "mongod --fork --dbpath ./mongodb --logpath ./mongodb/mongodb.log"')


We'll be using Algorithmia for our Machine Learning, so we create a client instance to connect. To avoid embedding our API Key into the source code, we'll grab it from the system env:

# create an Algorithmia client and temp dir in Hosted Data
try:
algorithmia_api_key = environ['ALGORITHMIA_API_KEY']
except KeyError:
raise SystemExit('Please set the evironment variable ALGORITHMIA_API_KEY, obtained from https://algorithmia.com/user#credentials')
client = Algorithmia.client(algorithmia_api_key)
algo_temp_dir = 'data://.my/temp/'
if not client.dir(algo_temp_dir).exists():
client.dir(algo_temp_dir).create()


Now we're ready to start writing the actual application code. The only datastructure we'll use for now is the User profile, which will contain their id (an email address), a pasword (hashed for security), an profile image (avatar) which defaults to the default_avatar.png served from our static directory, their full name, and a short description of themselves (bio). We also provide a to_dict convenience method which does not include the password hash (for use when sending User instances to the frontend) , and a from_dict method which allows us to create a User instance from a dict (we'll use this to restore Users previously saved in MongoDB, so this method does expect a password hash).

To make it easy to load users from the DB (with or without a password, depending on the situation), we'll also add a user_loader method:

# datastructures
class User():

def __init__(self, email, password, avatar='/default_avatar.png', name='', bio=''):
self.id = email
self.passhash = generate_password_hash(password) if password else None
self.avatar = avatar
self.name = name
self.bio = bio

def to_dict(self):
return dict(id=self.id, avatar=self.avatar, name=self.name, bio=self.bio)

@staticmethod
def from_dict(user_dict):
user = User(user_dict['id'], None, user_dict['avatar'], user_dict['name'], user_dict['bio'])
user.passhash = user_dict['passhash']
return user


def user_loader(email, password=None):
user_dict = users.find_one({'id': email})
if not user_dict:
return None
if password and not check_password_hash(user_dict['passhash'], password):
return None
return User.from_dict(user_dict)

Now let's add our machine learning functions: is_nude will call sfw/NudityDetectioni2v to screen images for nudity, while auto_crop will use opencv/SmartThumbnail to crop images to a specific size, while centering the image around "salient features" -- objects such as faces which shouldn't ever be cropped out of a profile image. We'll also include a convenience wrapper for uploading files to Algorithmia's hosted data in a single call:

# Algorithmia helper functions
def upload_file_algorithmia(local_filename, unique_id):
remote_file = algo_temp_dir + unique_id
client.file(remote_file).putFile(local_filename)
return remote_file


def is_nude(remote_file):
try:
algo = client.algo('sfw/NudityDetectioni2v/0.2.13')
return algo.pipe(remote_file).result['nude']
except Exception as x:
print('ERROR: unable to check %s for nudity: %s' % (remote_file, x))
return False


def auto_crop(remote_file, height, width):
input = {
'height': height,
'width': width,
'image': remote_file
}
try:
algo = client.algo('opencv/SmartThumbnail/2.2.3')
return algo.pipe(input).result['output']
except Exception as x:
print('ERROR: unable to auto-crop %s: %s' % (remote_file, x))
return remote_file

 

Our frontend code will need a way of managing a users's authenticated session, so we add methods for creating a JWT Token and for validating a presented token -- the latter being a wrapper function which can be used to decorate other methods, guaranteeing that they can only be run if a User was found via the presented token, and adding the user into the method call:

# auth helper functions
def generate_jwt(user):
return jwt.encode({
'id': user.id,
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=30)},
app.config['SECRET_KEY'])


def token_required(f):
@wraps(f)
def _verify(*args, **kwargs):
auth_headers = request.headers.get('Authorization', '').split()
try:
if len(auth_headers) != 2:
raise jwt.InvalidTokenError()
token = auth_headers[1]
data = jwt.decode(token, app.config['SECRET_KEY'])
user = user_loader(data['id'])
if not user:
return jsonify({'message':'Invalid credentials','authenticated':False}), 401
return f(user, *args, **kwargs)
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return jsonify({'message':'Invalid or expired token','authenticated':False}), 401
return _verify

 

All communication with the frontend app will be via REST API, so we now add a set of routes which define the methods to be run for GET and POST requests to specific URLs. These all return JSON which will be parsed by the frontend, with one notable exception: if somebody browses to the root URL "/" of our app, we should just hand back the index.htm file to be displayed in the web browser.

"/register" and "/login" both accept an email/password pair as input, and either create a new user (checking for collisions first), or log in an existing user. In both cases, they pass back the user's profile data, as well as a new authentication token which the frontend app can store and present calls to other routes.

GET calls to "/account" will retrieve the current user's profile; note the use of the @token_required decorator which turns the presented token into a User, which is then passed into the method call. A POST to the same URL allows them to change their name and bio, and saves this change to MongoDB before returning the updated profile info. There is no method provided for changing the user's password: feel free to try writing this on your own!

Lastly, POSTs to "/avatar" (which sets a new profile image) expect multipart form data instead of JSON, since multipart is better for binary file transfers. Instead of using request.get_json(), this method checks request.files['avatar'] for a new image, runs it through Algorithmia's nudity check and auto-crop routines, then saves it into a folder called "avatars" under our app's "static" directory, updating the User profile to use this new image URL.

# routes for webapp
@app.route('/', methods=['GET'])
def home():
return send_file('static/index.htm')


@app.route('/register', methods=('POST',))
def register():
data = request.get_json()
if user_loader(data['email']):
return jsonify({'message':'A user with that email already exists','authenticated':False}), 409
user = User(data['email'],data['password'])
users.insert_one(user.__dict__)
token = generate_jwt(user)
return jsonify({'token': token.decode('UTF-8'), 'user': user.to_dict()}), 201


@app.route('/login', methods=('POST',))
def login():
data = request.get_json()
user = user_loader(data['email'],data['password'])
if not user:
return jsonify({'message':'Invalid credentials','authenticated':False}), 401
token = generate_jwt(user)
return jsonify({'token': token.decode('UTF-8'), 'user': user.to_dict()})


@app.route('/account', methods=['GET'])
@token_required
def get_account(user):
token = generate_jwt(user)
return jsonify({'token': token.decode('UTF-8'), 'user': user.to_dict()})


@app.route('/account', methods=['POST'])
@token_required
def post_account(user):
data = request.get_json()
user.name = data.get('name',user.name)
user.bio = data.get('bio',user.bio)
users.replace_one({'id': user.id}, user.__dict__)
return jsonify({'user': user.to_dict()}), 201


@app.route('/avatar', methods=['POST'])
@token_required
def post_avatar(user):
avatar = request.files['avatar']
file_ext = path.splitext(avatar.filename)[1]
with NamedTemporaryFile(suffix=file_ext) as f:
with Image.open(avatar) as img:
try:
cover = resizeimage.resize_cover(img, [400, 400])
cover.save(f, img.format)
except:
img.save(f)
remote_file = upload_file_algorithmia(f.name, user.id+file_ext)
if is_nude(remote_file):
return jsonify({'message':'That image may contain nudity'}), 422
cropped_remote_file = auto_crop(remote_file, 200, 200)
cropped_file = client.file(cropped_remote_file).getFile()
user.avatar = ('/avatars/%s%s' % (user.id, file_ext)).lower()
copyfile(cropped_file.name, 'static/'+user.avatar)
users.replace_one({'id': user.id}, user.__dict__)
return jsonify({'user': user.to_dict()}), 201

 

One last finishing touch: so that the server can be easily started by just calling this file, we'll add a main() method which starts up the server:


# to start server: "python3 ./app.py"
if __name__ == '__main__':
app.run()

 

That's it! Compare your file to the completed example before moving on to the frontend code.


NEXT: writing and styling the frontend