From 1de44ad39f755ff76dfce2374047314883fa2b53 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Wed, 4 Nov 2020 11:57:34 -0500 Subject: [PATCH 01/64] Add pyjwt to API requirements --- api/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/api/requirements.txt b/api/requirements.txt index 8a009e1..a7f38ba 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -10,6 +10,7 @@ Jinja2==2.11.2 lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 +PyJWT==1.7.1 pylint==2.5.3 python-dateutil==2.8.1 pytz==2020.1 From 44865c2ef7a276097f86d8576f50e1c412a4eaba Mon Sep 17 00:00:00 2001 From: Tyler Jordan Wright Date: Wed, 4 Nov 2020 14:25:46 -0500 Subject: [PATCH 02/64] Changed color of selected row to a color that is easier to see --- src/components/ItemTable/ItemTable.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ItemTable/ItemTable.js b/src/components/ItemTable/ItemTable.js index a45d8ed..cbc1bf2 100644 --- a/src/components/ItemTable/ItemTable.js +++ b/src/components/ItemTable/ItemTable.js @@ -20,9 +20,7 @@ export default function ItemTable({ data }) { opacity: 0.2, }, rowSelected: { - "&$selected, &$selected:hover": { - backgroundColor: theme.palette.primary, - }, + backgroundColor: theme.palette.type === 'light' ? theme.palette.primary[100] : theme.palette.primary[600], }, bandedRows: { '&:nth-of-type(even)': { @@ -134,8 +132,10 @@ export default function ItemTable({ data }) { history.push(`/${row.original.queue}/${row.original.number}`); setSelecetedRow({ queue: row.original.queue, number: row.original.number }) }} + // This functionality should be achieved by using the selected prop and + // overriding the selected class but this applied the secondary color at 0.08% opacity. + // Overridding the root class is a workaround. classes={{ root: isSelected ? classes.rowSelected : classes.bandedRows }} - selected={selectedRow.queue === row.original.queue && selectedRow.number === row.original.number} {...row.getRowProps()} > {row.cells.map(cell => ( From 0398df25068e48ed596ace6c5c9255a5e17aed74 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Wed, 4 Nov 2020 20:11:54 -0500 Subject: [PATCH 03/64] Add dotenv support to API --- api/api.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/api/api.py b/api/api.py index f77e7fc..dbadc9a 100644 --- a/api/api.py +++ b/api/api.py @@ -1,6 +1,10 @@ from flask import Flask from flask_restful import Api, Resource import ECNQueue +import os, dotenv + +# Load envrionment variables for ./.env +dotenv.load_dotenv() # Create Flask App app = Flask(__name__) @@ -8,8 +12,6 @@ # Create API Interface api = Api(app) - - class Item(Resource): def get(self, queue: str, number: int) -> str: """Returns the JSON representation of the item requested. @@ -80,12 +82,9 @@ def get(self) -> list: list: Dictionaries with the number of items in each queue. """ return ECNQueue.getQueueCounts() - -api.add_resource(QueueList, "/api/get_queues") -api.add_resource(Item, "/api//") -api.add_resource(Queue, "/api/") -if __name__ == "__main__": - app.run() +api.add_resource(QueueList, "/api/get_queues") +api.add_resource(Item, "/api//") +api.add_resource(Queue, "/api/") \ No newline at end of file From f6a15d3882afe4bdb0834ef48eaaab223310abd8 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Wed, 4 Nov 2020 20:12:11 -0500 Subject: [PATCH 04/64] Update gitignore to include api dotenv files --- .gitignore | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index c518367..e238c91 100644 --- a/.gitignore +++ b/.gitignore @@ -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 @@ -28,3 +30,4 @@ yarn-error.log* /api/venv __pycache__/ venv-manager.log +/api/.env \ No newline at end of file From f9dd83886be82608933f3527c94496096da17ad1 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Thu, 5 Nov 2020 00:06:51 -0500 Subject: [PATCH 05/64] Add python-dotenv to pip requirements --- api/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/api/requirements.txt b/api/requirements.txt index a7f38ba..890a147 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -13,6 +13,7 @@ mccabe==0.6.1 PyJWT==1.7.1 pylint==2.5.3 python-dateutil==2.8.1 +python-dotenv==0.15.0 pytz==2020.1 six==1.15.0 toml==0.10.1 From 982ce7b1c0fa3cb16ae15c7fab4d2bedf306b29b Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Thu, 5 Nov 2020 00:07:52 -0500 Subject: [PATCH 06/64] Replace pyjwt with Flask-JWT-Extended --- api/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/requirements.txt b/api/requirements.txt index 890a147..1aaa908 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -3,6 +3,7 @@ astroid==2.4.2 click==7.1.2 Flask==1.1.2 Flask-RESTful==0.3.8 +Flask-JWT-Extended==3.24.1 gunicorn==20.0.4 isort==4.3.21 itsdangerous==1.1.0 @@ -10,7 +11,6 @@ Jinja2==2.11.2 lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 -PyJWT==1.7.1 pylint==2.5.3 python-dateutil==2.8.1 python-dotenv==0.15.0 From 937dbc00f8de7942bd4d63ba8a16856d72f48d0f Mon Sep 17 00:00:00 2001 From: Tyler Jordan Wright Date: Fri, 6 Nov 2020 15:16:57 -0500 Subject: [PATCH 07/64] Added logic that removes selected row styling after ItemView is closed. --- src/App.js | 2 +- src/components/ItemTable/ItemTable.js | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/App.js b/src/App.js index 8320c96..a7cfa5a 100644 --- a/src/App.js +++ b/src/App.js @@ -73,7 +73,7 @@ function App() { - console.log("Clicked!") }/> + diff --git a/src/components/ItemTable/ItemTable.js b/src/components/ItemTable/ItemTable.js index cbc1bf2..b7b85d5 100644 --- a/src/components/ItemTable/ItemTable.js +++ b/src/components/ItemTable/ItemTable.js @@ -7,9 +7,9 @@ import RelativeTime from "react-relative-time"; import ItemTableFilter from "../ItemTableFilter/" import { ArrowDownward, ArrowUpward } from "@material-ui/icons"; -export default function ItemTable({ data }) { +export default function ItemTable({ data, rowCanBeSelected }) { - const theme = useTheme(); + const theme = useTheme(); const useStyles = makeStyles({ // Fully visible for active icons activeSortIcon: { @@ -27,12 +27,11 @@ export default function ItemTable({ data }) { backgroundColor: theme.palette.type === 'light' ? theme.palette.grey[50] : theme.palette.grey[700], }, }, - columnBorders: { borderLeftWidth: "1px", borderLeftStyle: "solid", borderColor: theme.palette.type === "light" ? theme.palette.grey[300] : theme.palette.grey[500] - } + }, }); const classes = useStyles(); @@ -135,10 +134,13 @@ export default function ItemTable({ data }) { // This functionality should be achieved by using the selected prop and // overriding the selected class but this applied the secondary color at 0.08% opacity. // Overridding the root class is a workaround. - classes={{ root: isSelected ? classes.rowSelected : classes.bandedRows }} + classes={{ root: (isSelected && rowCanBeSelected) ? classes.rowSelected : classes.bandedRows }} {...row.getRowProps()} > {row.cells.map(cell => ( - + {cell.render("Cell")} ))} @@ -153,9 +155,12 @@ export default function ItemTable({ data }) { ItemTable.propTypes = { /** Array of items from all active queues to display in table. */ - "items": PropTypes.array + "items": PropTypes.array, + /** State variable indicating if rows can be selected. When false, all rows are deselected. */ + "rowCanBeSelected": PropTypes.bool }; ItemTable.defaultProps = { - "items": [] + "items": [], + "rowCanBeSelected": true }; \ No newline at end of file From 4e8fae4bca9d923308194b39c0b7f9975aa906c4 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Sun, 8 Nov 2020 16:23:00 -0500 Subject: [PATCH 08/64] Implement basic shared username/password auth using dotenv --- api/api.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/api/api.py b/api/api.py index dbadc9a..a9752f8 100644 --- a/api/api.py +++ b/api/api.py @@ -1,7 +1,8 @@ -from flask import Flask +from flask import Flask, request from flask_restful import Api, Resource -import ECNQueue +from werkzeug.security import check_password_hash import os, dotenv +import ECNQueue # Load envrionment variables for ./.env dotenv.load_dotenv() @@ -12,6 +13,27 @@ # Create API Interface api = Api(app) + + +class Login(Resource): + def post(self): + 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) + + return ({ "message": "Login successful"}, 200) + class Item(Resource): def get(self, queue: str, number: int) -> str: """Returns the JSON representation of the item requested. @@ -85,6 +107,10 @@ def get(self) -> list: -api.add_resource(QueueList, "/api/get_queues") +api.add_resource(Login, "/login") api.add_resource(Item, "/api//") -api.add_resource(Queue, "/api/") \ No newline at end of file +api.add_resource(Queue, "/api/") +api.add_resource(QueueList, "/api/get_queues") + +if __name__ == "__main__": + app.run() \ No newline at end of file From dcf3c3d7b795b3b4bc5a4d725a4b95930d53faf7 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Sun, 8 Nov 2020 16:36:41 -0500 Subject: [PATCH 09/64] Implement login, token generation and route protection with JWTs --- api/api.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/api/api.py b/api/api.py index a9752f8..3d2da84 100644 --- a/api/api.py +++ b/api/api.py @@ -1,5 +1,9 @@ -from flask import Flask, request +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, set_refresh_cookies +) from werkzeug.security import check_password_hash import os, dotenv import ECNQueue @@ -14,6 +18,30 @@ 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'] = '/token/refresh' + +tokenManager = JWTManager(app) + + class Login(Resource): def post(self): @@ -32,9 +60,22 @@ def post(self): if not check_password_hash(os.environ.get("SHARED_PASSWORD_HASH"), data["password"]): return ({ "message": "Password invalid"}, 401) - return ({ "message": "Login successful"}, 200) + 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 { "token": access_token } class Item(Resource): + @jwt_required def get(self, queue: str, number: int) -> str: """Returns the JSON representation of the item requested. @@ -67,6 +108,7 @@ def get(self, queue: str, number: int) -> str: return ECNQueue.Item(queue, number).toJson() class Queue(Resource): + @jwt_required def get(self, queue: str) -> str: """Returns the JSON representation of the queue requested. @@ -85,6 +127,7 @@ def get(self, queue: str) -> str: return queues class QueueList(Resource): + @jwt_required def get(self) -> list: """Returns a list of dictionaries with the number of items in each queue. From 271caababf41ca961bf318a38fd01cca50009c43 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Sun, 8 Nov 2020 17:40:27 -0500 Subject: [PATCH 10/64] Implement /tokens/refresh endpoint to get new access tokens --- api/api.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/api/api.py b/api/api.py index 3d2da84..12829a1 100644 --- a/api/api.py +++ b/api/api.py @@ -2,7 +2,8 @@ from flask_restful import Api, Resource from flask_jwt_extended import ( JWTManager, create_access_token, create_refresh_token, - jwt_required, set_refresh_cookies + 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 @@ -37,7 +38,7 @@ # 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'] = '/token/refresh' +app.config['JWT_REFRESH_COOKIE_PATH'] = '/tokens/refresh' tokenManager = JWTManager(app) @@ -63,7 +64,6 @@ def post(self): 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 @@ -72,7 +72,14 @@ def _does_this_work(response): set_refresh_cookies(response, refresh_token) return response - return { "token": access_token } + 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_toekn": access_token}, 200) class Item(Resource): @jwt_required @@ -151,6 +158,7 @@ def get(self) -> list: api.add_resource(Login, "/login") +api.add_resource(RefreshAccessToken, "/tokens/refresh") api.add_resource(Item, "/api//") api.add_resource(Queue, "/api/") api.add_resource(QueueList, "/api/get_queues") From ba3c9b7cdd2bef8f459596c7f282669fc4236f12 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Tue, 10 Nov 2020 15:22:57 -0500 Subject: [PATCH 11/64] Update Login resource docs --- api/api.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/api/api.py b/api/api.py index 12829a1..b29b4f6 100644 --- a/api/api.py +++ b/api/api.py @@ -45,7 +45,24 @@ class Login(Resource): - def post(self): + 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) From b75f3d7275cd9207efe8885f3daaf61caf288843 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Tue, 10 Nov 2020 15:57:37 -0500 Subject: [PATCH 12/64] Make Item resource return tuple w/ quueue JSON and HTTP return code plus update docs --- api/api.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api/api.py b/api/api.py index b29b4f6..8c6a657 100644 --- a/api/api.py +++ b/api/api.py @@ -52,6 +52,7 @@ def post(self) -> tuple: 200 (OK): On success. 401 (Unauthroized): When username or password are incorrect. 422 (Unprocessable Entitiy): When the username or password can't be parsed. + 500 (Internal Server Error): On API error. Example: curl -X POST @@ -100,9 +101,13 @@ def post(self): class Item(Resource): @jwt_required - def get(self, queue: str, number: int) -> str: + def get(self, queue: str, number: int) -> tuple: """Returns the JSON representation of the item requested. + Return Codes: + 200 (OK): On success. + 500 (Internal Server Error): On API error. + Example: /api/ce/100 returns: { @@ -127,9 +132,9 @@ 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: Response containing queue as JSON and HTTP response code. """ - return ECNQueue.Item(queue, number).toJson() + return (ECNQueue.Item(queue, number).toJson(), 200) class Queue(Resource): @jwt_required From 5c5a1002ee92ba8bb5aa5514ce97c0de4b9d4e19 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Tue, 10 Nov 2020 16:02:31 -0500 Subject: [PATCH 13/64] Update Queue resource to return a tuple of data and HTTP response code plus update docs --- api/api.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/api/api.py b/api/api.py index 8c6a657..e651b87 100644 --- a/api/api.py +++ b/api/api.py @@ -132,28 +132,32 @@ def get(self, queue: str, number: int) -> tuple: item (int): The number of the item requested. Returns: - tuple: Response containing queue as JSON and HTTP response code. + tuple: Item as JSON and HTTP response code. """ return (ECNQueue.Item(queue, number).toJson(), 200) class Queue(Resource): @jwt_required - def get(self, queue: str) -> str: + def get(self, queues: str) -> tuple: """Returns the JSON representation of the queue requested. + Return Codes: + 200 (OK): On success. + 500 (Internal Server Error): On API error. + 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 (queues, 200) class QueueList(Resource): @jwt_required From ffc049b761f46daa3dd88835736e7acdb81e5802 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Tue, 10 Nov 2020 16:05:09 -0500 Subject: [PATCH 14/64] Correct refactoring error that broke Queue resource --- api/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api.py b/api/api.py index e651b87..7c85b5c 100644 --- a/api/api.py +++ b/api/api.py @@ -157,7 +157,7 @@ def get(self, queues: str) -> tuple: for queue in queues_requested: queue_list.append(ECNQueue.Queue(queue).toJson()) - return (queues, 200) + return (queue_list, 200) class QueueList(Resource): @jwt_required @@ -186,7 +186,7 @@ def get(self) -> list: api.add_resource(Login, "/login") api.add_resource(RefreshAccessToken, "/tokens/refresh") api.add_resource(Item, "/api//") -api.add_resource(Queue, "/api/") +api.add_resource(Queue, "/api/") api.add_resource(QueueList, "/api/get_queues") if __name__ == "__main__": From 5b752deed8607a83ec7d7ef1f7cc7a662d8f4fac Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Tue, 10 Nov 2020 19:01:13 -0500 Subject: [PATCH 15/64] Update QueueList docs --- api/api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/api.py b/api/api.py index 7c85b5c..f4b71e2 100644 --- a/api/api.py +++ b/api/api.py @@ -52,7 +52,6 @@ def post(self) -> tuple: 200 (OK): On success. 401 (Unauthroized): When username or password are incorrect. 422 (Unprocessable Entitiy): When the username or password can't be parsed. - 500 (Internal Server Error): On API error. Example: curl -X POST @@ -106,7 +105,6 @@ def get(self, queue: str, number: int) -> tuple: Return Codes: 200 (OK): On success. - 500 (Internal Server Error): On API error. Example: /api/ce/100 returns: @@ -143,7 +141,6 @@ def get(self, queues: str) -> tuple: Return Codes: 200 (OK): On success. - 500 (Internal Server Error): On API error. Args: queues (str): Plus (+) deliminited list of queues. @@ -161,9 +158,12 @@ def get(self, queues: str) -> tuple: class QueueList(Resource): @jwt_required - def get(self) -> list: + def get(self) -> tuple: """Returns a list of dictionaries with the number of items in each queue. + Return Codes: + 200 (OK): On success. + Example: [ { @@ -177,9 +177,9 @@ 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() + return (ECNQueue.getQueueCounts(), 200) From 4757b28a9bbde04b2e0d749b1e638ddeefe17fb4 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Tue, 10 Nov 2020 19:10:40 -0500 Subject: [PATCH 16/64] Add JWT_REFRESH_CRSF_HEADER for easier reference --- api/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/api.py b/api/api.py index f4b71e2..e1ffb89 100644 --- a/api/api.py +++ b/api/api.py @@ -38,7 +38,10 @@ # 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' +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) From 4e40b028fe4101d957eb3d0897fd576677fb50e9 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:09:53 -0500 Subject: [PATCH 17/64] Create base LoginForm -- nonfunctional --- src/components/LoginForm/LoginForm.js | 100 ++++++++++++++++++++++++++ src/components/LoginForm/LoginForm.md | 11 +++ src/components/LoginForm/index.js | 1 + 3 files changed, 112 insertions(+) create mode 100644 src/components/LoginForm/LoginForm.js create mode 100644 src/components/LoginForm/LoginForm.md create mode 100644 src/components/LoginForm/index.js diff --git a/src/components/LoginForm/LoginForm.js b/src/components/LoginForm/LoginForm.js new file mode 100644 index 0000000..ee4e902 --- /dev/null +++ b/src/components/LoginForm/LoginForm.js @@ -0,0 +1,100 @@ +import React, { useState } from "react"; +import { Box, Paper, TextField, Button, Avatar, Typography, useTheme, makeStyles } from "@material-ui/core"; +import { Redirect } from "react-router-dom"; +import { useLogin } from "../AuthProvider/"; + +export default function LoginForm() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const handleUsernameChange = (event) => setUsername(event.target.value); + const handlePasswordChange = (event) => setPassword(event.target.value); + + const handleSubmit = (event) => { + event.preventDefault(); + return true; + } + + const theme = useTheme(); + const useStyles = makeStyles({ + "box_root": { + background: `linear-gradient(120deg, ${theme.palette.secondary.main}30 0%, ${theme.palette.primary.main}10 100%)`, + width: "100%", + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center" + }, + "avatar_root": { + padding: theme.spacing(1), + width: theme.spacing(10), + height: theme.spacing(10) + }, + "paper_root": { + minWidth: theme.breakpoints.values.sm/2, + padding: theme.spacing(3), + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + }, + "button_root": { + marginTop: theme.spacing(2) + } + }) + const classes = useStyles(); + + const isLoggedIn = useLogin(); + if (isLoggedIn) { + return + } + + return ( + +
+ + + + Sign In + + + + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/LoginForm/LoginForm.md b/src/components/LoginForm/LoginForm.md new file mode 100644 index 0000000..7e0553c --- /dev/null +++ b/src/components/LoginForm/LoginForm.md @@ -0,0 +1,11 @@ +The LoginForm acts as the only public facing page for the webqueue2. If any part of the app is access without access tokens, the user will be redirected here. It takes a username and password, attempts to login an, if successful, sets access tokens and redirects users to webqueue2. + +--- +```jsx +import LoginForm from "./LoginForm"; + + +``` +```jsx static + +``` \ No newline at end of file diff --git a/src/components/LoginForm/index.js b/src/components/LoginForm/index.js new file mode 100644 index 0000000..3789b92 --- /dev/null +++ b/src/components/LoginForm/index.js @@ -0,0 +1 @@ +export { default } from "./LoginForm"; \ No newline at end of file From ca130f81ababbd4cde281a768d81d2b9a82e1dc3 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:10:45 -0500 Subject: [PATCH 18/64] Create base auth utilities -- only login --- src/auth/index.js | 1 + src/auth/utilities.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/auth/index.js create mode 100644 src/auth/utilities.js diff --git a/src/auth/index.js b/src/auth/index.js new file mode 100644 index 0000000..dad0b46 --- /dev/null +++ b/src/auth/index.js @@ -0,0 +1 @@ +export { login } from "./utilities"; \ No newline at end of file diff --git a/src/auth/utilities.js b/src/auth/utilities.js new file mode 100644 index 0000000..1237078 --- /dev/null +++ b/src/auth/utilities.js @@ -0,0 +1,33 @@ +/** Utility Functions for webqueue2 API */ + + + +/** + * Returns an access token to be used for authorization. + * @example + * login("janeDoe", "superSecretPassword") + * @param {String} username + * @param {String} password + * @returns {Boolean | String} An access token on success, `false` otherwise. + */ +export async function login(username, password){ + const loginInit = { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ "username": username, "password": password}) + }; + + let loginResponse = await fetch("/login", loginInit); + let data = await loginResponse.json(); + + if (data === null){ + return false; + } + if (!loginResponse.ok){ + console.error(`Login failed. Got code ${loginResponse.status} (${loginResponse.statusText})`); + return false; + } + + return data.access_token || false; +} + From 50f03ecd09789691d882a86e3ac59316d8f86c54 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:11:13 -0500 Subject: [PATCH 19/64] Create AuthProvider component --- src/components/AuthProvider/AuthProvider.js | 32 +++++++++++++++++++++ src/components/AuthProvider/AuthProvider.md | 14 +++++++++ src/components/AuthProvider/index.js | 1 + 3 files changed, 47 insertions(+) create mode 100644 src/components/AuthProvider/AuthProvider.js create mode 100644 src/components/AuthProvider/AuthProvider.md create mode 100644 src/components/AuthProvider/index.js diff --git a/src/components/AuthProvider/AuthProvider.js b/src/components/AuthProvider/AuthProvider.js new file mode 100644 index 0000000..c63b9e2 --- /dev/null +++ b/src/components/AuthProvider/AuthProvider.js @@ -0,0 +1,32 @@ +import React, { useState, createContext, useContext } from "react"; + + + +const LoginContext = createContext(); +const LoginSetterContext = createContext(); +const TokenContext = createContext(); +const TokenSetterContext = createContext(); + +export const useLogin = () => useContext(LoginContext); +export const useLoginSetter = () => useContext(LoginSetterContext); +export const useToken = () => useContext(TokenContext); +export const useTokenSetter = () => useContext(TokenSetterContext); + + + +export default function AuthProvider({ children }) { + const [loggedIn, setLoggedIn] = useState(false); + const [token, setToken] = useState(null); + + return ( + + + + + {children} + + + + + ); +}; \ No newline at end of file diff --git a/src/components/AuthProvider/AuthProvider.md b/src/components/AuthProvider/AuthProvider.md new file mode 100644 index 0000000..aad5bee --- /dev/null +++ b/src/components/AuthProvider/AuthProvider.md @@ -0,0 +1,14 @@ +AuthProvider + +Description + +--- + +```jsx +import AuthProvider from "./AuthProvider"; + + +``` +```jsx static + +``` \ No newline at end of file diff --git a/src/components/AuthProvider/index.js b/src/components/AuthProvider/index.js new file mode 100644 index 0000000..165e516 --- /dev/null +++ b/src/components/AuthProvider/index.js @@ -0,0 +1 @@ +export { default, useLogin, useLoginSetter, useToken, useTokenSetter } from "./AuthProvider"; \ No newline at end of file From 4e79718e1ea1779adbac9871fadfa839c74cc58d Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:21:51 -0500 Subject: [PATCH 20/64] Create PrivateRoute component for checking auth --- src/components/PrivateRoute/PrivateRoute.js | 24 +++++++++++++++++++++ src/components/PrivateRoute/PrivateRoute.md | 1 + src/components/PrivateRoute/index.js | 1 + 3 files changed, 26 insertions(+) create mode 100644 src/components/PrivateRoute/PrivateRoute.js create mode 100644 src/components/PrivateRoute/PrivateRoute.md create mode 100644 src/components/PrivateRoute/index.js diff --git a/src/components/PrivateRoute/PrivateRoute.js b/src/components/PrivateRoute/PrivateRoute.js new file mode 100644 index 0000000..8507e82 --- /dev/null +++ b/src/components/PrivateRoute/PrivateRoute.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from "prop-types"; +import { Route, Redirect } from 'react-router-dom'; +import { useLogin } from "../AuthProvider/"; + +export default function PrivateRoute({ children, ...rest }) { + const isLoggedIn = useLogin(); + console.log("isLoggedIn", isLoggedIn); + + return ( + + { + isLoggedIn + ? children + : + } + + ); +}; + +PrivateRoute.propTypes = { + /** The route's path. */ + "path": PropTypes.string.isRequired +}; \ No newline at end of file diff --git a/src/components/PrivateRoute/PrivateRoute.md b/src/components/PrivateRoute/PrivateRoute.md new file mode 100644 index 0000000..7f0d5c0 --- /dev/null +++ b/src/components/PrivateRoute/PrivateRoute.md @@ -0,0 +1 @@ +The PrivateRoute wraps [React Router](https://reactrouter.com/)'s [Route component](https://reactrouter.com/web/api/Route) and checks for authentication using [AuthProvider](#/Components/AuthProvider). If authentication is valid, the children of the PrivateRoute are rendered. Otherwise, the user is redirected to the login page. \ No newline at end of file diff --git a/src/components/PrivateRoute/index.js b/src/components/PrivateRoute/index.js new file mode 100644 index 0000000..4c9765d --- /dev/null +++ b/src/components/PrivateRoute/index.js @@ -0,0 +1 @@ +export { default } from "./PrivateRoute"; \ No newline at end of file From 58e6b965ccefbf160359d48eba9259299e7c3c4c Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:24:22 -0500 Subject: [PATCH 21/64] Move previous App component to AppView --- src/components/AppView/AppView.js | 133 ++++++++++++++++++++++++++++++ src/components/AppView/AppView.md | 10 +++ src/components/AppView/index.js | 1 + 3 files changed, 144 insertions(+) create mode 100644 src/components/AppView/AppView.js create mode 100644 src/components/AppView/AppView.md create mode 100644 src/components/AppView/index.js diff --git a/src/components/AppView/AppView.js b/src/components/AppView/AppView.js new file mode 100644 index 0000000..acfb4a2 --- /dev/null +++ b/src/components/AppView/AppView.js @@ -0,0 +1,133 @@ +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { Box, makeStyles, Paper, useTheme } from "@material-ui/core"; +import { Route } from "react-router-dom"; +import clsx from "clsx"; +import ItemTableAppBar from "../ItemTableAppBar/"; +import ItemTable from "../ItemTable/"; +import ItemViewAppBar from "../ItemViewAppBar/"; +import ItemView from "../ItemView/"; +import QueueSelector from "../QueueSelector/"; + +export default function AppView({ setDarkMode }){ + const [activeItem, setActiveItem] = useState({}); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [queues, setQueues] = useState([]); + const [items, setItems] = useState([]); + const [selectedQueues, setSelectedQueues] = useState([]); + const [queueCounts, setQueueCounts] = useState([]); + + useEffect( _ => { + async function getQueues(){ + if (selectedQueues.length > 0){ + let queuesToLoad = ""; + + for (let selectedQueue of selectedQueues){ + queuesToLoad += `+${selectedQueue.name}`; + } + + const apiResponse = await fetch(`/api/${queuesToLoad}`); + const queueJson = await apiResponse.json(); + setQueues(queueJson); + } else { + setQueues([]) + } + } + getQueues(); + }, [selectedQueues]); + + useEffect( _ => { + let tempItems = []; + for (let queue of queues){ + tempItems = tempItems.concat(queue.items); + } + setItems(tempItems); + }, [queues]); + + useEffect( _ => { + async function getQueueCounts(){ + const apiResponse = await fetch(`/api/get_queues`); + const queueCountJson = await apiResponse.json(); + setQueueCounts(queueCountJson); + }; + getQueueCounts(); + return _ => setQueueCounts([]); + }, [selectedQueues]); + + const theme = useTheme(); + const transitionWidth = theme.transitions.create(["width"], { + duration: theme.transitions.duration.enteringScreen, + easing: theme.transitions.easing.easeInOut + }); + const useStyles = makeStyles({ + "leftCol": { + overflow: "auto", + width: "100vw", + height: "100vh", + transition: transitionWidth, + }, + "rightCol": { + overflow: "auto", + width: "0", + height: "100vh", + transition: transitionWidth, + scrollbarWidth: 0, + }, + "rightColShift": { + overflowY: "auto", + width: "100vw", + flexShrink: "0", + transition: transitionWidth + }, + [theme.breakpoints.up("md")]: { + "rightColShift": { + width: "40vw", + } + }, + }); + const classes = useStyles(); + + return( + + + + + + console.log("Clicked!") }/> + + + + {items.length === 0 ? null : + { + const item = items.find((item) => { + return item.queue === match.params.queue && item.number === Number(match.params.number); + }); + + if (item === undefined) { + return ( + + ); + } + + setActiveItem(item); + + return ( + <> + + + + ); + } + } + /> + } + + + ); +}; + +AppView.propTypes = {}; + +AppView.defaultProps = {}; \ No newline at end of file diff --git a/src/components/AppView/AppView.md b/src/components/AppView/AppView.md new file mode 100644 index 0000000..9d4107e --- /dev/null +++ b/src/components/AppView/AppView.md @@ -0,0 +1,10 @@ +The primary view for webqueue2. + +--- +```jsx +import AppView from "./AppView"; + +``` +```jsx static + +``` \ No newline at end of file diff --git a/src/components/AppView/index.js b/src/components/AppView/index.js new file mode 100644 index 0000000..08cc8fb --- /dev/null +++ b/src/components/AppView/index.js @@ -0,0 +1 @@ +export { default } from "./AppView"; \ No newline at end of file From d20a3b4b0b7e6aaa13f4041fcbfa1f99c705f526 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:24:48 -0500 Subject: [PATCH 22/64] Update app entrypoint for auth routing --- src/App.js | 136 ++++++----------------------------------------------- 1 file changed, 14 insertions(+), 122 deletions(-) diff --git a/src/App.js b/src/App.js index 6c08ecf..c410707 100644 --- a/src/App.js +++ b/src/App.js @@ -1,134 +1,26 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { ThemeProvider } from "@material-ui/core/styles"; -import { Box, makeStyles, Paper } from "@material-ui/core"; -import { Route } from "react-router-dom"; -import clsx from "clsx"; import webqueueTheme from "./theme"; -import ItemTableAppBar from "./components/ItemTableAppBar/"; -import ItemTable from "./components/ItemTable/"; -import ItemViewAppBar from "./components/ItemViewAppBar/"; -import ItemView from "./components/ItemView/"; -import QueueSelector from "./components/QueueSelector/"; +import { Switch, Route } from "react-router-dom"; +import PrivateRoute from "./components/PrivateRoute/"; +import AppView from "./components/AppView/"; +import LoginForm from "./components/LoginForm/"; function App() { const [darkMode, setDarkMode] = useState(false); - const [activeItem, setActiveItem] = useState({}); - const [sidebarOpen, setSidebarOpen] = useState(false); - const [queues, setQueues] = useState([]); - const [items, setItems] = useState([]); - const [selectedQueues, setSelectedQueues] = useState([]); - const [queueCounts, setQueueCounts] = useState([]); - - useEffect( _ => { - async function getQueues(){ - if (selectedQueues.length > 0){ - let queuesToLoad = ""; - - for (let selectedQueue of selectedQueues){ - queuesToLoad += `+${selectedQueue.name}`; - } - - const apiResponse = await fetch(`/api/${queuesToLoad}`); - const queueJson = await apiResponse.json(); - setQueues(queueJson); - } else { - setQueues([]) - } - } - getQueues(); - }, [selectedQueues]); - - useEffect( _ => { - let tempItems = []; - for (let queue of queues){ - tempItems = tempItems.concat(queue.items); - } - setItems(tempItems); - }, [queues]); - - useEffect( _ => { - async function getQueueCounts(){ - const apiResponse = await fetch(`/api/get_queues`); - const queueCountJson = await apiResponse.json(); - setQueueCounts(queueCountJson); - }; - getQueueCounts(); - return _ => setQueueCounts([]); - }, [selectedQueues]); const theme = webqueueTheme(darkMode); - const transitionWidth = theme.transitions.create(["width"], { - duration: theme.transitions.duration.enteringScreen, - easing: theme.transitions.easing.easeInOut - }); - const useStyles = makeStyles({ - "leftCol": { - overflow: "auto", - width: "100vw", - height: "100vh", - transition: transitionWidth, - }, - "rightCol": { - overflow: "auto", - width: "0", - height: "100vh", - transition: transitionWidth, - scrollbarWidth: 0, - }, - "rightColShift": { - overflowY: "auto", - width: "100vw", - flexShrink: "0", - transition: transitionWidth - }, - [theme.breakpoints.up("md")]: { - "rightColShift": { - width: "40vw", - } - }, - }); - const classes = useStyles(); - - return ( + return ( - - - - - - console.log("Clicked!") }/> - - - - {items.length === 0 ? null : - { - const item = items.find((item) => { - return item.queue === match.params.queue && item.number === Number(match.params.number); - }); - - if (item === undefined) { - return ( - - ); - } - - setActiveItem(item); - - return ( - <> - - - - ); - } - } - /> - } - - + + + + + + + + ); } From c3e9d2ceea74e2025f2a35d54597a5a665cd3905 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 15:25:02 -0500 Subject: [PATCH 23/64] Include AuthProvider --- src/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 578b96b..0290d94 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import * as serviceWorker from './serviceWorker'; import { createBrowserHistory } from 'history'; import { CssBaseline } from '@material-ui/core'; import { BrowserRouter as Router } from 'react-router-dom'; +import AuthProvider from "./components/AuthProvider/"; export const history = createBrowserHistory({ basename: process.env.PUBLIC_URL @@ -13,9 +14,11 @@ export const history = createBrowserHistory({ ReactDOM.render( - - - + + + + + , document.getElementById('root') ); From 34f56ae41b51bb683dbe15df05a52558ffbd773e Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 13 Nov 2020 17:35:41 -0500 Subject: [PATCH 24/64] Integrate API login and error handling --- src/components/LoginForm/LoginForm.js | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/LoginForm/LoginForm.js b/src/components/LoginForm/LoginForm.js index ee4e902..daa43ab 100644 --- a/src/components/LoginForm/LoginForm.js +++ b/src/components/LoginForm/LoginForm.js @@ -1,17 +1,31 @@ import React, { useState } from "react"; import { Box, Paper, TextField, Button, Avatar, Typography, useTheme, makeStyles } from "@material-ui/core"; import { Redirect } from "react-router-dom"; -import { useLogin } from "../AuthProvider/"; +import { Alert } from '@material-ui/lab'; +import { useLogin, useLoginSetter, useTokenSetter } from "../AuthProvider/"; +import { login } from "../../auth/"; export default function LoginForm() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [error, setError] = useState(false); const handleUsernameChange = (event) => setUsername(event.target.value); const handlePasswordChange = (event) => setPassword(event.target.value); - const handleSubmit = (event) => { + const setLogin = useLoginSetter(); + const setToken = useTokenSetter(); + const handleSubmit = async (event) => { event.preventDefault(); + let access_token = await login(username, password); + + if (!access_token){ + setError(true); + return false; + } + + setLogin(true); + setToken(access_token); return true; } @@ -30,6 +44,9 @@ export default function LoginForm() { width: theme.spacing(10), height: theme.spacing(10) }, + "alert_root": { + marginTop: theme.spacing(2) + }, "paper_root": { minWidth: theme.breakpoints.values.sm/2, padding: theme.spacing(3), @@ -49,6 +66,14 @@ export default function LoginForm() { return } + const LoginErrorAlert = _ => { + return ( + + Username or password is incorrect. + + ); + } + return (
@@ -60,6 +85,7 @@ export default function LoginForm() { Sign In + { error && } Date: Fri, 13 Nov 2020 19:07:17 -0500 Subject: [PATCH 25/64] Add auth headers to API requests --- src/components/AppView/AppView.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/AppView/AppView.js b/src/components/AppView/AppView.js index acfb4a2..6fb0006 100644 --- a/src/components/AppView/AppView.js +++ b/src/components/AppView/AppView.js @@ -8,6 +8,7 @@ import ItemTable from "../ItemTable/"; import ItemViewAppBar from "../ItemViewAppBar/"; import ItemView from "../ItemView/"; import QueueSelector from "../QueueSelector/"; +import { useToken } from "../AuthProvider/"; export default function AppView({ setDarkMode }){ const [activeItem, setActiveItem] = useState({}); @@ -17,8 +18,14 @@ export default function AppView({ setDarkMode }){ const [selectedQueues, setSelectedQueues] = useState([]); const [queueCounts, setQueueCounts] = useState([]); + const access_token = useToken(); + useEffect( _ => { async function getQueues(){ + if (access_token === null){ + return + } + if (selectedQueues.length > 0){ let queuesToLoad = ""; @@ -26,7 +33,11 @@ export default function AppView({ setDarkMode }){ queuesToLoad += `+${selectedQueue.name}`; } - const apiResponse = await fetch(`/api/${queuesToLoad}`); + let myHeaders = new Headers(); + myHeaders.append("Authorization", `Bearer ${access_token}`); + let requestOptions = { headers: myHeaders }; + + const apiResponse = await fetch(`/api/${queuesToLoad}`, requestOptions); const queueJson = await apiResponse.json(); setQueues(queueJson); } else { @@ -34,7 +45,7 @@ export default function AppView({ setDarkMode }){ } } getQueues(); - }, [selectedQueues]); + }, [selectedQueues, access_token]); useEffect( _ => { let tempItems = []; @@ -46,13 +57,21 @@ export default function AppView({ setDarkMode }){ useEffect( _ => { async function getQueueCounts(){ - const apiResponse = await fetch(`/api/get_queues`); + if (access_token === null){ + return + } + + let myHeaders = new Headers(); + myHeaders.append("Authorization", `Bearer ${access_token}`); + let requestOptions = { headers: myHeaders }; + + const apiResponse = await fetch(`/api/get_queues`, requestOptions); const queueCountJson = await apiResponse.json(); setQueueCounts(queueCountJson); }; getQueueCounts(); return _ => setQueueCounts([]); - }, [selectedQueues]); + }, [selectedQueues, access_token]); const theme = useTheme(); const transitionWidth = theme.transitions.create(["width"], { From 27d51fa5ac9ed94945766d1b8fd0ea63b83a539d Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Sun, 15 Nov 2020 18:26:55 -0500 Subject: [PATCH 26/64] Add view password toggle to login screen --- src/components/LoginForm/LoginForm.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/LoginForm/LoginForm.js b/src/components/LoginForm/LoginForm.js index daa43ab..6cd39ad 100644 --- a/src/components/LoginForm/LoginForm.js +++ b/src/components/LoginForm/LoginForm.js @@ -1,5 +1,7 @@ import React, { useState } from "react"; -import { Box, Paper, TextField, Button, Avatar, Typography, useTheme, makeStyles } from "@material-ui/core"; +import { Box, Paper, TextField, Button, Avatar, Typography, InputAdornment, IconButton, useTheme, makeStyles } from "@material-ui/core"; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; import { Redirect } from "react-router-dom"; import { Alert } from '@material-ui/lab'; import { useLogin, useLoginSetter, useTokenSetter } from "../AuthProvider/"; @@ -9,6 +11,7 @@ export default function LoginForm() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(false); + const [showPassword, setShowPassword] = useState(false); const handleUsernameChange = (event) => setUsername(event.target.value); const handlePasswordChange = (event) => setPassword(event.target.value); @@ -74,6 +77,20 @@ export default function LoginForm() { ); } + const ViewPasswordToggle = _ => { + return ( + + setShowPassword(!showPassword) } + onMouseDown={ (event) => event.preventDefault() } + > + { showPassword ? : } + + + ); + } + return ( @@ -99,7 +116,7 @@ export default function LoginForm() { variant="outlined" /> + }} />