diff --git a/backend/users/views.py b/backend/users/views.py index 1b1ca1c..fae206a 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -114,11 +114,19 @@ def post(self, request): if email is None: return Response({"detail": "email not provided"}, status=400) if get_user_model().objects.filter(email=email).exists(): + + # Check if the email is verified + user = get_user_model().objects.get(email=email) + if not user.is_active: + return Response( + {"detail": "email not verified", "is_verified": "false"}, status=400 + ) + from_mail = settings.EMAIL_HOST_USER to_mail = email send_reset_email(to_mail=to_mail, from_mail=from_mail) - return Response({"detail": "Email sent"}) + return Response({"detail": "Email sent", "is_verified": "true"}) else: return Response({"error": "Email not found"}, status=400) diff --git a/frontend/src/App.js b/frontend/src/App.js index cc76183..4a63dfe 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,89 +1,89 @@ -import './App.css'; -import LandingPage from './pages/LandingPage'; -import Auth from './pages/Auth'; -import EmailVerify from './pages/EmailVerify'; +import "./App.css"; +import LandingPage from "./pages/LandingPage"; +import Auth from "./pages/Auth"; +import EmailVerify from "./pages/EmailVerify"; import { - BrowserRouter as Router, - Route, - Switch, - Redirect, -} from 'react-router-dom'; - -import Dashboard from './pages/Dashboard'; -import { useContext, useEffect, useState } from 'react'; -import AuthContext from './store/auth-context'; -import Modal from './components/Alert'; -import URL from './URL'; + BrowserRouter as Router, + Route, + Switch, + Redirect, +} from "react-router-dom"; +import Dashboard from "./pages/Dashboard"; +import { useContext, useEffect, useState } from "react"; +import AuthContext from "./store/auth-context"; +import Modal from "./components/Alert"; +import ForgotPassword from "./components/ForgotPassword"; +import URL from "./URL"; function App() { - const authCtx = useContext(AuthContext); - const [key, setKey] = useState(); - const [isError, setIsError] = useState(false) + const authCtx = useContext(AuthContext); + const [key, setKey] = useState(); + const [isError, setIsError] = useState(false); - useEffect(()=>{ + useEffect(() => { + var requestOptions = { + method: "GET", + redirect: "follow", + }; - var requestOptions = { - method: 'GET', - redirect: 'follow' - }; - - fetch(`${URL}/`, requestOptions) - .then(response => { - if(response.status!==404) - { - setIsError(true) - } - else - { - setIsError(false) - } - console.log(response); - return response; - }) - .catch(error => { - console.log(!!error) - if(error) - { - setIsError(true) - } - else - { - setIsError(false) - } - }); - },[]) + fetch(`${URL}/`, requestOptions) + .then(response => { + if (response.status !== 404) { + setIsError(true); + } else { + setIsError(false); + } + console.log(response); + return response; + }) + .catch(error => { + console.log(!!error); + if (error) { + setIsError(true); + } else { + setIsError(false); + } + }); + }, []); - return ( - - + return ( + + + + {authCtx.isLoggedIn ? ( + + ) : ( + + )} + - - {authCtx.isLoggedIn ? : - } - + {authCtx.isLoggedIn && ( + + + + )} - {authCtx.isLoggedIn && ( - - - - )} + {!authCtx.isLoggedIn && ( + + + + )} + + + - {!authCtx.isLoggedIn && ( - - - - )} - - - + - - - - - - ); + + + + + + ); } export default App; diff --git a/frontend/src/assets/css/ForgotPassword.css b/frontend/src/assets/css/ForgotPassword.css new file mode 100644 index 0000000..026edee --- /dev/null +++ b/frontend/src/assets/css/ForgotPassword.css @@ -0,0 +1,68 @@ +.password-container { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.password-container h1, .error-message { + font-size: 2.5rem; + margin: 0; + color: var(--textcolor); +} + +.password-container p { + font-size: 1.3rem; +} + +.password-container a, .try-again { + color: rgb(243, 233, 140); + cursor: pointer; + font-weight: 600; +} + +.try-again:hover { + text-decoration: underline; +} + +label, +input { + display: block; + margin: 5px 0; +} + +.password-container label { + color: var(--textcolor); + font-weight: 600; +} + +.password-container input { + background-color: rgba(139, 137, 137, 0.39); + color: #e8f0fe; + border-radius: 5px; + border: none; + padding: 8px 10px; + font-size: 1.2rem; + margin: 10px 0 20px; + width: 90%; + min-width: 200px; +} + +input:focus { + outline: none; +} + +.password-container button { + display: block; + margin: 20px 0px; + padding: 8px 20px; + font-size: 1.2rem; + cursor: pointer; + background-color: rgb(243, 233, 140); + border: none; + border-radius: 2px; + font-weight: 600; +} + +.password-container button:hover { + background-color: #f5e44f; +} diff --git a/frontend/src/components/ForgotPassword.js b/frontend/src/components/ForgotPassword.js new file mode 100644 index 0000000..bbd3de4 --- /dev/null +++ b/frontend/src/components/ForgotPassword.js @@ -0,0 +1,187 @@ +import '../assets/css/ForgotPassword.css'; + +import React, { useState } from 'react'; + +import Alert from './Alert'; +import URL from '../URL'; +import axios from 'axios'; +import { Link, useHistory } from 'react-router-dom'; + +const FORGOT_PW_ENDPOINT = '/api/users/forgot_password/'; +const NEW_PW_ENDPOINT = '/api/users/new_password/'; +const UNVERIFIED_MSG = 'There was an issue verifying your email.' + +const ForgotPassword = () => { + const history = useHistory(); + + // use isEmailVerified to render the appropriate content + // null return emailForm, true return passwordForm, false return an error message on the page + const [isEmailVerified, setIsEmailVerified] = useState(null) + const [loading, setLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(false); + const [successMsg, setSuccessMsg] = useState(false); + const [emailState, setEmailState] = useState({ email: '' }) + const [passwordState, setPasswordState] = useState({ + password1: '', + password2: '', + }); + const { password1, password2 } = passwordState; + + const handleEmailChange = e => { + const { value } = e.target; + setEmailState({ email: value}) + } + + const handlePasswordChange = e => { + const { name, value } = e.target; + setPasswordState({ + ...passwordState, + [name]: value, + }); + }; + + const submitEmail = email => fetch(`${URL}${FORGOT_PW_ENDPOINT}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email }), + }) + + const handleEmailSubmit = async e => { + e.preventDefault() + try { + if (!emailState.email) { + setErrorMsg('Enter a valid email address.'); + return; + } + const response = await submitEmail(emailState.email) + console.log('response:', response) + if (response.status !== 200) { + setIsEmailVerified('unverified') + setErrorMsg(response.statusText) + } + setSuccessMsg('Please check your email to reset for a link to reset your password.') + } catch (err) { + setIsEmailVerified('unverified') + setErrorMsg(err.message) + console.error(err) + } + setEmailState({email: ''}) + } + + const sendNewPassword = newPassword => + axios.post(URL + NEW_PW_ENDPOINT, newPassword); + + const handleNewPasswordSubmit = async e => { + e.preventDefault(); + setLoading(true); + try { + if (password1 === password2) { + const response = await sendNewPassword(password2); + if (response.statusText === 'OK') { + history.push('/'); + } + } + if (password1 !== password2) { + setErrorMsg('The passwords do not match.'); + } + } catch (err) { + setErrorMsg(err.message); + console.error(err); + } + setLoading(false); + setPasswordState({ password1: '', password2: '' }); + }; + + const submitBtn = ( + + ) + + const emailForm = ( + <> +

