diff --git a/backend/src/auth/dependencies.py b/backend/src/auth/dependencies.py
index 90ad8e91..0455f7ad 100644
--- a/backend/src/auth/dependencies.py
+++ b/backend/src/auth/dependencies.py
@@ -1,3 +1,4 @@
+from http import HTTPStatus
from typing import Annotated, Optional
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
@@ -56,9 +57,15 @@ def authenticate_user(email: str, password: str):
)
).first()
if not user:
- return False
+ raise HTTPException(
+ status_code=HTTPStatus.UNAUTHORIZED,
+ detail={"error": "Invalid username.", "error_id": "1"},
+ )
if not verify_password(password, user.hashed_password):
- return False
+ raise HTTPException(
+ status_code=HTTPStatus.UNAUTHORIZED,
+ detail={"error": "Incorrect password.", "error_id": "2"},
+ )
return user
diff --git a/backend/src/auth/router.py b/backend/src/auth/router.py
index 2994137d..77dcb984 100644
--- a/backend/src/auth/router.py
+++ b/backend/src/auth/router.py
@@ -7,7 +7,7 @@
from fastapi import BackgroundTasks, Depends, APIRouter, HTTPException, Response
from fastapi.security import OAuth2PasswordRequestForm
import httpx
-from sqlalchemy import select
+from sqlalchemy import select, update
from sqlalchemy.orm import selectinload
from src.auth.utils import create_token, send_reset_password_email
from src.common.constants import (
@@ -77,12 +77,10 @@ def sign_up(
def log_in(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()], response: Response
) -> Token:
- user = authenticate_user(form_data.username, form_data.password)
- if not user:
- raise HTTPException(
- status_code=HTTPStatus.UNAUTHORIZED,
- detail="Incorrect username or password.",
- )
+ try:
+ user = authenticate_user(form_data.username, form_data.password)
+ except HTTPException as exc:
+ raise exc
return create_token(user, response)
@@ -151,27 +149,10 @@ def auth_google(
)
-@routerWithAuth.get("/session")
-def get_user(
- current_user: Annotated[User, Depends(get_current_user)],
- session=Depends(get_session),
-) -> UserPublic:
- user = session.get(User, current_user.id)
- if user:
- user.last_accessed = datetime.now()
- session.add(user)
- session.commit()
-
- return current_user
-
-
-@routerWithAuth.get("/logout")
-def logout(response: Response):
- response.delete_cookie(key="session")
- return ""
-
-
-@routerWithAuth.post("/password-reset")
+#######################
+# Reset password #
+#######################
+@router.post("/password-reset")
def request_password_reset(
data: PasswordResetRequestData,
background_task: BackgroundTasks,
@@ -184,7 +165,16 @@ def request_password_reset(
.where(User.account_type == AccountType.NORMAL)
).first()
if not user:
- return
+ print(
+ f"""ERROR: Attempt to reset password for email {email} that doesn't match any existing user"""
+ )
+ raise HTTPException(HTTPStatus.NOT_FOUND)
+
+ # Mark all existing password reset codes for the given user as used to prevent usage of old codes
+ session.execute(
+ update(PasswordReset).where(PasswordReset.user_id == user.id).values(used=True)
+ )
+ session.commit()
code = str(uuid4())
password_reset = PasswordReset(user_id=user.id, code=code, used=False)
@@ -193,7 +183,7 @@ def request_password_reset(
background_task.add_task(send_reset_password_email, email, code)
-@routerWithAuth.put("/password-reset")
+@router.put("/password-reset")
def complete_password_reset(
code: str,
data: PasswordResetCompleteData,
@@ -203,9 +193,16 @@ def complete_password_reset(
password_reset = session.scalars(
select(PasswordReset).where(PasswordReset.code == code)
).first()
- if not password_reset or password_reset.used:
+ if not password_reset:
+ print(
+ "ERROR: Attempt to use password reset code that wasn't previously generated by Jippy"
+ )
raise HTTPException(HTTPStatus.NOT_FOUND)
+ if password_reset.used:
+ print("ERROR: Attempt to use password reset code that has already been used")
+ raise (HTTPException(HTTPStatus.CONFLICT))
+
user = session.get(User, password_reset.user_id)
user.hashed_password = get_password_hash(data.password)
password_reset.used = True
@@ -214,6 +211,26 @@ def complete_password_reset(
session.commit()
+@routerWithAuth.get("/session")
+def get_user(
+ current_user: Annotated[User, Depends(get_current_user)],
+ session=Depends(get_session),
+) -> UserPublic:
+ user = session.get(User, current_user.id)
+ if user:
+ user.last_accessed = datetime.now()
+ session.add(user)
+ session.commit()
+
+ return current_user
+
+
+@routerWithAuth.get("/logout")
+def logout(response: Response):
+ response.delete_cookie(key="session")
+ return ""
+
+
@routerWithAuth.put("/change-password")
def change_password(
user: Annotated[User, Depends(get_current_user)],
diff --git a/frontend/app/(unauthenticated)/login/page.tsx b/frontend/app/(unauthenticated)/login/page.tsx
index b33bed32..3929dc14 100644
--- a/frontend/app/(unauthenticated)/login/page.tsx
+++ b/frontend/app/(unauthenticated)/login/page.tsx
@@ -95,7 +95,7 @@ function LoginPage() {
Your email or password is incorrect. Please try again, or{" "}
-
+
reset your password
.
diff --git a/frontend/app/(unauthenticated)/register/page.tsx b/frontend/app/(unauthenticated)/register/page.tsx
index f9581b22..5baf041f 100644
--- a/frontend/app/(unauthenticated)/register/page.tsx
+++ b/frontend/app/(unauthenticated)/register/page.tsx
@@ -86,8 +86,13 @@ function RegisterPage() {
{/* Body */}
{isError && (
-
-
+
+
+
+
This email is already registered.{" "}
diff --git a/frontend/app/(unauthenticated)/reset-password/_components/reset-password-complete-form.tsx b/frontend/app/(unauthenticated)/reset-password/_components/reset-password-complete-form.tsx
index 9c91e822..6a7e6e3c 100644
--- a/frontend/app/(unauthenticated)/reset-password/_components/reset-password-complete-form.tsx
+++ b/frontend/app/(unauthenticated)/reset-password/_components/reset-password-complete-form.tsx
@@ -3,6 +3,7 @@
import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
+import { HttpStatusCode } from "axios";
import { CircleAlert } from "lucide-react";
import { z } from "zod";
@@ -54,6 +55,9 @@ export default function ResetPasswordCompleteForm({
onComplete: () => void;
}) {
const [isError, setIsError] = useState(false);
+ const [errorMsg, setErrorMsg] = useState(
+ "Your password needs to match.",
+ );
const form = useForm({
resolver: zodResolver(resetPasswordCompleteFormSchema),
@@ -74,6 +78,17 @@ export default function ResetPasswordCompleteForm({
if (response.error) {
setIsError(true);
+ if (response.status === HttpStatusCode.Conflict) {
+ setErrorMsg(
+ "Password reset link has expired. Please check your email for the latest link",
+ );
+ } else if (response.status === HttpStatusCode.NotFound) {
+ setErrorMsg(
+ "Invalid password reset link. Please only click on links sent by us",
+ );
+ } else {
+ setErrorMsg("Error while resetting your password. Please try again");
+ }
} else {
setIsError(false);
onComplete();
@@ -91,11 +106,14 @@ export default function ResetPasswordCompleteForm({
{isError && (
-
-
-
- Your password needs to match.
-
+
+
+
+
+ {errorMsg}
)}