From cb2af2fc9fb333e9ac5785880288f0ddd883d1e2 Mon Sep 17 00:00:00 2001 From: Mr-DareDevil Date: Mon, 28 Dec 2020 22:26:14 +0530 Subject: [PATCH 1/2] Upd: Email Service added --- .gitignore | 3 + README.md | 41 +-- app/__init__.py | 8 +- app/db.py | 84 +++-- app/routes.py | 71 ++++- app/templates/reset-password.html | 495 ++++++++++++++++++++++++++++++ config.py | 9 +- sample.env | 5 +- utils.py | 61 ++++ 9 files changed, 694 insertions(+), 83 deletions(-) create mode 100644 app/templates/reset-password.html create mode 100644 utils.py diff --git a/.gitignore b/.gitignore index a81c8ee..5e6bc6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# IDE specific config files +.vscode/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 1d39835..1621711 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# omg-frames-api +# IWasAt This is the backend of the [OMG Frames](https://github.com/dsc-x/omg-frames) having the register/login routes, and other related ones. Link to the front-end repo: [omg-frames](https://github.com/dsc-x/omg-frames) -Backend is running at: [http://104.236.25.178/api/v1](http://104.236.25.178/api/v1) +Backend is running at: [https://api.iwasat.events/api/v1](https://api.iwasat.events/api/v1/) -All the API endpoints are prefixed by `/api/v1` e.g `http://104.236.25.178/api/v1/login` +All the API endpoints are prefixed by `/api/v1` e.g `https://api.iwasat.events/api/v1/login` ## Technologies used @@ -18,7 +18,7 @@ All the API endpoints are prefixed by `/api/v1` e.g `http://104.236.25.178/api/v ## API Endpoints -For API documentation go to [http://104.236.25.178/apidocs/](http://104.236.25.178/apidocs/). +For API documentation go to [https://api.iwasat.events/apidocs/](https://api.iwasat.events/apidocs/). ## Development @@ -51,21 +51,22 @@ This will start the local server in port 5000. ## Project structure ``` - omg-frames-api - . - ├── app - │   ├── db.py - │   ├── __init__.py - │   └── routes.py - ├── config.py - ├── docs - │   ├── getframes.yml - │   ├── login.yml - │   ├── postframes.yml - │   └── register.yml - ├── README.md - ├── requirements.txt - ├── sample.env - └── server.py +omg-frames-api +├── app +│   ├── db.py +│   ├── __init__.py +│   └── routes.py +├── config.py +├── docs +│   ├── deleteframes.yml +│   ├── getframes.yml +│   ├── login.yml +│   ├── postframes.yml +│   ├── register.yml +│   └── updateframes.yml +├── README.md +├── requirements.txt +├── sample.env +└── server.py ``` diff --git a/app/__init__.py b/app/__init__.py index b31fe93..316d7fd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,6 +3,7 @@ from app.db import Db from flasgger import Swagger from flask_cors import CORS +from flask_mail import Mail template = { "swagger": "2.0", @@ -19,8 +20,8 @@ "http", "https" ], - 'securityDefinitions': { - 'basicAuth': { 'type': 'apiKey', 'name': 'Authorization', 'in': 'header'} + 'securityDefinitions': { + 'basicAuth': {'type': 'apiKey', 'name': 'Authorization', 'in': 'header'} } } @@ -28,9 +29,8 @@ app = Flask(__name__) CORS(app) app.config.from_object(Config) +mail = Mail(app) swagger = Swagger(app, template=template) from app import routes - - diff --git a/app/db.py b/app/db.py index 9e27d32..da1130f 100644 --- a/app/db.py +++ b/app/db.py @@ -1,36 +1,7 @@ -from config import Config, FirebaseConfig +from config import FirebaseConfig from passlib.hash import pbkdf2_sha256 +from utils import Utils import pyrebase -import jwt -import datetime - - -def encode_auth_token(user_id): - try: - payload = { - 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1, minutes=0), - 'iat': datetime.datetime.utcnow(), - 'id': user_id - } - return jwt.encode( - payload, - Config.SECRET_KEY, - algorithm='HS256' - ) - except Exception as e: - return e - - -def decode_auth_token(auth_token): - try: - payload = jwt.decode(auth_token, Config.SECRET_KEY) - return payload['id'] - except jwt.ExpiredSignatureError: - print('ERROR: Signature expired. Please log in again.') - return None - except jwt.InvalidTokenError: - print('ERROR: Invalid token. Please log in again.') - return None class Db: @@ -59,7 +30,7 @@ def add_participants(db, userDetails): print(' * Participant added to db') return True except Exception as e: - print('ERROR: ' , e) + print('ERROR: ', e) return False @staticmethod @@ -70,7 +41,7 @@ def check_valid_details(db, userDetails): assert len(userDetails["password"]) > 6 participants = db.child('participants').get().val() - if participants == None: + if participants is None: # no participant data return True for user_id in participants: @@ -99,7 +70,7 @@ def authorise_participants(db, userDetails): @staticmethod def get_token(db, user_id): try: - token = encode_auth_token(user_id) + token = Utils.encode_auth_token(user_id) return token.decode('UTF-8') except Exception as e: print('ERROR:', e) @@ -108,10 +79,10 @@ def get_token(db, user_id): @staticmethod def save_frame(db, token, frame): try: - user_id = decode_auth_token(token) - if user_id!= None: + user_id = Utils.decode_auth_token(token) + if user_id is not None: frame_id = db.child('participants').child(user_id).child('frames').push(frame) - frame_obj={ + frame_obj = { "frame_data": frame, "frame_id": frame_id["name"] } @@ -127,12 +98,12 @@ def save_frame(db, token, frame): @staticmethod def get_frames(db, token): try: - user_id = decode_auth_token(token) - if user_id != None: + user_id = Utils.decode_auth_token(token) + if user_id is not None: frames = db.child('participants').child(user_id).child('frames').get().val() frame_arr = [] - if frames!= None: - frame_arr = [{"frame_id":fid, "frame_data":frames[fid]} for fid in frames] + if frames is not None: + frame_arr = [{"frame_id": fid, "frame_data": frames[fid]} for fid in frames] return frame_arr else: print('ERROR: Token Value is None') @@ -144,8 +115,8 @@ def get_frames(db, token): @staticmethod def delete_frames(db, token, frame_id): try: - user_id = decode_auth_token(token) - if user_id != None: + user_id = Utils.decode_auth_token(token) + if user_id is not None: db.child('participants').child(user_id).child('frames').child(frame_id).remove() return True else: @@ -157,12 +128,31 @@ def delete_frames(db, token, frame_id): @staticmethod def update_frames(db, token, frame_id, frame_data): - user_id = decode_auth_token(token) - if user_id != None: + user_id = Utils.decode_auth_token(token) + if user_id is not None: db.child('participants').child(user_id).child('frames').child(frame_id).remove() upd_frame_id = db.child('participants').child(user_id).child('frames').push(frame_data) - frame = {"frame_id":upd_frame_id['name'], "frame_data":frame_data} + frame = {"frame_id": upd_frame_id['name'], "frame_data": frame_data} return frame else: print('ERROR: Token Value is None') - return None \ No newline at end of file + return None + + @staticmethod + def check_email_address(db, email): + participants = db.child('participants').get().val() + if participants is None: + # No records found + return None + else: + for user_id in participants: + user = participants[user_id] + if user and (user["email"] == email): + return user_id + return None + + @staticmethod + def change_password(db, user_id, password): + user_details = db.child('participants').child(user_id).get().val() + user_details['password'] = pbkdf2_sha256.hash(password) + db.child('participants').child(user_id).update(user_details) diff --git a/app/routes.py b/app/routes.py index 160973c..be3065d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,6 @@ -from flask import request, jsonify, make_response, abort -from app import app, Db +from flask import request, jsonify, make_response +from app import app, Db, mail +from utils import Utils from flasgger.utils import swag_from BASE_URL = '/api/v1' @@ -7,12 +8,62 @@ database = Db.init_db() -@app.route(BASE_URL+'/') +@app.route(BASE_URL + '/') def index(): return make_response(jsonify({"message": "DSC Frames API"})), 201 -@app.route(BASE_URL+'/register', methods=['POST']) +@app.route(BASE_URL + '/send-reset-mail', methods=['POST']) +def send_reset_mail(): + data = request.json + if 'email' not in data.keys(): + responseObject = { + 'message': 'email not specified in the body' + } + return make_response(jsonify(responseObject)), 400 + else: + emailAddr = data['email'] + userId = Db.check_email_address(database, emailAddr) + if userId is not None: + resetLink = f'https://iwasat.events/reset.html?token={Utils.get_reset_token(userId)}' + Utils.send_reset_password_mail(mail, resetLink, emailAddr) + responseObject = { + 'message': 'reset link was sent to the respective email address' + } + return make_response(jsonify(responseObject)), 200 + else: + responseObject = { + 'message': 'email doesnot match' + } + return make_response(jsonify(responseObject)), 401 + + +@app.route(BASE_URL + '/update-password', methods=['POST']) +def update_password(): + data = request.json + if 'token' not in data.keys() and 'password' not in data.keys(): + responseObject = { + 'message': 'email not specified in the body' + } + return make_response(jsonify(responseObject)), 400 + else: + token = data['token'] + password = data['password'] + userId = Utils.verify_reset_token(token) + if userId is None: + responseObject = { + 'message': 'invalid reset token' + } + return make_response(jsonify(responseObject)), 401 + else: + Db.change_password(database, userId, password) + responseObject = { + 'message': 'password updated successfully' + } + return make_response(jsonify(responseObject)), 200 + + +@app.route(BASE_URL + '/register', methods=['POST']) @swag_from('../docs/register.yml') def register(): user = request.json @@ -24,19 +75,19 @@ def register(): return make_response(jsonify({"message": "Internal Server error"})), 500 -@app.route(BASE_URL+'/login', methods=['POST']) +@app.route(BASE_URL + '/login', methods=['POST']) @swag_from('../docs/login.yml') def login(): user = request.json user_data = Db.authorise_participants(database, user) - if (user_data != None and user_data[0] != None): + if (user_data is not None and user_data[0] is not None): user_token = Db.get_token(database, user_data[0]) return make_response(jsonify({"token": user_token, "data": user_data[1]})), 202 else: return make_response(jsonify({"message": "Login failed"})), 401 -@app.route(BASE_URL+'/frames', methods=['POST', 'GET', 'DELETE', 'PUT']) +@app.route(BASE_URL + '/frames', methods=['POST', 'GET', 'DELETE', 'PUT']) @swag_from('../docs/getframes.yml', methods=['GET']) @swag_from('../docs/postframes.yml', methods=['POST']) @swag_from('../docs/deleteframes.yml', methods=['DELETE']) @@ -58,7 +109,7 @@ def frames(): frame = request.json['frame'] if frame: responseObject = Db.save_frame(database, auth_token, frame) - if responseObject != None: + if responseObject is not None: return make_response(jsonify(responseObject)), 201 else: responseObject = { @@ -72,7 +123,7 @@ def frames(): return make_response(jsonify(responseObject)), 400 elif request.method == 'GET': frames_arr = Db.get_frames(database, auth_token) - if frames_arr != None: + if frames_arr is not None: responseObject = { 'frames': frames_arr } @@ -98,7 +149,7 @@ def frames(): frame_id = request.json['frame_id'] frame_data = request.json['frame_data'] upd_frame = Db.update_frames(database, auth_token, frame_id, frame_data) - if upd_frame != None: + if upd_frame is not None: responseObject = { 'message': 'Frame was updated successfully', 'data': upd_frame diff --git a/app/templates/reset-password.html b/app/templates/reset-password.html new file mode 100644 index 0000000..d25c4c0 --- /dev/null +++ b/app/templates/reset-password.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config.py b/config.py index e28ae14..255b0c9 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,13 @@ class Config(object): SECRET_KEY = os.getenv('SECRET_KEY') or os.urandom(24) + MAIL_SERVER = 'smtp.gmail.com' + MAIL_PORT = 465 + MAIL_USERNAME = os.getenv('MAIL_USERNAME') + MAIL_PASSWORD = os.getenv('MAIL_PASWORD') + MAIL_USE_TLS = False + MAIL_USE_SSL = True + SENDER_ADDR = os.getenv('SENDER_ADDR') FirebaseConfig = { @@ -14,4 +21,4 @@ class Config(object): "messagingSenderId": "21222617342", "appId": "1:21222617342:web:f03af782ee33832a39f5af", "measurementId": "G-Y797P25ZN9" -} \ No newline at end of file +} diff --git a/sample.env b/sample.env index c99ed94..88cdb1f 100644 --- a/sample.env +++ b/sample.env @@ -3,4 +3,7 @@ FLASK_RUN_HOST=localhost DEBUG=True # change to False in production FLASK_ENV=development # change to 'production' FIREBASE_API_KEY="your-api-key" # specified in the firebase console -SECRET_KEY="winterofcode2020" # should be random and secret +SECRET_KEY="your-secret-key" # should be random and secret +MAIL_USERNAME='' +MAIL_PASWORD='' +SENDER_ADDR='' \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..9ac2940 --- /dev/null +++ b/utils.py @@ -0,0 +1,61 @@ +import jwt +import datetime +from config import Config +from itsdangerous import TimedJSONWebSignatureSerializer as Serializer +from flask_mail import Message +from flask import render_template + + +class Utils: + @staticmethod + def encode_auth_token(user_id): + try: + payload = { + 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1, minutes=0), + 'iat': datetime.datetime.utcnow(), + 'id': user_id + } + return jwt.encode( + payload, + Config.SECRET_KEY, + algorithm='HS256' + ) + except Exception as e: + return e + + @staticmethod + def decode_auth_token(auth_token): + try: + payload = jwt.decode(auth_token, Config.SECRET_KEY) + return payload['id'] + except jwt.ExpiredSignatureError: + print('ERROR: Signature expired. Please log in again.') + return None + except jwt.InvalidTokenError: + print('ERROR: Invalid token. Please log in again.') + return None + + @staticmethod + def get_reset_token(user_id, expires_sec=1800): + s = Serializer(Config.SECRET_KEY, expires_sec) + return s.dumps({'user_id': user_id}).decode('utf-8') + + @staticmethod + def verify_reset_token(token): + s = Serializer(Config.SECRET_KEY) + try: + user_id = s.loads(token)['user_id'] + except Exception as e: + print('ERROR: ', str(e)) + return None + return user_id + + @staticmethod + def send_reset_password_mail(mail, link, recipient_addr): + msg = Message("Send Mail Tutorial!", + sender=Config.SENDER_ADDR, + recipients=[recipient_addr]) + msg.subject = 'Reset password for IWasAtEvents' + msg.body = 'You or someone else has requested that a new password be generated for your account. If you made this request, then please follow this link:' + link + msg.html = render_template('reset-password.html', link=link) + mail.send(msg) From e07f424c2f3723ad4bc2f599fa05474279ffef7f Mon Sep 17 00:00:00 2001 From: Mr-DareDevil Date: Mon, 28 Dec 2020 23:09:04 +0530 Subject: [PATCH 2/2] Upd: Api docs added for the new routes --- app/routes.py | 4 +++- docs/sendresetmail.yml | 25 +++++++++++++++++++++++++ docs/updatepassword.yml | 29 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 docs/sendresetmail.yml create mode 100644 docs/updatepassword.yml diff --git a/app/routes.py b/app/routes.py index be3065d..886b853 100644 --- a/app/routes.py +++ b/app/routes.py @@ -14,6 +14,7 @@ def index(): @app.route(BASE_URL + '/send-reset-mail', methods=['POST']) +@swag_from('../docs/sendresetmail.yml') def send_reset_mail(): data = request.json if 'email' not in data.keys(): @@ -39,11 +40,12 @@ def send_reset_mail(): @app.route(BASE_URL + '/update-password', methods=['POST']) +@swag_from('../docs/updatepassword.yml') def update_password(): data = request.json if 'token' not in data.keys() and 'password' not in data.keys(): responseObject = { - 'message': 'email not specified in the body' + 'message': 'token or password is absent in the request body' } return make_response(jsonify(responseObject)), 400 else: diff --git a/docs/sendresetmail.yml b/docs/sendresetmail.yml new file mode 100644 index 0000000..6f71324 --- /dev/null +++ b/docs/sendresetmail.yml @@ -0,0 +1,25 @@ +Sending Reset Password Mail to the user +The email provided will be checked in the database, if present then an email with reset URL link will be sent to that email address +--- +tags: + - user +parameters: + - name: body + in: body + required: true + schema: + required: + - email + properties: + email: + type: string + description: Email of the user whose password has to be updated. +responses: + "200": + description: Reset link was sent to the email address + "400": + description: Email is not specified in the request body + "401": + description: Email doesnot exist. Registration required + "500": + description: Internal Server Error \ No newline at end of file diff --git a/docs/updatepassword.yml b/docs/updatepassword.yml new file mode 100644 index 0000000..8a7f041 --- /dev/null +++ b/docs/updatepassword.yml @@ -0,0 +1,29 @@ +Sending Reset Password Mail to the user +The email provided will be checked in the database, if present then an email with reset URL link will be sent to that email address +--- +tags: + - user +parameters: + - name: body + in: body + required: true + schema: + required: + - token + - password + properties: + token: + type: string + description: Reset token to validate the user + password: + type: string + description: New Password of the user +responses: + "200": + description: Password successfully updated + "400": + description: Token or Password is absent in the request body + "401": + description: Reset token invalid or expired + "500": + description: Internal Server Error \ No newline at end of file