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 session manipulation API
  • Loading branch information
tdemin committed Jun 13, 2019
1 parent 67c208e commit ff5d272
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 20 deletions.
9 changes: 6 additions & 3 deletions project_amber/app.py
Expand Up @@ -5,7 +5,9 @@
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
from project_amber.handlers.auth import login, logout
from project_amber.handlers.session import handle_session_req, \
handle_session_id_req
from project_amber.handlers.task import handle_task_id_request, \
handle_task_request
from project_amber.handlers.users import signup
Expand All @@ -16,12 +18,13 @@

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.add_url_rule("/api/task", "task", handle_task_request, \
methods=["GET", "POST"])
app.add_url_rule("/api/task/<task_id>", "task_id", handle_task_id_request, \
methods=["GET", "PATCH", "DELETE"])
app.add_url_rule("/api/session", "session", handle_session_req, methods=["GET"])
app.add_url_rule("/api/session/<session_id>", "session_id", \
handle_session_id_req, methods=["GET", "DELETE"])

if config["allow_signup"]:
app.add_url_rule("/api/signup", "signup", signup, methods=["POST"])
Expand Down
7 changes: 7 additions & 0 deletions project_amber/const.py
Expand Up @@ -2,9 +2,16 @@

EMPTY_RESP = dumps({}) # Empty response, to be used in requests.

DAY_SECONDS = 60 * 60 * 24
MATURE_SESSION = DAY_SECONDS * 2 # The difference in times between the login
# time and the time when a session is considered "mature" (e.g can remove other
# sessions).

MSG_NO_TOKEN = "No X-Auth-Token present"
MSG_INVALID_TOKEN = "Invalid token"
MSG_USER_NOT_FOUND = "This user does not exist"
MSG_USER_EXISTS = "The user with this name already exists"
MSG_IMMATURE_SESSION = "This session is too new, and cannot remove others"

MSG_TASK_NOT_FOUND = "This task does not exist"
MSG_TEXT_NOT_SPECIFIED = "No text specified"
2 changes: 1 addition & 1 deletion project_amber/errors.py
Expand Up @@ -42,7 +42,7 @@ class NotFound(HTTPError):
def __init__(self, message="Entity not found"):
super().__init__(self.code, message)

class NoAccess(HTTPError):
class Forbidden(HTTPError):
"""
Exception class for restricted access areas.
"""
Expand Down
10 changes: 1 addition & 9 deletions project_amber/handlers/auth.py
Expand Up @@ -7,14 +7,6 @@
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:
Expand Down Expand Up @@ -44,5 +36,5 @@ def logout():
Logout handler. Accepts empty JSON. Returns HTTP 200 on success.
"""
user = handleChecks()
removeSession(user.token)
removeSession(user.token, user.id)
return EMPTY_RESP
73 changes: 73 additions & 0 deletions project_amber/handlers/session.py
@@ -0,0 +1,73 @@
from json import dumps
from time import time

from flask import request

from project_amber.const import MATURE_SESSION, MSG_IMMATURE_SESSION, EMPTY_RESP
from project_amber.errors import Forbidden
from project_amber.helpers.auth import handleChecks, getSessions, getSession,\
removeSessionById

def handle_session_req():
"""
Request handler for `/api/session`. Only accepts GET requests. Returns a
list of sessions like the one below:
```
{
"sessions": [
{
"id": 1,
"login_time": 123456, // timestamp
"address": "127.0.0.1"
}
{
"id": 2,
"login_time": 123457,
"address": "10.0.0.1"
}
]
}
```
"""
user = handleChecks()
sessions = getSessions(user.id)
sessionList = []
for session in sessions:
sessionList.append({
"id": session.id,
"login_time": session.login_time,
"address": session.address
})
return dumps({
"sessions": sessionList
})

def handle_session_id_req(session_id: int):
"""
Login handler for `/api/session/<id>`. Accepts GET and DELETE
requests. Returns 404 if this session does not exist. On successful
GET, returns JSON like this:
```
{
"id": 1,
"login_time": 123456, // timestamp
"address": "127.0.0.1"
}
```
On DELETE, this will return HTTP 200 with empty JSON. There is a special
case here: if a client session is too recent, this will respond with
HTTP 403.
"""
user = handleChecks()
if request.method == "GET":
session = getSession(session_id, user.id)
return dumps({
"id": session.id,
"login_time": session.login_time,
"address": session.address
})
if request.method == "DELETE":
if (time() - user.login_time) < MATURE_SESSION:
raise Forbidden(MSG_IMMATURE_SESSION)
removeSessionById(session_id, user.id)
return EMPTY_RESP
4 changes: 2 additions & 2 deletions project_amber/handlers/task.py
Expand Up @@ -2,7 +2,7 @@

from flask import request

from project_amber.const import EMPTY_RESP
from project_amber.const import EMPTY_RESP, MSG_TEXT_NOT_SPECIFIED
from project_amber.errors import BadRequest
from project_amber.helpers.auth import handleChecks
from project_amber.helpers.task import addTask, getTask, getTasks, \
Expand Down Expand Up @@ -59,7 +59,7 @@ def handle_task_request():
if request.method == "POST":
text = request.json.get("text")
if text is None:
raise BadRequest("No text specified")
raise BadRequest(MSG_TEXT_NOT_SPECIFIED)
status = request.json.get("status")
# if only I could `get("status", d=0)` like we do that with dicts
if status is None:
Expand Down
39 changes: 35 additions & 4 deletions project_amber/helpers/auth.py
Expand Up @@ -19,10 +19,11 @@ class LoginUser:
and ID. The corresponding fields are `name` and `id`, respectively.
Also contains a token field.
"""
def __init__(self, name: str, uid: int, token: str):
def __init__(self, name: str, uid: int, token: str, login_time: int):
self.name = name
self.id = uid
self.token = token
self.login_time = login_time

