diff --git a/backend/src/app.py b/backend/src/app.py index 37c5598..90feb34 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,13 +1,51 @@ -import time -from flask import Flask +import os +import time +import flask import flask_sqlalchemy import flask_praetorian import flask_cors -import os +db = flask_sqlalchemy.SQLAlchemy() +guard = flask_praetorian.Praetorian() +cors = flask_cors.CORS() -app = Flask(__name__) +# A generic user model that might be used by an app powered by flask-praetorian +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.Text, unique=True) + password = db.Column(db.Text) + roles = db.Column(db.Text) + is_active = db.Column(db.Boolean, default=True, server_default='true') + + @property + def rolenames(self): + try: + return self.roles.split(',') + except Exception: + return [] + + @classmethod + def lookup(cls, username): + return cls.query.filter_by(username=username).one_or_none() + + @classmethod + def identify(cls, id): + return cls.query.get(id) + + @property + def identity(self): + return self.id + + def is_valid(self): + return self.is_active + + +# Initialize flask app for the example +app = flask.Flask(__name__) +app.config['SECRET_KEY'] = 'top secret' +app.config['JWT_ACCESS_LIFESPAN'] = {'hours': 24} +app.config['JWT_REFRESH_LIFESPAN'] = {'days': 30} # Read environment variables if "DEBUG" in os.environ and os.environ["DEBUG"] == 'yes': debug = True @@ -22,26 +60,93 @@ if "PORT" in os.environ: else: port = 5000 +# Initialize the flask-praetorian instance for the app +guard.init_app(app, User) -db = flask_sqlalchemy.SQLAlchemy() -guard = flask_praetorian.Praetorian() -cors = flask_cors.CORS() +# Initialize a local database for the example +app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'database.db')}" +db.init_app(app) -class User(db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.Text, unique=True) - password = db.Column(db.Text) +# Initializes CORS so that the api_tool can talk to the example app +cors.init_app(app) + +# Add users for the example +with app.app_context(): + db.create_all() + if db.session.query(User).filter_by(username='Yasoob').count() < 1: + db.session.add(User( + username='Yasoob', + password=guard.hash_password('strongpassword'), + roles='admin' + )) + db.session.commit() - -@app.route('/') +# Set up some routes for the example +@app.route('/api/') def home(): - return "Hello World" + return {"Hello": "World"}, 200 + +@app.route('/api/login', methods=['POST']) +def login(): + """ + Logs a user in by parsing a POST request containing user credentials and + issuing a JWT token. + .. example:: + $ curl http://localhost:5000/api/login -X POST \ + -d '{"username":"Yasoob","password":"strongpassword"}' + """ + req = flask.request.get_json(force=True) + username = req.get('username', None) + password = req.get('password', None) + user = guard.authenticate(username, password) + ret = {'access_token': guard.encode_jwt_token(user)} + return ret, 200 + + +@app.route('/api/refresh', methods=['POST']) +def refresh(): + """ + Refreshes an existing JWT by creating a new one that is a copy of the old + except that it has a refrehsed access expiration. + .. example:: + $ curl http://localhost:5000/api/refresh -X GET \ + -H "Authorization: Bearer " + """ + print("refresh request") + old_token = request.get_data() + new_token = guard.refresh_jwt_token(old_token) + ret = {'access_token': new_token} + return ret, 200 + + +@app.route('/api/protected') +@flask_praetorian.auth_required +def protected(): + """ + A protected endpoint. The auth_required decorator will require a header + containing a valid JWT + .. example:: + $ curl http://localhost:5000/api/protected -X GET \ + -H "Authorization: Bearer " + """ + return {'message': f'protected endpoint (allowed user {flask_praetorian.current_user().username})'} + @app.route('/time') def get_current_time(): return {'time': time.time()} +# Run the example +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) +import time +from flask import Flask +import flask_sqlalchemy +import flask_praetorian +import flask_cors + + if __name__ == '__main__': app.run(debug=debug, host=host, port=port) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c166b5b..a161163 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5197,11 +5197,18 @@ } }, "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "requires": { - "domelementtype": "1" + "domelementtype": "^2.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } } }, "domutils": { @@ -7330,22 +7337,40 @@ } }, "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" }, "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } } } }, @@ -12887,9 +12912,9 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -14415,6 +14440,11 @@ "workbox-webpack-plugin": "5.1.4" } }, + "react-token-auth": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/react-token-auth/-/react-token-auth-1.1.8.tgz", + "integrity": "sha512-stS7VbLu57qTC4H2R0Cd7nxuhXeQ2oR3oanXf+sX46BTq6N3ecKfmgTDnfJ0ce+QNX4tWUSbgqMzEPDbkmp7KA==" + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -14628,15 +14658,15 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "renderkid": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", - "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", "requires": { - "css-select": "^2.0.2", - "dom-converter": "^0.2", - "htmlparser2": "^3.10.1", - "lodash": "^4.17.20", - "strip-ansi": "^3.0.0" + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" }, "dependencies": { "ansi-regex": { @@ -14644,6 +14674,56 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==" + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "requires": { + "boolbase": "^1.0.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b2f204f..5199637 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", + "react-token-auth": "^1.1.8", "web-vitals": "^1.1.1" }, "scripts": { diff --git a/frontend/src/auth/AuthProvider.js b/frontend/src/auth/AuthProvider.js new file mode 100644 index 0000000..103c224 --- /dev/null +++ b/frontend/src/auth/AuthProvider.js @@ -0,0 +1,11 @@ +import { createAuthProvider } from "react-token-auth"; + +export const [useAuth, authFetch, login, logout] = +createAuthProvider({ + accessTokenKey: 'access_token', + onUpdateToken: (token) => fetch('/api/refresh', { + method: 'POST', + body: token.access_token + }) + .then(r => r.json()) +}) \ No newline at end of file diff --git a/frontend/src/components/InputField.js b/frontend/src/components/InputField.js index f71a029..14fcab3 100644 --- a/frontend/src/components/InputField.js +++ b/frontend/src/components/InputField.js @@ -5,7 +5,7 @@ function InputField(props) { return ( ); } diff --git a/frontend/src/components/SubmitField.js b/frontend/src/components/SubmitField.js index ec5e168..a709cd5 100644 --- a/frontend/src/components/SubmitField.js +++ b/frontend/src/components/SubmitField.js @@ -6,7 +6,7 @@ function SubmitField(props) { const InputValue = props.LabelName; return ( ); } diff --git a/frontend/src/components/pages/Home.js b/frontend/src/components/pages/Home.js index 968475b..1cd108b 100644 --- a/frontend/src/components/pages/Home.js +++ b/frontend/src/components/pages/Home.js @@ -2,6 +2,7 @@ import React from "react"; import "../../App.css"; import HeroSection from "../HeroSection"; import Footer from "../../Footer"; +import { useEffect } from "react/cjs/react.development"; function Home() { return ( diff --git a/frontend/src/components/pages/Login.js b/frontend/src/components/pages/Login.js index b3fe806..5f85009 100644 --- a/frontend/src/components/pages/Login.js +++ b/frontend/src/components/pages/Login.js @@ -1,28 +1,72 @@ import React from "react"; +import { useState, useEffect } from "react/cjs/react.development"; import "../../App.css"; import Footer from "../../Footer"; import InputField from "../InputField"; import SubmitField from "../SubmitField"; +import { login, useAuth, logout } from "../../auth/AuthProvider"; export default function Login() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const onSubmitClick = (e) => { + e.preventDefault(); + console.log("You pressed login"); + let opts = { + username: username, + password: password, + }; + console.log(opts); + fetch("/api/login", { + method: "post", + body: JSON.stringify(opts), + }) + .then((r) => r.json()) + .then((token) => { + if (token.access_token) { + login(token); + console.log(token); + } else { + console.log("Please type in the correct username / password"); + } + }); + }; + + const handleUsernameChange = (e) => { + setUsername(e.target.value); + }; + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + }; + + const [logged] = useAuth(); + return ( <>

Login

-
- - -
- - + {!logged ? ( +
+ + +
+ + + ) : ( + + )}