Enter email

+

Enter your email to reset your password.

+
+ + + {submitBtn} +
+ + ) + + const passwordForm = ( + <> +

Reset password

+

Forgot your password? Reset it here.

+
+ + + + +

+ + Remember your password? + +

+ {submitBtn} +
+ + ) + + const errorMessage = ( + <> +

{UNVERIFIED_MSG}

+

+ window.location.reload()}>Try again? +

+ + ) + + const renderContent = (isEmailVerified) => { + switch (isEmailVerified) { + case true: + return passwordForm; + case 'unverified': + return errorMessage; + default: + return emailForm; + } + } + + return ( + <> +
+ {renderContent(isEmailVerified)} + {errorMsg && ( + +

{errorMsg}

+ Tap to dismiss. +
+ )} + {successMsg && ( + +

{successMsg}

+ Tap to dismiss. +
+ )} +
+ + ); +}; + +export default ForgotPassword; diff --git a/frontend/src/components/Login.js b/frontend/src/components/Login.js index 136086a..5c25f76 100644 --- a/frontend/src/components/Login.js +++ b/frontend/src/components/Login.js @@ -8,293 +8,304 @@ import { useHistory } from "react-router"; import AuthContext from "../store/auth-context"; const Login = () => { - const authCtx = useContext(AuthContext); - const history = useHistory(); - const mq = window.matchMedia("(mix-width: 480px)"); + const authCtx = useContext(AuthContext); + const history = useHistory(); + const mq = window.matchMedia("(mix-width: 480px)"); - const [coverClass, setCoverClass] = useState("cover cover-register"); + const [coverClass, setCoverClass] = useState("cover cover-register"); - const forgetPasswordHandler = () => { - swal({ - text: "Take a deep breath and try to remember your password.", - confirmButtonColor: "#f5e44f", - confirmButtonText: "Yes, delete it!", - }); - }; + const forgetPasswordHandler = () => { + swal({ + text: "Take a deep breath and try to remember your password.\nDo you still need to reset your password?", + confirmButtonColor: "#f5e44f", + confirmButtonText: "Yes, delete it!", + buttons: ["Nope!", "Yes!"], + }) + .then((value) => { + value ? history.push('/forgot-password') : swal.close() + }) + }; - const [loading, setLoading] = useState(false); - const [created, setCreated] = useState(false); - const [error, setError] = useState({ - status: false, - body: "", - }); + const [loading, setLoading] = useState(false); + const [created, setCreated] = useState(false); + const [error, setError] = useState({ + status: false, + body: "", + }); - const [email, setEmail] = useState(""); - const emailChangeHandler = (event) => { - setEmail(event.target.value); - }; + const [email, setEmail] = useState(""); + const emailChangeHandler = event => { + setEmail(event.target.value); + }; - const [pass, setPass] = useState(""); - const passChangeHandler = (event) => { - setPass(event.target.value); - }; + const [pass, setPass] = useState(""); + const passChangeHandler = event => { + setPass(event.target.value); + }; - const [pass2, setPass2] = useState(""); - const pass2ChangeHandler = (event) => { - setPass2(event.target.value); - }; + const [pass2, setPass2] = useState(""); + const pass2ChangeHandler = event => { + setPass2(event.target.value); + }; - const [name, setName] = useState(""); - const nameChangeHandler = (event) => { - setName(event.target.value); - }; + const [name, setName] = useState(""); + const nameChangeHandler = event => { + setName(event.target.value); + }; - const [warning, setWarning] = useState({ - status: false, - body: "", - }); + const [warning, setWarning] = useState({ + status: false, + body: "", + }); - //Register handler - const registerHandler = (e) => { - e.preventDefault(); - setCreated(false); + //Register handler + const registerHandler = e => { + e.preventDefault(); + setCreated(false); - if (pass == pass2) { - setLoading(true); - setError({ - status: false, - body: "", - }); + if (pass == pass2) { + setLoading(true); + setError({ + status: false, + body: "", + }); - var myHeaders = new Headers(); - myHeaders.append("Content-Type", "application/json"); + var myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); - let registerDetails = JSON.stringify({ - email: email, - password: pass, - name: name, - }); + let registerDetails = JSON.stringify({ + email: email, + password: pass, + name: name, + }); - var requestOptions = { - method: "POST", - headers: myHeaders, - body: registerDetails, - redirect: "follow", - }; + var requestOptions = { + method: "POST", + headers: myHeaders, + body: registerDetails, + redirect: "follow", + }; - fetch(`${URL}/api/users/register/`, requestOptions) - .then((response) => { - const data = response.json(); - setLoading(false); + fetch(`${URL}/api/users/register/`, requestOptions) + .then(response => { + const data = response.json(); + setLoading(false); - if (response.status == 200) setCreated(true); - else setCreated(false); + if (response.status == 200) setCreated(true); + else setCreated(false); - return data; - }) - .then((result) => { - console.log(result); + return data; + }) + .then(result => { + console.log(result); - let firstkey = Object.keys(result)[0]; + let firstkey = Object.keys(result)[0]; - setError({ - status: true, - body: result[firstkey], - }); - }) - .catch((error) => console.log("error", error)); - } else { - setError({ - status: true, - body: "The passwords dont match!", - }); - } - }; + setError({ + status: true, + body: result[firstkey], + }); + }) + .catch(error => console.log("error", error)); + } else { + setError({ + status: true, + body: "The passwords dont match!", + }); + } + }; - // Login handler - const loginHandler = (e) => { - e.preventDefault(); - setLoading(true); + // Login handler + const loginHandler = e => { + e.preventDefault(); + setLoading(true); - var myHeaders = new Headers(); - myHeaders.append("Content-Type", "application/json"); + var myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); - let authDetails = JSON.stringify({ - email: email, - password: pass, - }); + let authDetails = JSON.stringify({ + email: email, + password: pass, + }); - var requestOptions = { - method: "POST", - headers: myHeaders, - body: authDetails, - redirect: "follow", - }; + var requestOptions = { + method: "POST", + headers: myHeaders, + body: authDetails, + redirect: "follow", + }; - fetch(`${URL}/api/auth/login/`, requestOptions) - .then((response) => { - setLoading(false) - if (response.ok) { - return response.json(); - } - return Promise.reject(response); - }) - .then((result) => { - authCtx.login(result.key); + fetch(`${URL}/api/auth/login/`, requestOptions) + .then(response => { + setLoading(false); + if (response.ok) { + return response.json(); + } + return Promise.reject(response); + }) + .then(result => { + authCtx.login(result.key); - if (result.key) history.replace("/dashboard"); + if (result.key) history.replace("/dashboard"); - let firstkey = Object.keys(result)[0]; + let firstkey = Object.keys(result)[0]; - setWarning({ - status: true, - body: result[firstkey], - }); + setWarning({ + status: true, + body: result[firstkey], + }); - if (result.key) - setWarning({ - status: true, - body: "Loggin successful", - }); - }) - .catch((error) => { - error.json().then((result)=>{ - let firstkey = Object.keys(result)[0]; + if (result.key) + setWarning({ + status: true, + body: "Loggin successful", + }); + }) + .catch(error => { + error.json().then(result => { + let firstkey = Object.keys(result)[0]; - setWarning({ - status: true, - body: result[firstkey], - }); - }); - }) - }; - var width = Math.max(window.screen.width); + setWarning({ + status: true, + body: result[firstkey], + }); + }); + }); + }; + var width = Math.max(window.screen.width); - return ( -
- -
-

Welcome back

-

- New here?{" "} - { - setCoverClass("cover"); - setWarning({ status: false, body: "" }); - setError({ status: false, body: "" }); - }} - > - Create an account now! - -

