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 794d72df..bda15b6d 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,
@@ -103,12 +103,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)
@@ -285,6 +283,68 @@ def complete_password_reset(
session.commit()
+#######################
+# Reset password #
+#######################
+@router.post("/password-reset")
+def request_password_reset(
+ data: PasswordResetRequestData,
+ background_task: BackgroundTasks,
+ session=Depends(get_session),
+):
+ email = data.email
+ user = session.scalars(
+ select(User)
+ .where(User.email == email)
+ .where(User.account_type == AccountType.NORMAL)
+ ).first()
+ if not user:
+ 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)
+ session.add(password_reset)
+ session.commit()
+ background_task.add_task(send_reset_password_email, email, code)
+
+
+@router.put("/password-reset")
+def complete_password_reset(
+ code: str,
+ data: PasswordResetCompleteData,
+ session=Depends(get_session),
+):
+ # 9b90a1bd-ccab-4dcb-93c9-9ef2367dbcc4
+ password_reset = session.scalars(
+ select(PasswordReset).where(PasswordReset.code == code)
+ ).first()
+ 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
+ session.add(user)
+ session.add(password_reset)
+ session.commit()
+
+
@routerWithAuth.get("/session")
def get_user(
current_user: Annotated[User, Depends(get_current_user)],
diff --git a/frontend/app/(unauthenticated)/login/page.tsx b/frontend/app/(unauthenticated)/login/page.tsx
index 490ffef5..a4763175 100644
--- a/frontend/app/(unauthenticated)/login/page.tsx
+++ b/frontend/app/(unauthenticated)/login/page.tsx
@@ -99,7 +99,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 01a92f1f..45ef1acc 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}
)}