diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 3858af83..37a61407 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -64,11 +64,12 @@ const LoginForm = () => { onClick={() => navigate("/forgot-password")} variant="link" className="justify-start px-1 mt-2" + type="button" > Forgot Password? {/* Submit Button */} - diff --git a/store/app/crud/email.py b/store/app/crud/email.py index 1b1a7e1f..31906eda 100644 --- a/store/app/crud/email.py +++ b/store/app/crud/email.py @@ -4,7 +4,7 @@ from typing import List from store.app.crud.base import BaseCrud -from store.app.model import EmailSignUpToken +from store.app.model import EmailSignUpToken, PasswordResetToken class EmailCrud(BaseCrud): @@ -19,15 +19,26 @@ async def get_email_signup_token(self, id: str) -> EmailSignUpToken | None: async def delete_email_signup_token(self, id: str) -> None: await self._delete_item(id) - async def remove_existing_token_for_email(self, email: str) -> None: - user_tokens: List[EmailSignUpToken] = await self._get_items_from_secondary_index( - "email", email, EmailSignUpToken + async def create_password_reset_token(self, email: str) -> PasswordResetToken: + reset_token = PasswordResetToken.create(email=email) + await self._add_item(reset_token) + return reset_token + + async def get_password_reset_token(self, id: str) -> PasswordResetToken | None: + return await self._get_item(id, PasswordResetToken, throw_if_missing=False) + + async def delete_password_reset_token(self, id: str) -> None: + await self._delete_item(id) + + async def delete_password_reset_token_by_email(self, email: str) -> None: + user_tokens: List[PasswordResetToken] = await self._get_items_from_secondary_index( + "email", email, PasswordResetToken ) if not user_tokens: return - delete_tasks = [self.delete_email_signup_token(token.id) for token in user_tokens] + delete_tasks = [self._delete_item(token.id) for token in user_tokens] await asyncio.gather(*delete_tasks) diff --git a/store/app/model.py b/store/app/model.py index 751f6919..a8408cc3 100644 --- a/store/app/model.py +++ b/store/app/model.py @@ -117,6 +117,19 @@ def create(cls, email: str) -> Self: return cls(id=new_uuid(), email=email) +class PasswordResetToken(StoreBaseModel): + """Object created when user requests a password reset. + + Used to validate and authorize password reset requests. + """ + + email: str + + @classmethod + def create(cls, email: str) -> Self: + return cls(id=new_uuid(), email=email) + + class OAuthKey(StoreBaseModel): """Keys for OAuth providers which identify users.""" diff --git a/store/app/routers/users.py b/store/app/routers/users.py index 1a8d13ae..e14c750b 100644 --- a/store/app/routers/users.py +++ b/store/app/routers/users.py @@ -399,8 +399,8 @@ async def generate_password_reset_token( ) -> ForgotPasswordResponse: try: if user := await crud.get_user_from_email(data.email): - await crud.remove_existing_token_for_email(user.email) - reset_token = await crud.create_email_signup_token(email=user.email) + await crud.delete_password_reset_token_by_email(user.email) + reset_token = await crud.create_password_reset_token(email=user.email) await send_reset_password_email(email=user.email, token=reset_token.id) logger.info(f"Password reset email sent to {user.email}") @@ -424,7 +424,7 @@ class ResetPasswordResponse(BaseModel): async def validate_password_reset_token( data: ResetPasswordRequest, crud: Annotated[Crud, Depends(Crud.get)] ) -> ResetPasswordResponse: - reset_token = await crud.get_email_signup_token(data.token) + reset_token = await crud.get_password_reset_token(data.token) if not reset_token: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired reset token") @@ -439,6 +439,6 @@ async def validate_password_reset_token( ) # Remove reset token - await crud.delete_email_signup_token(data.token) + await crud.delete_password_reset_token(data.token) return ResetPasswordResponse(message="Password updated successful", email=updated_user.email)