From a9970b72366459aa8d77ddaaa9ee349dfb504658 Mon Sep 17 00:00:00 2001 From: Amlan Date: Sun, 28 Feb 2021 18:10:47 +0530 Subject: [PATCH 1/3] Created routes for forget and reset password --- flask-backend/.envsample | 4 ++ flask-backend/api/__init__.py | 10 +++- flask-backend/api/helpers/mail.py | 25 ++++++++++ flask-backend/api/userAuthentication/auth.py | 52 +++++++++++++++++++- flask-backend/requirements.txt | 4 ++ 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 flask-backend/.envsample create mode 100644 flask-backend/api/helpers/mail.py diff --git a/flask-backend/.envsample b/flask-backend/.envsample new file mode 100644 index 00000000..31ea1004 --- /dev/null +++ b/flask-backend/.envsample @@ -0,0 +1,4 @@ +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_SERVER= +MAIL_PORT= \ No newline at end of file diff --git a/flask-backend/api/__init__.py b/flask-backend/api/__init__.py index 9ac0507c..f4c4372f 100644 --- a/flask-backend/api/__init__.py +++ b/flask-backend/api/__init__.py @@ -3,6 +3,8 @@ from flask_login import LoginManager from flask_marshmallow import Marshmallow from flask_cors import CORS, cross_origin +from dotenv import load_dotenv +from flask_jwt_extended import JWTManager db = SQLAlchemy() ma = Marshmallow() @@ -14,16 +16,20 @@ def create_app(): app.config['SECRET_KEY'] = 'thisismysecretkeydonotstealit' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - + + app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! + jwt = JWTManager(app) db.init_app(app) + # Load environment variables + load_dotenv() + login_manager = LoginManager() login_manager.login_view = 'auth.login' login_manager.init_app(app) from .models.models import User - @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) diff --git a/flask-backend/api/helpers/mail.py b/flask-backend/api/helpers/mail.py new file mode 100644 index 00000000..e3bc428c --- /dev/null +++ b/flask-backend/api/helpers/mail.py @@ -0,0 +1,25 @@ +import os +from flask_mail import Message, Mail + +from .. import create_app + +def send_email(recipient, url): + app = create_app() + app.config.update(dict( + DEBUG = True, + MAIL_SERVER = os.getenv('MAIL_SERVER'), + MAIL_PORT = os.getenv('MAIL_PORT'), + MAIL_USE_TLS = True, + MAIL_USE_SSL = False, + MAIL_USERNAME = os.getenv('MAIL_USERNAME'), + MAIL_PASSWORD = os.getenv('MAIL_PASSWORD'), + )) + mail = Mail(app) + msg = Message('Password Reset', sender='test@openmf.com', recipients=[recipient]) + msg.body = f'Please use this url to reset your password: {url}' + try: + mail.send(msg) + except ConnectionRefusedError as err: + print("Mail Server not working") + print(err) + raise ConnectionRefusedError \ No newline at end of file diff --git a/flask-backend/api/userAuthentication/auth.py b/flask-backend/api/userAuthentication/auth.py index f4f45ef3..de40c7b4 100644 --- a/flask-backend/api/userAuthentication/auth.py +++ b/flask-backend/api/userAuthentication/auth.py @@ -1,7 +1,10 @@ +import datetime from flask import Blueprint, render_template, redirect, url_for, request, flash, request from werkzeug.security import generate_password_hash, check_password_hash -from flask_login import login_user, logout_user, login_required +from flask_login import login_user, logout_user, login_required, current_user +from flask_jwt_extended import create_access_token, decode_token from ..models.models import User +from ..helpers.mail import send_email from .. import db auth = Blueprint('auth', __name__) @@ -39,4 +42,49 @@ def login_post(): @login_required def logout(): logout_user() - return 'logged out successfully', 200 \ No newline at end of file + return 'logged out successfully', 200 + +@auth.route('/forgot-password', methods=['POST']) +def forgot_password(): + try: + data = request.get_json() + email = str(data['email']) + except: + return 'Please provide an email', 400 + + user = User.query.filter_by(email=email).first() + + if not user: + return 'User with that email does not exist', 404 + + # Create token from email + expires = datetime.timedelta(hours=24) + token = create_access_token(email, expires_delta=expires) + + url = f'http://localhost:3000/reset-password/{token}' + send_email(email, url) + + return 'Reset link sent to your email', 200 + +@auth.route('/reset-password/', methods=['POST']) +def reset_password(token): + + if not token: + return 'Invalid Url', 400 + + try: + data = request.get_json() + password = str(data['password']) + except: + return 'Please provide a password', 400 + + # Get email from token + email = decode_token(token)['sub'] + + user = User.query.filter_by(email=email).first() + if not user: + return 'Invalid token', 401 + + user.password = generate_password_hash(password, method='sha256') + db.session.commit() + return 'Password successfully reset', 200 diff --git a/flask-backend/requirements.txt b/flask-backend/requirements.txt index ae143a4e..ada030b8 100644 --- a/flask-backend/requirements.txt +++ b/flask-backend/requirements.txt @@ -2,7 +2,9 @@ astroid==2.4.2 click==7.1.2 Flask==1.1.1 Flask-Cors==3.0.10 +Flask-JWT-Extended==4.0.2 Flask-Login==0.5.0 +Flask-Mail==0.9.0 flask-marshmallow==0.13.0 Flask-SQLAlchemy==2.4.4 isort==4.3.21 @@ -14,7 +16,9 @@ marshmallow==3.7.1 marshmallow-sqlalchemy==0.23.1 mccabe==0.6.1 pdfkit==0.6.1 +PyJWT==2.0.1 pylint==2.5.3 +python-dotenv==0.15.0 six==1.15.0 SQLAlchemy==1.3.18 toml==0.10.1 From 563fea866c57dffd5f2ef675a0dbf0eeac0820e7 Mon Sep 17 00:00:00 2001 From: Amlan Date: Sun, 28 Feb 2021 20:07:40 +0530 Subject: [PATCH 2/3] Implemented forgt password feature in React App --- React-frontend/src/App.js | 16 +++- React-frontend/src/components/LoginForm.js | 60 +++++++-------- React-frontend/src/components/core/Layout.js | 14 ++-- .../src/pages/ForgotPasswordPage.js | 59 +++++++++++++++ React-frontend/src/pages/ResetPasswordPage.js | 75 +++++++++++++++++++ React-frontend/src/utils/resetPassword.js | 33 ++++++++ 6 files changed, 214 insertions(+), 43 deletions(-) create mode 100644 React-frontend/src/pages/ForgotPasswordPage.js create mode 100644 React-frontend/src/pages/ResetPasswordPage.js create mode 100644 React-frontend/src/utils/resetPassword.js diff --git a/React-frontend/src/App.js b/React-frontend/src/App.js index c058b90d..c005208d 100644 --- a/React-frontend/src/App.js +++ b/React-frontend/src/App.js @@ -5,21 +5,29 @@ import { Route, Switch } from 'react-router-dom'; import LoginPage from './pages/LoginPage'; import HomePage from './pages/HomePage'; import AboutPage from './pages/AboutPage'; -import ContactPage from './pages/ContactPage' -import Alert from './components/core/Alert' +import ContactPage from './pages/ContactPage'; +import ForgotPasswordPage from './pages/ForgotPasswordPage'; +import ResetPasswordPage from './pages/ResetPasswordPage'; +import Alert from './components/core/Alert'; import store from './store/store'; function App() { return ( -
+
- + + +
diff --git a/React-frontend/src/components/LoginForm.js b/React-frontend/src/components/LoginForm.js index 49482454..59032f69 100644 --- a/React-frontend/src/components/LoginForm.js +++ b/React-frontend/src/components/LoginForm.js @@ -4,6 +4,7 @@ import { MDBContainer, MDBInput, MDBBtn, MDBCard, MDBCardBody } from 'mdbreact'; import { login } from '../store/actions/auth'; import formReducer from '../utils/formReducer'; +import { Link } from 'react-router-dom'; const FormPage = () => { const dispatch = useDispatch(); @@ -14,17 +15,17 @@ const FormPage = () => { }; const { isLoading } = useSelector(state => state.auth); const [formData, setFormData] = useReducer(formReducer, initialFormData); - const [passwordShown, setPasswordShown] = useState(false) + const [passwordShown, setPasswordShown] = useState(false); // password toggle handler const togglePasswordVisibilty = () => { - setPasswordShown(passwordShown ? false : true) - } + setPasswordShown(passwordShown ? false : true); + }; const loginHandler = async e => { e.preventDefault(); - e.target.className += " was-validated" - if (formData.email && formData.password){ + e.target.className += ' was-validated'; + if (formData.email && formData.password) { dispatch(login(formData)); } }; @@ -34,11 +35,7 @@ const FormPage = () => {
-
+

Sign In

{isLoading @@ -54,11 +51,10 @@ const FormPage = () => { value={formData.email} type='email' error='wrong' - required + required success='right' name='email' - onChange={event => setFormData(event.target)} - > + onChange={event => setFormData(event.target)}>

Please provide a valid email.
@@ -71,27 +67,26 @@ const FormPage = () => { group required value={formData.password} - type={passwordShown ? 'text': 'password'} + type={passwordShown ? 'text' : 'password'} name='password' - onChange={event => setFormData(event.target)} - > -
+ onChange={event => setFormData(event.target)}> +
Please provide a password.
-
Looks good!
+
Looks good!
- - -
+ + +
{

- Forgot - - Password? - + + Forgot Password? +

{ +const Layout = ({ children, sidebarBool = true, background = true }) => { return ( -
+
- - {sidebarBool && } - {children} + + {sidebarBool && } + + {children} +
diff --git a/React-frontend/src/pages/ForgotPasswordPage.js b/React-frontend/src/pages/ForgotPasswordPage.js new file mode 100644 index 00000000..90774d69 --- /dev/null +++ b/React-frontend/src/pages/ForgotPasswordPage.js @@ -0,0 +1,59 @@ +import { MDBBtn, MDBInput } from 'mdbreact'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import Layout from '../components/core/Layout'; +import { setAlert } from '../store/actions/alerts'; +import { sendResetPasswordMail } from '../utils/resetPassword'; + +const ForgotPasswordPage = () => { + const dispatch = useDispatch(); + const [email, setEmail] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const sendResetMailHandler = () => { + if (!email) { + dispatch(setAlert('Please enter an email')); + return; + } + setIsLoading(true); + sendResetPasswordMail(email) + .then(res => { + dispatch(setAlert('Reset link sent. Check your email', 'success')); + setIsLoading(false); + }) + .catch(err => { + console.log(err); + dispatch(setAlert(err.message, 'danger')); + setIsLoading(false); + }); + }; + + return ( + +
+

Forgot Password

+
+ setEmail(event.target.value)} + /> + + Send Reset Password Mail + + + Go to Home + +
+
+
+ ); +}; + +export default ForgotPasswordPage; diff --git a/React-frontend/src/pages/ResetPasswordPage.js b/React-frontend/src/pages/ResetPasswordPage.js new file mode 100644 index 00000000..3eb582f7 --- /dev/null +++ b/React-frontend/src/pages/ResetPasswordPage.js @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import { MDBBtn, MDBInput } from 'mdbreact'; +import { Link, useParams } from 'react-router-dom'; + +import Layout from '../components/core/Layout'; +import { changePassword } from '../utils/resetPassword'; +import { useDispatch } from 'react-redux'; +import { setAlert } from '../store/actions/alerts'; + +const ResetPasswordPage = () => { + const params = useParams(); + const dispatch = useDispatch(); + const [password, setPassword] = useState(''); + const [password2, setPassword2] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const resetPasswordHandler = () => { + const token = params.token; + if (!password || !password2) { + dispatch(setAlert('Please enter password', 'danger')); + return; + } + if (password !== password2) { + dispatch(setAlert('Passwords do not match', 'danger')); + return; + } + setIsLoading(true); + changePassword(token, password) + .then(res => { + dispatch(setAlert('Password reset. Please log in', 'success')); + setIsLoading(false); + }) + .catch(err => { + console.log(err); + dispatch(setAlert(err.message, 'danger')); + setIsLoading(false); + }); + }; + + return ( + +
+

Reset Password

+
+ setPassword(event.target.value)} + /> + setPassword2(event.target.value)} + /> + + Reset Password + + + Go to Home + +
+
+
+ ); +}; + +export default ResetPasswordPage; diff --git a/React-frontend/src/utils/resetPassword.js b/React-frontend/src/utils/resetPassword.js new file mode 100644 index 00000000..661bb51c --- /dev/null +++ b/React-frontend/src/utils/resetPassword.js @@ -0,0 +1,33 @@ +import axios from '../axios'; + +export const sendResetPasswordMail = async email => { + try { + await axios.post( + '/forgot-password', + { email }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } catch (error) { + throw Error(error.response.data); + } +}; + +export const changePassword = async (token, password) => { + try { + await axios.post( + `/reset-password/${token}`, + { password }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + } catch (error) { + throw Error(error.response.data); + } +}; From 284261063f37d778160c6b2723164c72e797b9c4 Mon Sep 17 00:00:00 2001 From: Amlan Kumar Nandy <45410599+amlannandy@users.noreply.github.com> Date: Sun, 28 Feb 2021 20:13:22 +0530 Subject: [PATCH 3/3] Update Readme.md --- flask-backend/Readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask-backend/Readme.md b/flask-backend/Readme.md index 96f85826..9e51e9bf 100644 --- a/flask-backend/Readme.md +++ b/flask-backend/Readme.md @@ -38,6 +38,13 @@ To run the server use the following command: Eithr from a terminal window or from postman you can send requests. +**Setting up .env file for Forgot Password Feature** + +To enable the forgot password feature, you need to configure the .env file for sending emails through the Flask server. + +1. Go to flask-backend directory +2. Create a .env file and configure it by following the .envsample file. + API Documentation ----------------- [click here](https://github.com/shivanshu1333/My-GSoC-Proposals/blob/master/GSoC'20-SCoReLab-OpenMF.pdf)