You've already forked flask-mongo-api-boilerplate
mirror of
https://github.com/LukePeters/flask-mongo-api-boilerplate.git
synced 2026-05-17 07:36:30 +09:00
First push to GitHub
This commit is contained in:
38
api/main/__init__.py
Normal file
38
api/main/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from flask import Flask, request
|
||||
from flask_cors import CORS
|
||||
from pymongo import MongoClient
|
||||
from main.tools import JsonResp
|
||||
from jose import jwt
|
||||
import os
|
||||
|
||||
# Import Routes
|
||||
from main.user.routes import user_blueprint
|
||||
|
||||
def create_app():
|
||||
|
||||
# Flask Config
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile("config/config.cfg")
|
||||
cors = CORS(app, resources={r"/*": { "origins": app.config["FRONTEND_DOMAIN"] }})
|
||||
|
||||
# Misc Config
|
||||
os.environ["TZ"] = app.config["TIMEZONE"]
|
||||
|
||||
# Database Config
|
||||
if app.config["ENVIRONMENT"] == "development":
|
||||
mongo = MongoClient(app.config["MONGO_HOSTNAME"], app.config["MONGO_PORT"])
|
||||
app.db = mongo[app.config["MONGO_APP_DATABASE"]]
|
||||
else:
|
||||
mongo = MongoClient("localhost")
|
||||
mongo[app.config["MONGO_AUTH_DATABASE"]].authenticate(app.config["MONGO_AUTH_USERNAME"], app.config["MONGO_AUTH_PASSWORD"])
|
||||
app.db = mongo[app.config["MONGO_APP_DATABASE"]]
|
||||
|
||||
# Register Blueprints
|
||||
app.register_blueprint(user_blueprint, url_prefix="/user")
|
||||
|
||||
# Index Route
|
||||
@app.route("/")
|
||||
def index():
|
||||
return JsonResp({ "status": "Online" }, 200)
|
||||
|
||||
return app
|
||||
65
api/main/auth/__init__.py
Normal file
65
api/main/auth/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from flask import current_app as app
|
||||
from flask import request
|
||||
from functools import wraps
|
||||
from main.tools import JsonResp
|
||||
from jose import jwt
|
||||
import datetime
|
||||
|
||||
# Auth Decorator
|
||||
def token_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
access_token = request.headers.get('AccessToken')
|
||||
|
||||
try:
|
||||
data = jwt.decode(access_token, app.config['SECRET_KEY'])
|
||||
except Exception as e:
|
||||
return JsonResp({ "message": "Token is invalid", "exception": str(e) }, 401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
def encodeAccessToken(user_id, email, plan):
|
||||
|
||||
accessToken = jwt.encode({
|
||||
"user_id": user_id,
|
||||
"email": email,
|
||||
"plan": plan,
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15) # The token will expire in 15 minutes
|
||||
}, app.config["SECRET_KEY"], algorithm="HS256")
|
||||
|
||||
return accessToken
|
||||
|
||||
def encodeRefreshToken(user_id, email, plan):
|
||||
|
||||
refreshToken = jwt.encode({
|
||||
"user_id": user_id,
|
||||
"email": email,
|
||||
"plan": plan,
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(weeks=4) # The token will expire in 4 weeks
|
||||
}, app.config["SECRET_KEY"], algorithm="HS256")
|
||||
|
||||
return refreshToken
|
||||
|
||||
def refreshAccessToken(refresh_token):
|
||||
|
||||
# If the refresh_token is still valid, create a new access_token and return it
|
||||
try:
|
||||
user = app.db.users.find_one({ "refresh_token": refresh_token }, { "_id": 0, "id": 1, "email": 1, "plan": 1 })
|
||||
|
||||
if user:
|
||||
decoded = jwt.decode(refresh_token, app.config["SECRET_KEY"])
|
||||
new_access_token = encodeAccessToken(decoded["user_id"], decoded["email"], decoded["plan"])
|
||||
result = jwt.decode(new_access_token, app.config["SECRET_KEY"])
|
||||
result["new_access_token"] = new_access_token
|
||||
resp = JsonResp(result, 200)
|
||||
else:
|
||||
result = { "message": "Auth refresh token has expired" }
|
||||
resp = JsonResp(result, 403)
|
||||
|
||||
except:
|
||||
result = { "message": "Auth refresh token has expired" }
|
||||
resp = JsonResp(result, 403)
|
||||
|
||||
return resp
|
||||
21
api/main/config/config.cfg.sample
Normal file
21
api/main/config/config.cfg.sample
Normal file
@@ -0,0 +1,21 @@
|
||||
# Config files are not tracked in Git and must be placed manually in each
|
||||
# app environment (e.g. development, staging, production).
|
||||
|
||||
# General
|
||||
DEBUG = True
|
||||
TIMEZONE = "US/Eastern"
|
||||
SECRET_KEY = ""
|
||||
ENVIRONMENT = "development"
|
||||
FLASK_DIRECTORY = "##FLASK_DIRECTORY##"
|
||||
FLASK_DOMAIN = "##FLASK_DOMAIN##"
|
||||
FLASK_PORT = ##FLASK_PORT##
|
||||
FRONTEND_DOMAIN = "##FRONTEND_DOMAIN##"
|
||||
HTTP_HTTPS = "http://"
|
||||
|
||||
# Database
|
||||
MONGO_HOSTNAME = "##MONGO_HOSTNAME##"
|
||||
MONGO_PORT = 27017
|
||||
MONGO_AUTH_DATABASE = ""
|
||||
MONGO_AUTH_USERNAME = ""
|
||||
MONGO_AUTH_PASSWORD = ""
|
||||
MONGO_APP_DATABASE = "##MONGO_APP_DATABASE##"
|
||||
43
api/main/tools/__init__.py
Normal file
43
api/main/tools/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
def nowEpoch():
|
||||
return int(time.time()) * 1000
|
||||
|
||||
def JsonResp(data, status):
|
||||
from flask import Response
|
||||
import json
|
||||
return Response(json.dumps(data), mimetype="application/json", status=status)
|
||||
|
||||
def randID():
|
||||
randId = randString(3) + randString(3) + randString(3) + randString(3) + randString(3) + randString(3)
|
||||
return randId
|
||||
|
||||
def randString(length):
|
||||
randString = ""
|
||||
for _ in range(length):
|
||||
randString += random.choice("AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890")
|
||||
|
||||
return randString
|
||||
|
||||
def randStringCaps(length):
|
||||
randString = ""
|
||||
for _ in range(length):
|
||||
randString += random.choice("ABCDEFGHJKLMNPQRSTUVWXYZ23456789")
|
||||
|
||||
return randString
|
||||
|
||||
def randStringNumbersOnly(length):
|
||||
randString = ""
|
||||
for _ in range(length):
|
||||
randString += random.choice("23456789")
|
||||
|
||||
return randString
|
||||
|
||||
def validEmail(email):
|
||||
import re
|
||||
|
||||
if re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$", email) != None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
0
api/main/user/__init__.py
Normal file
0
api/main/user/__init__.py
Normal file
152
api/main/user/models.py
Normal file
152
api/main/user/models.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from flask import current_app as app
|
||||
from flask import Flask, request
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
from jose import jwt
|
||||
from main import tools
|
||||
from main import auth
|
||||
import json
|
||||
|
||||
class User:
|
||||
|
||||
def __init__(self):
|
||||
self.defaults = {
|
||||
"id": tools.randID(),
|
||||
"ip_addresses": [request.remote_addr],
|
||||
"acct_active": True,
|
||||
"date_created": tools.nowEpoch(),
|
||||
"last_login": tools.nowEpoch(),
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"email": "",
|
||||
"plan": "free"
|
||||
}
|
||||
|
||||
def get(self):
|
||||
token_data = jwt.decode(request.headers.get('AccessToken'), app.config['SECRET_KEY'])
|
||||
|
||||
user = app.db.users.find_one({ "id": token_data['user_id'] }, {
|
||||
"_id": 0,
|
||||
"password": 0
|
||||
})
|
||||
|
||||
if user:
|
||||
resp = tools.JsonResp(user, 200)
|
||||
else:
|
||||
resp = tools.JsonResp({ "message": "User not found" }, 404)
|
||||
|
||||
return resp
|
||||
|
||||
def getAuth(self):
|
||||
access_token = request.headers.get("AccessToken")
|
||||
refresh_token = request.headers.get("RefreshToken")
|
||||
|
||||
resp = tools.JsonResp({ "message": "User not logged in" }, 401)
|
||||
|
||||
if access_token:
|
||||
try:
|
||||
decoded = jwt.decode(access_token, app.config["SECRET_KEY"])
|
||||
resp = tools.JsonResp(decoded, 200)
|
||||
except:
|
||||
# If the access_token has expired, get a new access_token - so long as the refresh_token hasn't expired yet
|
||||
resp = auth.refreshAccessToken(refresh_token)
|
||||
|
||||
return resp
|
||||
|
||||
def login(self):
|
||||
resp = tools.JsonResp({ "message": "Invalid user credentials" }, 403)
|
||||
|
||||
try:
|
||||
data = json.loads(request.data)
|
||||
email = data["email"].lower()
|
||||
user = app.db.users.find_one({ "email": email }, { "_id": 0 })
|
||||
|
||||
if user and pbkdf2_sha256.verify(data["password"], user["password"]):
|
||||
access_token = auth.encodeAccessToken(user["id"], user["email"], user["plan"])
|
||||
refresh_token = auth.encodeRefreshToken(user["id"], user["email"], user["plan"])
|
||||
|
||||
app.db.users.update({ "id": user["id"] }, { "$set": {
|
||||
"refresh_token": refresh_token,
|
||||
"last_login": tools.nowEpoch()
|
||||
} })
|
||||
|
||||
resp = tools.JsonResp({
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"first_name": user["first_name"],
|
||||
"last_name": user["last_name"],
|
||||
"plan": user["plan"],
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
}, 200)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return resp
|
||||
|
||||
def logout(self):
|
||||
try:
|
||||
tokenData = jwt.decode(request.headers.get("AccessToken"), app.config["SECRET_KEY"])
|
||||
app.db.users.update({ "id": tokenData["user_id"] }, { '$unset': { "refresh_token": "" } })
|
||||
# Note: At some point I need to implement Token Revoking/Blacklisting
|
||||
# General info here: https://flask-jwt-extended.readthedocs.io/en/latest/blacklist_and_token_revoking.html
|
||||
except:
|
||||
pass
|
||||
|
||||
resp = tools.JsonResp({ "message": "User logged out" }, 200)
|
||||
|
||||
return resp
|
||||
|
||||
def add(self):
|
||||
data = json.loads(request.data)
|
||||
|
||||
expected_data = {
|
||||
"first_name": data['first_name'],
|
||||
"last_name": data['last_name'],
|
||||
"email": data['email'].lower(),
|
||||
"password": data['password']
|
||||
}
|
||||
|
||||
# Merge the posted data with the default user attributes
|
||||
self.defaults.update(expected_data)
|
||||
user = self.defaults
|
||||
|
||||
# Encrypt the password
|
||||
user["password"] = pbkdf2_sha256.encrypt(user["password"], rounds=20000, salt_size=16)
|
||||
|
||||
# Make sure there isn"t already a user with this email address
|
||||
existing_email = app.db.users.find_one({ "email": user["email"] })
|
||||
|
||||
if existing_email:
|
||||
resp = tools.JsonResp({
|
||||
"message": "There's already an account with this email address",
|
||||
"error": "email_exists"
|
||||
}, 400)
|
||||
|
||||
else:
|
||||
if app.db.users.save(user):
|
||||
|
||||
# Log the user in (create and return tokens)
|
||||
access_token = auth.encodeAccessToken(user["id"], user["email"], user["plan"])
|
||||
refresh_token = auth.encodeRefreshToken(user["id"], user["email"], user["plan"])
|
||||
|
||||
app.db.users.update({ "id": user["id"] }, {
|
||||
"$set": {
|
||||
"refresh_token": refresh_token
|
||||
}
|
||||
})
|
||||
|
||||
resp = tools.JsonResp({
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"first_name": user["first_name"],
|
||||
"last_name": user["last_name"],
|
||||
"plan": user["plan"],
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
}, 200)
|
||||
|
||||
else:
|
||||
resp = tools.JsonResp({ "message": "User could not be added" }, 400)
|
||||
|
||||
return resp
|
||||
27
api/main/user/routes.py
Normal file
27
api/main/user/routes.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from flask import Blueprint
|
||||
from flask import current_app as app
|
||||
from main.auth import token_required
|
||||
from main.user.models import User
|
||||
|
||||
user_blueprint = Blueprint("user", __name__)
|
||||
|
||||
@user_blueprint.route("/", methods=["GET"])
|
||||
@token_required
|
||||
def get():
|
||||
return User().get()
|
||||
|
||||
@user_blueprint.route("/auth/", methods=["GET"])
|
||||
def getAuth():
|
||||
return User().getAuth()
|
||||
|
||||
@user_blueprint.route("/login/", methods=["POST"])
|
||||
def login():
|
||||
return User().login()
|
||||
|
||||
@user_blueprint.route("/logout/", methods=["GET"])
|
||||
def logout():
|
||||
return User().logout()
|
||||
|
||||
@user_blueprint.route("/", methods=["POST"])
|
||||
def add():
|
||||
return User().add()
|
||||
Reference in New Issue
Block a user