-
- - - - - - Forgot password? - - {warning.status ? ( - {warning.body} - ) : null} - -
-
-
-
-

Register Now

-

- Existing User?{" "} - { - setCoverClass("cover cover-register"); - setWarning({ status: false, body: "" }); - setError({ status: false, body: "" }); - }} - > - Click here to login! - -

-
- - - - - - - - - {created ? ( - Account created! Please check your email and verify in order to login. - ) : error.status ? ( - {error.body} - ) : null} - {loading ? ( -
-
-
-
-
-
- ) : null} - -
-
-
- ); + return ( +
+ +
+

Welcome back

+

+ New here?{" "} + { + setCoverClass("cover"); + setWarning({ status: false, body: "" }); + setError({ status: false, body: "" }); + }} + > + Create an account now! + +

+
+ + + + + + Forgot password? + + {warning.status ? ( + + {warning.body} + + ) : null} + +
+
+
+ {" "} +
+
+

Register Now

+

+ Existing User?{" "} + { + setCoverClass("cover cover-register"); + setWarning({ status: false, body: "" }); + setError({ status: false, body: "" }); + }} + > + Click here to login! + +

+
+ + + + + + + + + {created ? ( + + Account created! Please check your email and verify + in order to login. + + ) : error.status ? ( + {error.body} + ) : null} + {loading ? ( +
+
+
+
+
+
+ ) : null} + +
+
+
+ ); }; export default Login;