diff --git a/Auth/AuthAPI.py b/Auth/AuthAPI.py index b903496..64151b2 100644 --- a/Auth/AuthAPI.py +++ b/Auth/AuthAPI.py @@ -1,7 +1,76 @@ # Score API here -from flask import Blueprint -import sys +from flask import Blueprint, request, current_app, json, jsonify +import sys, jwt +from creds import * from db import db sys.path.append("../") auth_api = Blueprint("auth", __name__) + +credsArray = [] +secretkey = current_app.config['SECRET_KEY'] + +@auth_api.route('/register', methods=["POST"]) +def Register(): + newCred = request.get_json() + # check if username is provided + try: + username = newCred["username"] + except KeyError: + return jsonify({"status": "fail", "message": "No username."}) + + # check if password is provided + try: + passwordHash = newCred["passwordHash"] + except KeyError: + return jsonify({"status": "fail", "message": "No password."}) + + credsArray.append(newCred) + + return jsonify({"status": "success", "message": "Credentials successfully registered."}), 200 + + +@auth_api.route('/login', methods=["POST"]) +def Login(): + def checkCred(givenCred): + # check if username is provided + try: + username = givenCred["username"] + except KeyError: + return jsonify({"status": "fail", "message": "No username."}) + + # check if password is provided + try: + passwordHash = givenCred["passwordHash"] + except KeyError: + return jsonify({"status": "fail", "message": "No password."}) + for i in credsArray: + if (username == i["username"] and passwordHash == i["passwordHash"]): + token = jwt.encode({"userID": username, "passwordHash": passwordHash}, secretkey, algorithm="HS256") + return jsonify({"status": "success", "token": token}), 200 + else: + return jsonify({"status": "fail", "message": "Authentication failed."}), 401 + + givenCred = request.get_json() + token = request.arg.get("token") + + + def checkToken(): + if (token != None): + try: + data = jwt.decode(token, secretkey, algorithm=["HS256"]) + return checkCred(data) + except: + data = givenCred + try: + return checkToken(data) + except: + return jsonify({"status": "fail", "message": "Bad token"}) + + # no token provided in URL argument (fresh login) + elif (token == None): + return checkCred(givenCred) + + + + diff --git a/Documentation.md b/Documentation.md new file mode 100644 index 0000000..50ebe21 --- /dev/null +++ b/Documentation.md @@ -0,0 +1,265 @@ +# Simulated ExamScore API Documentation + +## Profile API + +1. **GET** /profiles/ + + - **Description:** To retrieve the information (name and score) of a profile. + + - **Function used:** getProfile() + + - **Source:** /Profiles/ProfilesAPI.py (Line 9) + + - **Parameter(s):** + + - ```id``` (required, must be an integer), the ID of the desired profile. + + - **Request body:** None, only parameter on endpoint needs to be provided. + + - **Response(s):** + + - Success (200) + + ```python + { + "status": "success", + "profile": + {"name": "Nobel", + "scores": [1, 2, 3, 4, 5] + } + } + ``` + + - Fail + + ```python + { + "status": "fail", + "message": "Profile not found." + } + ``` + +2. **POST** /profiles/ + + - **Description:** To add a new profile into the database. + + - **Function used:** addProfile() + + - **Source:** /Profiles/ProfilesAPI.py (Line 19) + + - **Parameter(s):** None + + - **Request body:** + + ```python + { + "name": "string" + } + ``` + + - **Response(s):** + + - Success (200) + + ```python + { + "status": "success", + "message": "New profile added." + } + ``` + + - Fail + + ```python + { + "status": "fail", + "message": "Profile not found." + } + ``` + +3. **DELETE** /profiles/ + + - **Description:** To remove an existing profile from the database. + + - **Function used:** deleteProfile() + + - **Source:** /Profiles/ProfilesAPI.py (Line 27) + + - **Parameter(s):** + + - ```id``` (required, must be an integer), the ID of the desired profile. + + - **Request body:** None, only parameter on endpoint needs to be provided. + + - **Response(s):** + + - Success (200) + + ```python + { + "status": "success", + "message": "Profile deleted." + } + ``` + + - Fail + + ```python + { + "status": "fail", + "message": "Profile not found." + } + ``` + +4. **GET** /profiles//score + + - **Description:** To retrieve the scores of a profile which are greater or equal to the minimum score provided by the requester. + + - **Function used:** getMinScore() + + - **Source:** /Profiles/ProfilesAPI.py (Line 37) + + - **Parameter(s):** + + - ```id``` (required, must be an integer), the ID of the desired profile. + - ```minScore``` (optional, must be an integer), the minimum score, given as an argument. + + - **Request body:** None, only parameter on endpoint needs to be provided. + + - **Response(s):** + + - Success (200) + + ```python + { + "status": "success", + "scores": [3, 4, 5] + } + ``` + + - Fail + + ```python + { + "status": "fail", + "message": "Profile not found." + } + ``` + +## Authentication API + +1. **POST** /auth/register + + - **Description:** To register a new credential for API access. + + - **Function used:** Register() + + - **Source:** /Auth/AuthAPI.py (Line 13) + + - **Parameter(s):** None + + - **Request body:** + + ```python + { + "username": "string" + "passwordHash": "string" + } + ``` + + - **Response(s):** + + - Success (200) + + ```python + { + "status": "success", + "message": "Credentials successfully registered." + } + ``` + + - Fail (no username provided) + + ```python + { + "status": "fail", + "message": "No username." + } + ``` + + - Fail (no password [hash] provided) + + ```python + { + "status": "fail", + "message": "No password." + } + ``` + +2. **POST** /auth/login + + - **Description:** To authorise a credential holder to access the API. + + - **Function used:** Login() + + - **Source:** /Auth/AuthAPI.py (Line 33) + + - **Parameter(s):** None + + - **Request body:** + + ```python + { + "username": "string" + "passwordHash": "string" + } + ``` + + - **Response(s):** + + - Success (200) + + ```python + { + "status": "success", + "message": "Login successful." + } + ``` + + - Fail (401 Not authenticated) + + ```python + { + "status": "fail", + "message": "Authentication failed." + } + ``` + + - Fail (No username provided) + + ```python + { + "status": "fail", + "message": "No username." + } + ``` + + - Fail (No password provided) + + ```python + { + "status": "fail", + "message": "No password." + } + ``` + + - Fail (bad token) + + ```python + { + "status": "fail", + "message": "Bad token" + } + ``` + +​ diff --git a/Profiles/ProfilesAPI.py b/Profiles/ProfilesAPI.py index 4467047..d7454a7 100644 --- a/Profiles/ProfilesAPI.py +++ b/Profiles/ProfilesAPI.py @@ -1,7 +1,59 @@ # Profile API here -from flask import Blueprint +from flask import Blueprint, json, request, jsonify import sys from db import db sys.path.append("../") profiles_api = Blueprint("profiles", __name__) + +@profiles_api.route('/', methods=["GET"]) +def getProfile(id): + try: + profile = db[id] + except IndexError: + return jsonify({"status": "fail", "message": "Profile not found."}) + + return jsonify({"status": "success", "profile": profile}), 200 + + +@profiles_api.route('/', methods=["POST"]) +def addProfile(): + newProfile = request.get_json() + newProfile["scores"] = [] + db.append(newProfile) + return jsonify({"status": "success", "message": "New profile added."}), 200 + + +@profiles_api.route('/', methods=["DELETE"]) +def deleteProfile(): + try: + profile = db[id] + except IndexError: + return jsonify({"status": "fail", "message": "Profile not found."}) + + db.pop(id) + return jsonify({"message": "success", "message": "Profile deleted."}), 200 + +@profiles_api.route('//score', methods=["GET"]) +def getMinScore(): + try: + profile = db[id] + except IndexError: + return jsonify({"status": "fail", "message": "Profile not found."}) + + minScore = request.args.get("minScore") + ScoreList = profile.get("scores") + if minScore == None: + minScore == 0 + else: + minScore == int(minScore) + + def aboveMinScore(i): + if i >= minScore: + return i + + ScoresAboveMin = list(filter(aboveMinScore, ScoreList)) + + return jsonify({"status": "success", "scores": ScoresAboveMin}), 200 + + diff --git a/README.md b/README.md index 08c35ed..f052347 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,20 @@ # RHDEV-BE-2-flask -Homewwork template for BE training lesson 2: Flask and web servers -Setup a basic API to simulate a website that tracks profiles and scores for exams +A simulated db is provided. Note that the db will not be updated between runs. -A simulated db is provided. Note that the db will not be updated between runs In main: -GET / homepage that returns a welcome message - In profiles API (/profiles prefix) -GET /{id} to retrieve the name and all scores of a profile -POST /profiles to create a new profile (name only) -DELETE /{id} to delete a profile -GET /{id}/score?minScore= to retrieve all scores of a profile, above the min score - In authentication API (/auth prefix) -POST /register stores a username and hashedPassword (given as hashed) -Store it in a local array -Login /login checks if the provided information is valid and return a jwt token + success message - -Give a reasonable return format with appropriate status code and messages. -{“message” : “success/fail”, “data”:””} -Also submit a simplified documentation of your API. You can use the format below. - - - -OPTIONALS: -Add environmental variables into the system (for jwt signing secret) -In the login route, check if jwt token is provided and valid -Assume URL argument has token “?token=sdlkaskdnalsdnsald” -See if username and password field arre present + +GET / - Returns a welcome message at homepage + + In profiles API (/profiles prefix): + +* GET /{id} - retrieves the name and all scores of a profile +* POST /profiles - creates a new profile (name only) +* DELETE /{id} - deletes a profile +* GET /{id}/score?minScore= to retrieve all scores of a profile, above the minimum score + + In authentication API (/auth prefix): + +* POST /register - stores a username and hashedPassword (given as hashed) in a local array +* Login /login - checks if the provided information is valid and return a jwt token + success message if successful, else return a 401 error if failed + diff --git a/main.py b/main.py index fc7fbd4..9774b3f 100644 --- a/main.py +++ b/main.py @@ -2,11 +2,25 @@ from Profiles.ProfilesAPI import profiles_api from flask import Flask from db import db +from creds import * +from gevent.pywsgi import WSGIServer -# Write your flask code here app = Flask(__name__) +app.config['SECRET_KEY'] = AUTH_SECRET_KEY + +@app.route("/", methods=["GET"]) +def homepage(): + return "Welcome to ExamScore API." + app.register_blueprint(profiles_api, url_prefix="/profiles") app.register_blueprint(auth_api, url_prefix="/auth") + +if __name__ == "__main__": + app.run("localhost", port=8080) + + # Production + # http_server = WSGIServer(("0.0.0.0", 8080), app) + # http_server.serve_forever()