Skip to content

Commit

Permalink
Merge branch 'enhancement-lastupdated-styling' of github.itap.purdue.…
Browse files Browse the repository at this point in the history
…edu:ECN/webqueue2 into enhancement-lastupdated-styling
  • Loading branch information
campb303 committed Jan 22, 2021
2 parents 3eabe11 + 1084d86 commit 34b73ca
Show file tree
Hide file tree
Showing 33 changed files with 1,008 additions and 212 deletions.
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
# Testing
/coverage

# Productiom
# React Build Files
/build

# Misc
.DS_Store
# React local environment files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Misc
.DS_Store
.vscode/

# Node Package Management
Expand All @@ -28,3 +30,4 @@ yarn-error.log*
/api/venv
__pycache__/
venv-manager.log
/api/.env
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# webqueue2
A re-write of Purdue ECN's webqueue

![UI Snapshot](./docs/UI%20Snapshots/UI-Snapshot%202020-09-22%20at%201.48.58%20PM.png)
![UI Snapshot](./docs/UI%20Snapshots/UI-Snapshot%202020-12-03%20at%208.10.32%20PM.png)

## Stay Up To Date
See what's being worked on with [the webqueue2 Project](https://github.itap.purdue.edu/ECN/webqueue2/projects/).
Expand Down
18 changes: 16 additions & 2 deletions api/ECNQueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,22 @@ def __parseHeaders(self) -> list:
message = email.message_from_string(headerString)

headers = []
dateHeaders=[
"QStatus-Updated-Time",
"Status-Updated-Time",
"Edited-Time",
"QTime-Updated-Time",
"Merged-Time",
"Time-Updated-Time",
"Replied-Time",
"Assigned-To-Updated-Time",
"QAssigned-To-Updated-Time",
"Date",
"Sent"
]

for key in message.keys():
headers.append({"type": key, "content": message[key]})
headers.append({"type": key, "content": self.__getFormattedDate(message[key]) if key in dateHeaders else message[key]})

return headers

Expand Down Expand Up @@ -1331,4 +1345,4 @@ def loadQueues() -> list:
for queue in getValidQueues():
queues.append(Queue(queue))

return queues
return queues
143 changes: 124 additions & 19 deletions api/api.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,114 @@
from flask import Flask
from flask import Flask, request, after_this_request
from flask_restful import Api, Resource
from flask_jwt_extended import (
JWTManager, create_access_token, create_refresh_token,
jwt_required, get_jwt_identity, jwt_refresh_token_required,
set_refresh_cookies, unset_refresh_cookies
)
from werkzeug.security import check_password_hash
import os, dotenv
import ECNQueue

# Load envrionment variables for ./.env
dotenv.load_dotenv()

# Create Flask App
app = Flask(__name__)

# Create API Interface
api = Api(app)


################################################################################
# Configure Flask-JWT-Extended
################################################################################

# Set JWT secret key and create JWT manager
app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY")
# Set identity claim field key to sub for JWT RFC complience
# Flask-JWT-Extended uses 'identity' by default for compatibility reasons
app.config["JWT_IDENTITY_CLAIM"] = "sub"
# Set the key for error messages generated by Flask-JWT-Extended
app.config["JWT_ERROR_MESSAGE_KEY"] = "message"

# Look for JWTs in headers (for access) then cookies (for refresh)
app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies"]
# Restrict cookies to HTTPS in prod, allow HTTP in dev
app.config["JWT_COOKIE_SECURE"] = False if os.environ.get("ENVIRONMENT") == "dev" else True
# Restrict cookies using SameSite=strict flag
app.config["JWT_COOKIE_SAMESITE"] = "strict"
# Restrict refresh tokens to /token/refresh endpoint
app.config["JWT_REFRESH_COOKIE_PATH"] = '/tokens/refresh'
# Set the cookie key for CRSF validation string
# This is the default value. Adding it for easy reference
app.config["JWT_REFRESH_CSRF_HEADER_NAME"] = "X-CSRF-TOKEN"

tokenManager = JWTManager(app)



class Login(Resource):
def post(self) -> tuple:
"""Validates username/password and returns both access and refresh tokens.
Return Codes:
200 (OK): On success.
401 (Unauthroized): When username or password are incorrect.
422 (Unprocessable Entitiy): When the username or password can't be parsed.
Example:
curl -X POST
-H "Content-Type: application/json"
-d '{"username": "bob", "password": "super_secret"}'
{ "access_token": fjr09hfp09h932jp9ruj3.3r8ihf8h0w8hr08ifhj804h8i.8h48ith08ity409hip0t4 }
Returns:
tuple: Response containing tokens and HTTP response code.
"""
if not request.is_json:
return ({ "message": "JSON missing from request body"}, 422)

