Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix email verification screen flow #209

Merged
merged 1 commit into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class UserOut(BaseModel):
organization_id: Optional[int]
role: Optional[str]
requires_password_update: bool
email_verified: bool


class UserUpdate(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions backend/routes/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ async def read_users_me(current_user: User = Depends(get_current_user)):
organization_id=current_user.organization_id,
role=current_user.role,
requires_password_update=current_user.requires_password_update,
email_verified=current_user.email_verified,
)
return user_out

Expand Down
7 changes: 3 additions & 4 deletions backend/utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ async def send_verification_email_with_sendgrid(email: List[str], token: str):
html_content=f'Click on the link to verify your email: <a href="https://{APP_HOST}/verify-email?token={token}">https://{APP_HOST}/verify-email</a>',
)
# Disable click tracking in development
# if APP_ENV == "dev":
print("Disabling click tracking")
message.tracking_settings = TrackingSettings()
message.tracking_settings.click_tracking = ClickTracking(False, False)
if APP_ENV == "dev":
message.tracking_settings = TrackingSettings()
message.tracking_settings.click_tracking = ClickTracking(False, False)
try:
sg = SendGridAPIClient(SENDGRID_API_KEY)
response = await sg.send(message)
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/components/auth/RequireAuth.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { Navigate } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";

