Skip to content

Commit

Permalink
DB modules update (#6)
Browse files Browse the repository at this point in the history
Added services for MySQL and MongoDB
Basic routes exposition and simple use case

Co-authored-by: salvatore287 <jovan0000@protonmail.com>
Signed-off-by: vexy <veljko.tekelerovic@gmail.com>
  • Loading branch information
vexy and salvatore287 committed May 14, 2021
1 parent 292b682 commit 0538232
Show file tree
Hide file tree
Showing 20 changed files with 401 additions and 15 deletions.
10 changes: 10 additions & 0 deletions .gitignore
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ __pycache__/

# MacOS stuff
.DS_Store

# VS Code Workspace
*.code-workspace
.vscode
_config.yml

# Virtual Env
bin
pyvenv.cfg
lib
Empty file modified LICENSE
100644 → 100755
Empty file.
31 changes: 26 additions & 5 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,33 @@ Use this template as starting point for more complex projects and requirements.
`JSON Web Tokens` - or [JWT](https://jwt.io/) in short - is the foundation authentication principle used in this template.
Be sure **not to forget** to encode/decode token generation at your own strategy. Follow code comments for exact place where you could modify or customise this behaviour.

### No database !
DB layer has been **intentionally omitted** to allow space for your own implementation. In present form, the code handles all tokens **in memory**, making the data available only while the server is running. All registration data (as well as tokens) will disappear after the server shut down.
### Separate Database layer
DB layer has been split into separate `services` folder. It provides basic Python wrappers for `MySql` and `MongoDB` engines.
In basic form, the code handles all authentication tokens **in memory**, making the data available only while the server is running. All registration data (as well as tokens) will disappear after the server shut down.
For more convenient mechanism, store your tokens in some form of persistent storage, or reuse them in different way.

Data handling services supported so far:
1. SharedStorage (trivial implementation of in-memory storage)
2. MySQL wrapper
3. MongoDB wrapper

### Modularised
Template is designed to support modular structure. Main application modules are stored in `modules` folder. If you need more modules, you can place them inside - as long as they are connected in the main module.
Template is designed to support modular structure.
Following structure is used:
```
- flask-auth-template
|
|
/ services (# contain various services)
/ modules (# contain various additional modules)
/ models (# contain data models used in this template)
```

*NOTE:*
Main application modules are stored in `modules` folder. If you need more modules, you can place them inside - as long as they are connected in the main module. Customize your Flask bluperints to support modularized approach you need.

### Different authentication strategies
Presented here is basic HTTP AUTHENTICATION through Authentication field. Note there are **way secure** authentication mechanisms, such as `OAuth`.
Presented here is **basic** HTTP AUTHENTICATION through Authentication field. Note there are **way secure** authentication mechanisms, such as `OAuth`.

#### CORS setup
For the sake of simplicity, CORS has been enabled completely. Server will accept all origins no matter where the request comes from. Check and/or modify `@app.after_request` directive to further customise desired behaviour (lines [20-24](https://github.com/vexy/flask-auth-template/blob/master/main-module.py#L20-L24) in `main-module.py`).
Expand All @@ -32,9 +50,12 @@ Then proceed with installing dependencies:
```bash
# Run prepacked script
$ . install-dependencies.sh
# or

# install manually through pip3
$ pip3 install -r requirements.txt

# or
python3.8 -m pip instal -r requirements.txt
```

### Starting server
Expand Down
Empty file modified __init__.py
100644 → 100755
Empty file.
Empty file modified _config.yml
100644 → 100755
Empty file.
Empty file modified install-dependencies.sh
100644 → 100755
Empty file.
41 changes: 40 additions & 1 deletion main-module.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

# -*- coding: utf-8 -*-
from services.mongodb import Database
from flask import Flask, Blueprint

# services.* are used as DB hooks below
from services.storage import sharedStorage

from modules.auth import *
Expand All @@ -15,8 +18,9 @@
# register app blueprints
app.register_blueprint(authRoute)
app.register_blueprint(protectedRoute)
# register DB routes later

# make sure this is turned off
# make sure this is turned or configured according to your needs
@app.after_request
def attachCORSHeader(response):
response.headers.set('Access-Control-Allow-Headers', '*')
Expand All @@ -27,6 +31,8 @@ def attachCORSHeader(response):
# ------------------------------
@app.route('/')
def home():
# This route returns a JSON of "users in the system"
# Replace it with your own logic
output = []
for user in sharedStorage.asList():
output.append(str(user))
Expand All @@ -35,6 +41,39 @@ def home():
'storage': output
})

# Publicly accessible routes with DB support
# ------------------------------
@app.route('/mongo_db')
def mongo_db():
from services.mongodb import Database

# This route returns a list of data from a "User" collection
# it assumes having valid MongoDB connection
# Replace it with your own logic
mongoClient = Database("localhost", "user", "pwd")

output = []
output = mongoClient.filter("mainCollection", "{'name': 'someName'}")

# format JSON response
response = jsonify({'results': output})
return response

@app.route('/sql_db')
def mongo_db():
from services.mysql import Database

# This route returns a list of data from a "User" collection
# it assumes having valid MongoDB connection
# Replace it with your own logic
sqlClient = Database()

output = []
output = sqlClient.filter("someTable", "user123")

# format JSON response
response = jsonify({'results': output})
return response

# ---------------------------------
# Server start procedure
Expand Down
Empty file modified models/__init__.py
100644 → 100755
Empty file.
Empty file modified models/user.py
100644 → 100755
Empty file.
Empty file modified modules/__init__.py
100644 → 100755
Empty file.
8 changes: 6 additions & 2 deletions modules/auth.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# public blueprint exposure
authRoute = Blueprint('auth', __name__)

# 👇 implement your strategy here 👇
@authRoute.route('/login', methods=['POST'])
def login():
# get authorization field from HTTP request, early exit if it's not present
Expand Down Expand Up @@ -36,12 +37,14 @@ def login():

return make_response("Wrong credentials.", 401)

# 👇 implement your strategy here 👇
@authRoute.route('/logout')
def logout():
current_app.logger.info("Someone logged out")
# remove token from the storage
# eg. remove/invalidate token from our storage
return "You have been logged out.. But who are you ??"

# 👇 implement your strategy here 👇
@authRoute.route('/register', methods=['POST'])
def registration():
'''
Expand All @@ -56,10 +59,11 @@ def registration():
body = request.json
if body:
username = body['username']
pwd = body['password'] # 👈 add password hashing strategy here
pwd = body['password']
email = body['email']

# add to our storage
# 👈 add password hashing strategy here before saving to DB
newUser = User(username, pwd, email)

current_app.logger.info(f"<AUTH> Adding new user: {newUser.username}, email: {newUser.email}")
Expand Down
14 changes: 8 additions & 6 deletions modules/protected.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from flask import Flask, Blueprint
from flask import jsonify, request, make_response
from flask import current_app

from services.tokenizer import Tokenizer
from services.storage import sharedStorage
from models.user import User
from services.storage import sharedStorage # CONNECT WITH DIFFERENT DB SERVICES HERE

from models.user import User # REPLACE WITH YOUR OWN MODELS IF NEEDED
from functools import wraps

# public blueprint exposure
Expand All @@ -14,7 +16,7 @@
def token_access_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.args.get('token') #get token from URL
token = request.args.get('token') # get token from URL

if not token:
return jsonify({'message': 'Protected area. Valid access token required'}), 403
Expand All @@ -35,14 +37,14 @@ def decorated(*args, **kwargs):

# Protected ROUTES DEFINITION: (split further to standalone Blueprints)
# -----------------------------
@protectedRoute.route('/protected')
@protectedRoute.route('/protected1')
@token_access_required
def protected():
resp_body = jsonify({'message': 'Welcome to protected area, you made it'})
resp_body = jsonify({'message': 'Welcome to protected area 1, you made it'})
return resp_body

@protectedRoute.route('/protected2')
@token_access_required
def protected2():
resp_body = jsonify({'message': 'Welcome to protected area, you made it'})
resp_body = jsonify({'message': 'Welcome to protected area 2, you made it'})
return resp_body
Empty file modified paw_testkit.paw
100644 → 100755
Empty file.
9 changes: 8 additions & 1 deletion requirements.txt
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
Flask==1.1.1
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.3
MarkupSafe==1.1.1
PyJWT==1.7.1
pymongo==3.11.2
Werkzeug==1.0.1
PyMySQL==1.0.2
Empty file modified services/__init__.py
100644 → 100755
Empty file.
111 changes: 111 additions & 0 deletions services/mongodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from pymongo import MongoClient
import datetime

class DBError(Exception):
""" raised on critical db errors """
pass

class DBTypeError(Exception):
""" raised on db type errors """
pass

class Database:
client = None
db = None

def __init__(self, host, user, pwd, data, port=27017):
if not isinstance(host, str):
raise DBTypeError("'host' param must be type of 'str'.")
if not isinstance(user, str):
raise DBTypeError("'user' param must be type of 'str'.")
if not isinstance(pwd, str):
raise DBTypeError("'pwd' param must be type of 'str'.")
if not isinstance(data, str):
raise DBTypeError("'data' param must be type of 'str'.")
if not isinstance(port, int):
raise DBTypeError("'port' param must be type of 'int'.")
try:
self.client = MongoClient(f"mongodb://{user}:{pwd}@{host}:{port}/")
self.db = self.client[data]
print(self.client)
print(self.db)
except:
raise DBError("Failed to connect.")

def close(self):
if self.db is not None:
self.db.close()

def insert(self, collection, fields):
if not isinstance(collection, str):
raise DBTypeError("'collection' param must be type of 'str'.")
if not isinstance(fields, dict):
raise DBTypeError("'fields' param must be type of 'dict'.")
try:
col = self.db[collection]
return col.insert_one(fields)
except:
return None

def update(self, collection, objectID, fields):
if not isinstance(collection, str):
raise DBTypeError("'collection' param must be type of 'str'.")
if not isinstance(fields, dict):
raise DBTypeError("'fields' param must be type of 'dict'.")

try:
col = self.db[collection]
res = col.update_many({"id": objectID}, {"$set": fields})
return res.modified_count
except:
return None

def delete(self, collection, objectID):
if not isinstance(collection, str):
raise DBTypeError("'collection' param must be type of 'str'.")
if not isinstance(objectID, str):
raise DBTypeError("'objectID' param must be type of 'str' or 'ObjectID'.")
try:
col = self.db[collection]
res = col.delete_many({"id": objectID})
return res.deleted_count
except:
return -1

def filter(self, collection, filterCriteria):
if not isinstance(collection, str):
raise DBTypeError("'collection' param must be type of 'str'.")

try:
col = self.db[collection]
res = col.find(filterCriteria)
return res
except:
return None


#
# Example usage as a standalone module
#
if __name__ == "__main__":
HOST = ""
PORT = 27017
USER = "Admin"
PASS = "Pwd"
DATA = "admin"

db = Database(HOST, USER, PASS, DATA, PORT)
x = db.filter("users", "testUser1")
print(f"Find: {x}")
x = db.insert("users", {"name": "testUser1", "number": 3, "number2": 3.5, "addedOn": datetime.datetime.now()})
print(f"Insert: {x}") #x.inserted_id
x = db.filter("users", "testUser1")
print(f"Find: {x}")
x = db.update("users", "testUser1", {"number": 5, "number2": 7.75})
print(f"Update: {x}")
x = db.filter("users", "{user: 'testUser1'}")
print(f"Find: {x}")
x = db.delete("users", "testUser1")
print(f"Delete: {x}")
x = db.filter("users", "testUser1")
print(f"Find: {x}")
Loading

0 comments on commit 0538232

Please sign in to comment.