Skip to content
This repository has been archived by the owner on Jan 20, 2024. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
Add a current implementation of login/logout logic
* Add logging and some custom error templates
* Add a hackish error handling
  • Loading branch information
tdemin committed Jun 4, 2019
1 parent 9bb0481 commit cb96fa2
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 10 deletions.
32 changes: 26 additions & 6 deletions project_amber/app.py
@@ -1,14 +1,34 @@
from json import dumps

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound

from project_amber.config import config
from project_amber.db import db
from project_amber.errors import HTTPError
from project_amber.handlers.auth import login, logout, login_check

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = config["database"]
db = SQLAlchemy(app)
db.init_app(app)

app.add_url_rule("/api/login", "login", login, methods=["POST"])
app.add_url_rule("/api/logout", "logout", logout, methods=["POST"])
app.add_url_rule("/api/login_check", "login_check", login_check, methods=["GET"])

@app.before_first_request
def create_tables():
db.create_all() # create all tables on first run

db.create_all() # create all tables on first run
@app.errorhandler(HTTPError)
def handle_HTTP_errors(e):
return dumps({
"message": e.message
}), e.code

@app.route("/")
def hello():
return "works"
# Hack.
@app.errorhandler(NoResultFound)
def handle_NoResultFound_errors():
return dumps({
"message": "Entity not found."
}), 404
3 changes: 2 additions & 1 deletion project_amber/config.py
Expand Up @@ -7,7 +7,8 @@
configPaths = ["config.json"]

config = {
"database": ""
"database": "",
"loglevel": 0
}

# search for every file name and load the config from the first file
Expand Down
3 changes: 3 additions & 0 deletions project_amber/const.py
@@ -0,0 +1,3 @@
from json import dumps

EMPTY_RESP = dumps({}) # Empty response, to be used in requests.
3 changes: 3 additions & 0 deletions project_amber/db.py
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
63 changes: 63 additions & 0 deletions project_amber/errors.py
@@ -0,0 +1,63 @@
from http import HTTPStatus

from project_amber.logging import logError

class HTTPError(Exception):
"""
Base class for all possible errors.
"""
def __init__(self, code: int, message: str):
"""
Initialize the error object.
`code` - HTTP code.
`message` - Descriptive error message (string).
"""
self.code = code
self.message = message
logError(self.message)
super(HTTPError, self).__init__()

class BadRequest(HTTPError):
"""
Exception class for payload data parsing errors.
"""
code = HTTPStatus.BAD_REQUEST
message = "Bad request"
def __init__(self):
super().__init__(self.code, self.message)

class InternalServerError(HTTPError):
"""
Exception class for DB errors. Probably going to be left unused.
"""
code = HTTPStatus.INTERNAL_SERVER_ERROR
message = "Internal server error"
def __init__(self):
super().__init__(self.code, self.message)

class NotFound(HTTPError):
"""
Exception class for users/groups/subjects not found.
"""
code = HTTPStatus.NOT_FOUND
message = "Resource not found"
def __init__(self):
super().__init__(self.code, self.message)

class NoAccess(HTTPError):
"""
Exception class for restricted access areas.
"""
code = HTTPStatus.FORBIDDEN
message = "Access denied"
def __init__(self):
super().__init__(self.code, self.message)

class Unauthorized(HTTPError):
"""
Exception class for login/auth check errors.
"""
code = HTTPStatus.UNAUTHORIZED
message = "Unauthorized"
def __init__(self):
super().__init__(self.code, self.message)
48 changes: 48 additions & 0 deletions project_amber/handlers/auth.py
@@ -0,0 +1,48 @@
from json import dumps

from flask import request

from project_amber.const import EMPTY_RESP
from project_amber.errors import BadRequest
from project_amber.helpers.auth import handleChecks, removeSession, \
createSession

def login_check():
"""
Essentially a heartbeat request that drops HTTP 401 when
unauthorized. Returns HTTP 200 with an empty response if otherwise.
"""
handleChecks()
return EMPTY_RESP

def login():
"""
Login handler. Accepts this JSON:
```
{
"name": "some_user_name",
"password": "some_password"
}
```
Returns HTTP 200 with the JSON below on success:
```
{
"token": "some_auth_token"
}
```
Drops HTTP 401 on fail.
"""
if not request.is_json:
raise BadRequest
if not "name" in request.json or not "password" in request.json:
raise BadRequest
token = createSession(request.json["name"], request.json["password"])
return dumps({ "token": token })

def logout():
"""
Logout handler. Accepts empty JSON. Returns HTTP 200 on success.
"""
user = handleChecks()
removeSession(user.token)
return EMPTY_RESP
60 changes: 58 additions & 2 deletions project_amber/helpers/auth.py
@@ -1,10 +1,44 @@
from hashlib import sha256
from base64 import b64encode
from time import time

from bcrypt import hashpw, gensalt, checkpw
from flask import request

from project_amber.app import db
from project_amber.models.auth import User
from project_amber.db import db
from project_amber.errors import Unauthorized, BadRequest
from project_amber.models.auth import User, Session

class LoginUser:
"""
Representational class for request checks. Contains the user name
and ID. The corresponding fields are `name` and `id`, respectively.
Also contains a token field.
"""
def __init__(self, name: str, uid: int, token: str):
self.name = name
self.id = uid
self.token = token

def handleChecks() -> LoginUser:
"""
Login handler. Works with Flask's `request`. Returns an object
containing the user's name and their ID. Raises an exception if
the auth token is not valid.
This is essentially a request decorator that is implemented as a
function. This also checks whether the request contains valid JSON,
and drops HTTP 400 if not.
"""
if not request.is_json:
raise BadRequest
token = request.headers.get("X-Auth-Token")
if token is None:
raise Unauthorized
user_session = db.session.query(Session).filter_by(token=token).one()
user = db.session.query(User).filter_by(id=user_session.user)
user_details = LoginUser(user.name, user.id, token)
return user_details

def addUser(name: str, password: str) -> int:
"""
Expand Down Expand Up @@ -34,3 +68,25 @@ def verifyPassword(uid: int, password: str) -> bool:
user = db.session.query(User).filter_by(id=uid).one()
prehashed_pw = b64encode(sha256(password).digest())
return checkpw(prehashed_pw, user.password)

def createSession(name: str, password: str) -> str:
"""
Creates a new user session. Returns an auth token.
"""
user = db.session.query(User).filter_by(name=name).one()
if verifyPassword(user.id, password):
token = sha256(gensalt()).digest()
session = Session(token=token, user=user.id, login_time=time())
db.session.add(session)
db.session.commit()
return token
raise Unauthorized

def removeSession(token: str) -> str:
"""
Removes a user session by token. Returns the token on success.
"""
session = db.session.query(Session).filter_by(token=token).one()
db.session.delete(session)
db.session.commit()
return token
21 changes: 21 additions & 0 deletions project_amber/logging.py
@@ -0,0 +1,21 @@
import logging

from project_amber.config import config

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("sjbackend")

def log(message):
"""
Wrapper for the logger calls. Only intended to be used in DB code.
"""
# This wrapper only logs things in case of loglevel being set to 2
# (log requests).
if config["loglevel"] == 2:
logger.info(message)

def logError(message):
"""
Wrapper for the error messages.
"""
logger.error(message)
2 changes: 1 addition & 1 deletion project_amber/models/auth.py
@@ -1,4 +1,4 @@
from project_amber.app import db
from project_amber.db import db

class User(db.Model):
"""
Expand Down

0 comments on commit cb96fa2

Please sign in to comment.