function RequireAuth({ children }) {
const { isAuthenticated, isLoading, isEmailVerified } = useAuth();
const { isAuthenticated, isLoading, isEmailVerified, loginProcessCompleted } =
useAuth();

// if (isLoading || !loginProcessCompleted) {
if (isLoading) {
return <div>Loading...</div>; // Or your preferred loading indicator/component
}
Expand All @@ -13,8 +15,13 @@ function RequireAuth({ children }) {
return <Navigate to="/login" />;
}

if (!isEmailVerified) {
console.log("from require auth");
if (isAuthenticated && !isEmailVerified) {
// Redirect to the verify-email page if email is not verified
console.log("triggered");
return <Navigate to="/verify-email" />;
}

if (loginProcessCompleted && !isEmailVerified) {
// Redirect to the verify-email page if email is not verified
return <Navigate to="/verify-email" />;
}
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/contexts/AuthContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,19 @@ export const AuthProvider = ({ children }) => {
const [isEmailVerified, setIsEmailVerified] = useState(false); // Add a state for email verification
const [userRole, setUserRole] = useState(null);
const [isLoading, setIsLoading] = useState(true); // Add a loading state
const [loginProcessCompleted, setLoginProcessCompleted] = useState(false);

useEffect(() => {
const verifyToken = async () => {
try {
// Replace '/api/verifyToken' with your actual API endpoint
const response = await axios.get(`${API_URL}verify-token/`);
// Update based on the response message
if (response.data.message === "User is authenticated") {
setIsAuthenticated(true);

const userResponse = await axios.get(`${API_URL}users/me/`);
setUserRole(userResponse.data.role);

const emailResponse = await axios.get(
`${API_URL}users/is-email-verified/`,
);
setIsEmailVerified(emailResponse.data.email_verified);
setIsEmailVerified(userResponse.data.email_verified);
} else {
setIsAuthenticated(false);
}
Expand All @@ -57,14 +53,21 @@ export const AuthProvider = ({ children }) => {
setIsAuthenticated(newAuthState);
};

const updateEmailVerification = (newEmailVerificationState) => {
const updateEmailVerification = (newEmailVerificationState, callback) => {
setIsEmailVerified(newEmailVerificationState);
if (callback) {
callback();
}
};

const updateUserRole = (newUserRole) => {
setUserRole(newUserRole);
};

const markLoginProcessCompleted = (newState) => {
setLoginProcessCompleted(newState);
};

// The Provider component from our created context is used here.
// It makes the `isAuthenticated` state and `updateAuth` function available to any descendants of this component
return (
Expand All @@ -77,6 +80,8 @@ export const AuthProvider = ({ children }) => {
updateUserRole,
userRole,
isLoading,
loginProcessCompleted,
markLoginProcessCompleted,
}}
>
{children}
Expand Down
23 changes: 13 additions & 10 deletions frontend/src/pages/login/LoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ function LoginPage({ onLogin }) {
updateEmailVerification,
updateUserRole,
isAuthenticated,
isEmailVerified,
markLoginProcessCompleted,
} = useAuth();
const [errorMessage, setErrorMessage] = useState("");
const location = useLocation();
const [emailVerifiedMessage, setEmailVerifiedMessage] = useState("");

useEffect(() => {
if (isAuthenticated) {
if (isAuthenticated && isEmailVerified) {
console.log("Navigating to dashboards");
navigate("/dashboards");
}
}, [isAuthenticated, navigate]);
}, [isAuthenticated, isEmailVerified, navigate]);

useEffect(() => {
if (location.state?.emailVerified) {
Expand Down Expand Up @@ -70,24 +73,24 @@ function LoginPage({ onLogin }) {
);

if (response.status === 200) {
updateAuth(true);
markLoginProcessCompleted(true);
const userResponse = await axios.get(`${API_URL}users/me/`, {
headers: {
Authorization: `Bearer ${response.data.access_token}`,
},
});
console.log(userResponse.data.role);
updateUserRole(userResponse.data.role);
updateAuth(true);

if (userResponse.data.requires_password_update) {
navigate("/change-password");
} else if (userResponse.data.email_verified == false) {
console.log("email_verified is false");
navigate("/verify-email");
} else {
console.log("email_verified is true");
updateEmailVerification(true);
navigate("/dashboards");
updateEmailVerification(userResponse.data.email_verified);
if (userResponse.data.email_verified) {
navigate("/dashboards");
} else {
navigate("/verify-email");
}
}
}
} catch (error) {
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/pages/logout/LogoutPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { useAuth } from "../../contexts/AuthContext";

function Logout() {
const navigate = useNavigate();
const { updateAuth } = useAuth();
const {
updateAuth,
updateEmailVerification,
updateUserRole,
markLoginProcessCompleted,
} = useAuth();

useEffect(() => {
// Clear the authentication cookie
Expand All @@ -15,10 +20,19 @@ function Logout() {

// Update authentication state
updateAuth(false);
updateEmailVerification(false);
updateUserRole(null);
markLoginProcessCompleted(false);

// Redirect to the login page
navigate("/");
}, [navigate, updateAuth]);
}, [
navigate,
updateAuth,
updateEmailVerification,
updateUserRole,
markLoginProcessCompleted,
]);

// Optionally, you can render a message or a spinner here
return <div>Logging out...</div>;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/register/RegisterPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function RegisterPage() {
});

// Navigate to the verify-email page instead of login
navigate("/verify-email", { state: { email } });
navigate("/verify-email");
}
} catch (error) {
if (error.response) {
Expand Down
62 changes: 57 additions & 5 deletions frontend/src/pages/verify-email/VerifyEmailPage.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Box, Button, Container, Typography } from "@mui/material";
import {
Alert,
Box,
Button,
Container,
Snackbar,
Typography,
} from "@mui/material";
import { useAuth } from "../../contexts/AuthContext";
import { API_URL } from "../../utils/constants";
import axios from "axios";
import { set } from "date-fns";

const VerifyEmailPage = () => {
const location = useLocation();
const navigate = useNavigate();
const email = location.state?.email || null;
const [userResponse, setUserResponse] = useState(null);
const [token, setToken] = useState(null);
const { updateAuth, updateEmailVerification } = useAuth();
const { updateAuth, updateEmailVerification, loginProcessCompleted } =
useAuth();
const [openAlert, setOpenAlert] = useState(false);

useEffect(() => {
const params = new URLSearchParams(location.search);
const token = params.get("token");
setToken(token);
}, [location]);

useEffect(() => {
if (!token) {
axios
.get(`${API_URL}users/me/`)
.then((response) => {
if (response.data) {
setUserResponse(response.data);
} else {
// If no user data is returned, navigate to the login page
navigate("/login");
}
})
.catch((error) => {
// If an error occurs, navigate to the login page
navigate("/login");
});
}
}, [token]);

useEffect(() => {
if (token) {
console.log("token", token);
Expand All @@ -38,7 +67,7 @@ const VerifyEmailPage = () => {
.catch((error) => {
// Handle failed token verification
console.log("Failed to verify token");
navigate("/login", { state: { emailVerified: true } });
navigate("/login");
});
})
.catch((error) => {
Expand All @@ -51,17 +80,27 @@ const VerifyEmailPage = () => {
const handleResendEmail = async (event) => {
event.preventDefault();
axios
.post(`${API_URL}users/send-verification-email/`, { email })
.post(`${API_URL}users/send-verification-email/`, {
email: userResponse.email,
})
.then((response) => {
// Handle successful email resend
console.log("Verification email sent successfully");
setOpenAlert(true);
})
.catch((error) => {
// Handle failed email resend
console.log("Failed to send verification email");
});
};

const handleCloseAlert = (event, reason) => {
if (reason === "clickaway") {
return;
}
setOpenAlert(false);
};

return (
<Container maxWidth="sm">
<Box
Expand All @@ -87,6 +126,19 @@ const VerifyEmailPage = () => {
>
Resend Verification Email
</Button>
<Snackbar
open={openAlert}
autoHideDuration={6000}
onClose={handleCloseAlert}
>
<Alert
onClose={handleCloseAlert}
severity="success"
sx={{ width: "100%" }}
>
Verification email has been resent!
</Alert>
</Snackbar>
</Box>
</Container>
);
Expand Down
Loading