From 80be78369c232ca45ccf4b408b1a552ff3331db5 Mon Sep 17 00:00:00 2001 From: proffapt Date: Tue, 25 Jun 2024 01:19:07 +0530 Subject: [PATCH] docs(readme): updated webapps usage --- README.md | 372 ++++++++++++++++++++++++------------------------------ 1 file changed, 167 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 50fb7a9..edfb497 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Key Features: - Smart Token Storage for Efficiency - Supports both CLI & WebApps -> **Note** This package is not officially affiliated with IIT Kharagpur. +> [!Note] +> This package is not officially affiliated with IIT Kharagpur. https://github.com/proffapt/iitkgp-erp-login-pypi/assets/86282911/c0401f6a-80af-46ae-8a8f-ac735f0e67b5 > Guess the number of lines of python code it will take you to achieve this. @@ -31,12 +32,6 @@ https://github.com/proffapt/iitkgp-erp-login-pypi/assets/86282911/c0401f6a-80af- - Output - Usage - Using in WebApps - - Get Session Token - - Get Secret Question - - Get Login Details - - Is OTP Required - - Request OTP - - Sign In - Implementing Login workflow - Example @@ -78,7 +73,8 @@ print(LOGIN_URL) ERP login workflow is implemented in `login(headers, session, ERPCREDS=None, OTP_CHECK_INTERVAL=None, LOGGING=False, SESSION_STORAGE_FILE=None)` function in [erp.py](https://github.com/proffapt/iitkgp-erp-login-pypi/blob/main/src/iitkgp_erp_login/erp.py). -> **Note** This function currently compiles the login workflow "ONLY for the CLI", not for web apps. +> [!Note] +> This function currently compiles the login workflow "ONLY for the CLI", not for web apps.
@@ -113,7 +109,8 @@ The function can also be provided with these _optional_ arguments: | NOT Specified | The user is prompted to enter their credentials manually | | Specified (`ERPCREDS=erpcreds`) | The credentials are retrieved from the `erpcreds.py` file | - > **Note** Here, `credentials` refer to the roll number, password, and security question. + > [!Note] + > Here, `credentials` refer to the roll number, password, and security question.
Prerequisites - ERP credentials file @@ -150,7 +147,8 @@ The function can also be provided with these _optional_ arguments: The token file **MUST** be present in the same directory as the script where `iitkgp_erp_login` module is being imported. 1. Follow the steps in the [Gmail API - Python Quickstart](https://developers.google.com/gmail/api/quickstart/python) guide to obtain `credentials.json` file. - > **Note** The `credentials.json` file is permanent unless you manually delete its reference in your Google Cloud Console. + > [!Note] + > The `credentials.json` file is permanent unless you manually delete its reference in your Google Cloud Console. 2. To generate the `token.json` file, follow the steps below: - Import this module @@ -185,7 +183,8 @@ The function can also be provided with these _optional_ arguments: | NOT Specified | The session tokens will not be stored in a file | | Specified (`SESSION_STORAGE_FILE=".session"`) | The session tokens will be stored in `.session` file for later direct usage | - > **Note** The approximate expiry time for `ssoToken` is _~30 minutes_ and that of `session` object is _~2.5 hours_ + > [!Note] + > The approximate expiry time for `ssoToken` is _~30 minutes_ and that of `session` object is _~2.5 hours_
@@ -255,7 +254,8 @@ sessionToken, ssoToken = erp.login(headers, session, ERPCREDS=erpcreds, OTP_CHEC # Credentials: Automatic - from erpcreds.py | OTP: Automatic - checked every 2 seconds | Logging: Yes | TokenStorage: in .session file ``` -> **Note** These are just examples of how to use the _login_ function, not satisfying the prerequisites. +> [!Note] +> These are just examples of how to use the _login_ function, not satisfying the prerequisites. >> Some arguments of `login()` have their own prerequisites that must be satisfied in order to use them. See "Input" section of login for complete details.
@@ -333,218 +333,180 @@ while True: time.sleep(2) ``` -> **Note** This is merely a Proof of Concept example; this exact functionality has been integrated into the login function itself from version **2.3.1** onwards. +> [!Note] +> This is merely a Proof of Concept example; this exact functionality has been integrated into the login function itself from version **2.3.1** onwards.
## Using in WebApps -To implement the login workflow for `web applications` and `backend systems`, utilize the following modularized steps in the form of functions. +To implement the login workflow for `web applications` and `backend systems`, utilize the [session_manager](./src/iitkgp_erp_login/session_manager.py) module. -
- -### Get Session Token - -Gets session token from homepage response. - - - - - - - - - -
Input - -`session` (__requests.Session__): Session object
-`log` (__bool__, _optional_): Whether to enable logging - -
Output - -(`str`): Session token value - -
- -
- -### Get Secret Question - -Fetches the secret question for a roll number. - - - - - - - - - -
Input - -`headers` (__dict[str, str]__): Request headers
-`session` (__requests.Session__): Session object
-`roll_number` (__str__): Roll number
-`log` (__bool__, _optional_): Whether to enable logging - -
Output - -(`str`): The secret question text - -
- -
- -### Get Login Details - -Creates login details dictionary. - - - - - - - - - -
Input - -`roll_number` (__str__): User roll number -`password` (__str__): User password -`secret_answer` (__str__): Answer to secret question -`session_token` (__str__): Session token - -
Output - -(`LoginDetails`): Dictionary with login credentials - -
- -
- -### Is OTP Required - -Checks if OTP is required for login based on network. - - - - - - - - - -
Input - -`None` - -
Output - -(`bool`): True if OTP required, False otherwise - -
- -
- -### Request OTP - -Requests an OTP to be sent to the user. - - - - - - - - - - - - - - -
Input - -`headers` (__dict[str, str]__): Request headers
-`session` (__requests.Session__): Session object
-`login_details` (__LoginDetails__): Dictionary with credentials
-`log` (__bool__, _optional_): Whether to enable logging - -
Output - -`None` - -
Raises - -`ErpLoginError`: If OTP request fails - -
- -
- -### SignIn - -Signs in into the ERP for the given session. - - - - - - - - - -
Input - -`headers` (__dict[str, str]__): Request headers
-`session` (__requests.Session__): Session object
-`login_details` (__LoginDetails__): Dictionary with credentials
-`log` (__bool__, _optional_): Whether to enable logging - -
Output - -(`str`): ssoToken extracted from the login response - -
- -
+
### Implementing login workflow for webapps Following is a proof of concept example to achieve the login workflow: ```python -import requests -from flask import Flask -import iitkgp_erp_login.erp as erp +import logging +from flask_cors import CORS +from flask import Flask, request, jsonify +from iitkgp_erp_login import session_manager + app = Flask(__name__) -session = requests.Session() +CORS(app) + +jwt_secret_key = "top-secret-unhackable-key" headers = { - 'timeout': '20', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/51.0.2704.79 Chrome/51.0.2704.79 Safari/537.36', + 'timeout': '20', + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/51.0.2704.79 Chrome/51.0.2704.79 Safari/537.36', } -@app.route("/login") -def login(): - roll = # 1. Get roll number from form inputs - passw = # 2. Get password from form inputs - sessionToken = erp.get_sessiontoken(session) # 3 - secret_question = erp.get_secret_question(headers, session, roll) # 4 - # 5. Display the question on the frontend - secret_answer = # 6. Get password from the form input - loginDetails = erp.get_login_details(roll, passw, secret_answer, sessionToken) # 7 - - # 8. Handle OTP - if erp.is_otp_required(): - request_otp(headers=headers, session=session, login_details=login_details, log=False) # 8.1 Request OTP - login_details["email_otp"] = # 8.2 Get otp from form inputs - else: - print("OTP not required :yay") +session_manager = session_manager.SessionManager( + jwt_secret_key=jwt_secret_key, headers=headers) + + +class ErpResponse: + def __init__(self, success: bool, message: str = None, data: dict = None, status_code: int = 200): + self.success = success + self.message = message + self.data = data or {} + self.status_code = status_code - ssoToken = erp.signin(headers, session, loginDetails) # 9 + if not success: + logging.error(f" {message}") - # ... + def to_dict(self): + response = { + "status": "success" if self.success else "error", + "message": self.message + } + if self.data: + response.update(self.data) + return response + + def to_response(self): + return jsonify(self.to_dict()), self.status_code + + +def handle_auth() -> ErpResponse: + if "Authorization" in request.headers: + header = request.headers["Authorization"].split(" ") + if len(header) == 2: + return ErpResponse(True, data={ + "jwt": header[1] + }).to_response() + else: + return ErpResponse(False, "Poorly formatted authorization header. Should be of format 'Bearer '", status_code=401).to_response() + else: + return ErpResponse(False, "Authentication token not provided", status_code=401).to_response() + + +@app.route("/secret-question", methods=["POST"]) +def get_secret_question(): + try: + data = request.form + roll_number = data.get("roll_number") + if not roll_number: + return ErpResponse(False, "Roll Number not provided", status_code=400).to_response() + + secret_question, jwt = session_manager.get_secret_question( + roll_number) + return ErpResponse(True, data={ + "secret_question": secret_question, + "jwt": jwt + }).to_response() + except Exception as e: + return ErpResponse(False, str(e), status_code=500).to_response() + + +@app.route("/request-otp", methods=["POST"]) +def request_otp(): + try: + jwt = None + auth_resp, status_code = handle_auth() + if status_code != 200: + return auth_resp, status_code + else: + jwt = auth_resp.get_json().get("jwt") + + password = request.form.get("password") + secret_answer = request.form.get("secret_answer") + if not all([password, secret_answer]): + return ErpResponse(False, "Missing password or secret answer", status_code=400).to_response() + + session_manager.request_otp(jwt, password, secret_answer) + return ErpResponse(True, message="OTP has been sent to your connected email accounts").to_response() + except Exception as e: + return ErpResponse(False, str(e), status_code=500).to_response() + + +@app.route("/login", methods=["POST"]) +def login(): + try: + jwt = None + auth_resp, status_code = handle_auth() + if status_code != 200: + return auth_resp, status_code + else: + jwt = auth_resp.get_json().get("jwt") + + password = request.form.get("password") + secret_answer = request.form.get("secret_answer") + otp = request.form.get("otp") + if not all([secret_answer, password, otp]): + return ErpResponse(False, "Missing password, secret answer or otp", status_code=400).to_response() + + session_manager.login(jwt, password, secret_answer, otp) + return ErpResponse(True, message="Logged in to ERP").to_response() + except Exception as e: + return ErpResponse(False, str(e), status_code=500).to_response() + + +@app.route("/logout", methods=["GET"]) +def logout(): + try: + jwt = None + auth_resp, status_code = handle_auth() + if status_code != 200: + return auth_resp, status_code + else: + jwt = auth_resp.get_json().get("jwt") + + session_manager.end_session(jwt=jwt) + + return ErpResponse(True, message="Logged out of ERP").to_response() + except Exception as e: + return ErpResponse(False, str(e), status_code=500).to_response() + + +@app.route("/timetable", methods=["POST"]) +def timetable(): + try: + jwt = None + auth_resp, status_code = handle_auth() + if status_code != 200: + return auth_resp, status_code + else: + jwt = auth_resp.get_json().get("jwt") + + _, ssoToken = session_manager.get_erp_session(jwt=jwt) + + ERP_TIMETABLE_URL = "https://erp.iitkgp.ac.in/Acad/student/view_stud_time_table.jsp" + data = { + "ssoToken": ssoToken, + "module_id": '16', + "menu_id": '40', + } + r = session_manager.request( + jwt=jwt, method='POST', url=ERP_TIMETABLE_URL, headers=headers, data=data) + return ErpResponse(True, data={ + "status_code": r.status_code + }).to_response() + except Exception as e: + return ErpResponse(False, str(e), status_code=500).to_response() ```