data = request.json

fields_to_check = ["username", "password"]
for field in fields_to_check:
if field not in data.keys():
return ({ "message": f"{field} missing from request body"}, 422)

if data["username"] != os.environ.get("SHARED_USERNAME"):
return ({ "message": "Username invalid"}, 401)
if not check_password_hash(os.environ.get("SHARED_PASSWORD_HASH"), data["password"]):
return ({ "message": "Password invalid"}, 401)

access_token = create_access_token(data["username"])
refresh_token = create_refresh_token(data["username"])

# This decorator is needed because Flask-RESTful's 'resourceful routing`
# doesn't allow for direct modification to the Flask response object.
# See: https://flask-restful.readthedocs.io/en/latest/quickstart.html#resourceful-routing
@after_this_request
def _does_this_work(response):
set_refresh_cookies(response, refresh_token)
return response

return ({ "access_token": access_token }, 200)

class RefreshAccessToken(Resource):
@jwt_refresh_token_required
def post(self):
username = get_jwt_identity()
access_token = create_access_token(username)
return ({"access_token": access_token}, 200)

class Item(Resource):
def get(self, queue: str, number: int) -> str:
@jwt_required
def get(self, queue: str, number: int) -> tuple:
"""Returns the JSON representation of the item requested.
Return Codes:
200 (OK): On success.
Example:
/api/ce/100 returns:
{
Expand All @@ -38,32 +133,40 @@ def get(self, queue: str, number: int) -> str:
item (int): The number of the item requested.
Returns:
str: JSON representation of the item requested.
tuple: Item as JSON and HTTP response code.
"""
return ECNQueue.Item(queue, number).toJson()
return (ECNQueue.Item(queue, number).toJson(), 200)

class Queue(Resource):
def get(self, queue: str) -> str:
@jwt_required
def get(self, queues: str) -> tuple:
"""Returns the JSON representation of the queue requested.
Return Codes:
200 (OK): On success.
Args:
queue (str): The queue requested.
queues (str): Plus (+) deliminited list of queues.
Returns:
str: JSON representation of the queue requested.
tuple: Queues as JSON and HTTP response code.
"""
queues_requested = queue.split("+")
queues_requested = queues.split("+")

queues = []
queue_list = []
for queue in queues_requested:
queues.append(ECNQueue.Queue(queue).toJson())
queue_list.append(ECNQueue.Queue(queue).toJson())

return queues
return (queue_list, 200)

class QueueList(Resource):
def get(self) -> list:
@jwt_required
def get(self) -> tuple:
"""Returns a list of dictionaries with the number of items in each queue.
Return Codes:
200 (OK): On success.
Example:
[
{
Expand All @@ -77,15 +180,17 @@ def get(self) -> list:
]
Returns:
list: Dictionaries with the number of items in each queue.
tuple: Queues and item counts as JSON and HTTP response code.
"""
return ECNQueue.getQueueCounts()

api.add_resource(QueueList, "/api/get_queues")
api.add_resource(Item, "/api/<string:queue>/<int:number>")
api.add_resource(Queue, "/api/<string:queue>")
return (ECNQueue.getQueueCounts(), 200)



api.add_resource(Login, "/login")
api.add_resource(RefreshAccessToken, "/tokens/refresh")
api.add_resource(Item, "/api/<string:queue>/<int:number>")
api.add_resource(Queue, "/api/<string:queues>")
api.add_resource(QueueList, "/api/get_queues")

if __name__ == "__main__":
app.run()
app.run()
2 changes: 1 addition & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ PyJWT == 1.*
# API Documentation
mkdocs
mkdocs-material
mkautodoc
mkautodoc
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
"@testing-library/user-event": "^7.2.1",
"clsx": "^1.1.1",
"history": "^5.0.0",
"jwt-decode": "^3.1.2",
"material-table": "^1.63.1",
"react": "^16.13.1",
"react-cookie": "^4.0.3",
"react-dom": "^16.13.1",
"react-relative-time": "0.0.7",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-table": "^7.5.1",
"react-router-dom": "^5.2.0"
"react-table": "^7.5.1"
},
"scripts": {
"start:frontend": "react-scripts start",
Expand Down
Loading

0 comments on commit 34b73ca

Please sign in to comment.