def handleChecks() -> LoginUser:
"""
Expand All @@ -45,7 +46,7 @@ def handleChecks() -> LoginUser:
user = db.session.query(User).filter_by(id=user_session.user).one_or_none()
if user is None:
raise InternalServerError(MSG_USER_NOT_FOUND)
user_details = LoginUser(user.name, user.id, token)
user_details = LoginUser(user.name, user.id, token, user_session.login_time)
return user_details

def addUser(name: str, password: str) -> int:
Expand Down Expand Up @@ -105,13 +106,43 @@ def createSession(name: str, password: str) -> str:
return token
raise Unauthorized

def removeSession(token: str) -> str:
def removeSession(token: str, uid: int) -> str:
"""
Removes a user session by token. Returns the token on success.
"""
session = db.session.query(Session).filter_by(token=token).one_or_none()
session = db.session.query(Session).filter_by(token=token, user=uid)\
.one_or_none()
if session is None:
raise NotFound
db.session.delete(session)
db.session.commit()
return token

def removeSessionById(session_id: int, uid: int) -> int:
"""
Removes a user session by session ID. Returns the session ID on success.
"""
session = db.session.query(Session).filter_by(id=session_id, user=uid)\
.one_or_none()
if session is None:
raise NotFound
db.session.delete(session)
db.session.commit()
return session_id

def getSessions(uid: int) -> list:
"""
Returns a list of sessions of a user (class `Session`).
"""
sessions = db.session.query(Session).filter_by(user=uid).all()
return sessions

def getSession(session_id: int, uid: int) -> Session:
"""
Returns a single `Session` by its ID.
"""
session = db.session.query(Session).filter_by(id=session_id, user=uid)\
.one_or_none()
if session is None:
raise NotFound
return session
3 changes: 2 additions & 1 deletion project_amber/models/auth.py
Expand Up @@ -15,7 +15,8 @@ class Session(db.Model):
"""
Holds auth session details (auth token, the time of login, etc).
"""
token = db.Column(db.String(256), primary_key=True)
id = db.Column(db.Integer, primary_key=True)
token = db.Column(db.String(256), unique=True, nullable=False)
user = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
login_time = db.Column(db.Integer, nullable=False)
address = db.Column(db.String(100), nullable=False)
Expand Down

0 comments on commit ff5d272

Please sign in to comment.