Skip to content

Commit 6a0cebc

Browse files
authored
✨ Create endpoint to show password recovery email content and update email template (fastapi#664)
1 parent f70dac4 commit 6a0cebc

File tree

5 files changed

+41
-15
lines changed

5 files changed

+41
-15
lines changed

backend/app/api/routes/login.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from typing import Annotated, Any
33

44
from fastapi import APIRouter, Depends, HTTPException
5+
from fastapi.responses import HTMLResponse
56
from fastapi.security import OAuth2PasswordRequestForm
67

78
from app import crud
8-
from app.api.deps import CurrentUser, SessionDep
9+
from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser
910
from app.core import security
1011
from app.core.config import settings
1112
from app.core.security import get_password_hash
@@ -79,10 +80,10 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
7980
"""
8081
Reset password
8182
"""
82-
user_id = verify_password_reset_token(token=body.token)
83-
if not user_id:
83+
email = verify_password_reset_token(token=body.token)
84+
if not email:
8485
raise HTTPException(status_code=400, detail="Invalid token")
85-
user = session.get(User, int(user_id))
86+
user = crud.get_user_by_email(session=session, email=email)
8687
if not user:
8788
raise HTTPException(
8889
status_code=404,
@@ -95,3 +96,29 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
9596
session.add(user)
9697
session.commit()
9798
return Message(message="Password updated successfully")
99+
100+
101+
@router.post(
102+
"/password-recovery-html-content/{email}",
103+
dependencies=[Depends(get_current_active_superuser)],
104+
response_class=HTMLResponse,
105+
)
106+
def recover_password_html_content(email: str, session: SessionDep) -> Any:
107+
"""
108+
HTML Content for Password Recovery
109+
"""
110+
user = crud.get_user_by_email(session=session, email=email)
111+
112+
if not user:
113+
raise HTTPException(
114+
status_code=404,
115+
detail="The user with this username does not exist in the system.",
116+
)
117+
password_reset_token = generate_password_reset_token(email=email)
118+
email_data = generate_reset_password_email(
119+
email_to=user.email, email=email, token=password_reset_token
120+
)
121+
122+
return HTMLResponse(
123+
content=email_data.html_content, headers={"subject:": email_data.subject}
124+
)

backend/app/email-templates/build/reset_password.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
</style>
2222
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
2323
.mj-column-per-100 { width:100% !important; max-width: 100%; }
24-
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Hello {{ username }}</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">We've received a request to reset your password. You can do it by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{PASSWORD_RESET_LINK}}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset password</a></td></tr></table></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Or copy and paste the following link into your browser:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">This password will expire in {{ valid_hours }} hours.</div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
24+
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Hello {{ username }}</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">We've received a request to reset your password. You can do it by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset password</a></td></tr></table></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Or copy and paste the following link into your browser:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">This password will expire in {{ valid_hours }} hours.</div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
2525
</td></tr></table><![endif]--></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:14px;line-height:1;text-align:center;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

backend/app/email-templates/src/reset_password.mjml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<mj-section background-color="#fff" padding="40px 20px">
44
<mj-column vertical-align="middle" width="100%">
55
<mj-text align="center" padding="35px" font-size="20px" font-family="Arial, Helvetica, sans-serif" color="#333">{{ project_name }} - Password Recovery</mj-text>
6-
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family=", sans-serif" color="#555"><span>Hello {{ username }}</span></mj-text>
6+
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><span>Hello {{ username }}</span></mj-text>
77
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">We've received a request to reset your password. You can do it by clicking the button below:</mj-text>
8-
<mj-button align="center" font-size="18px" background-color="#009688" border-radius="8px" color="#fff" href="{{PASSWORD_RESET_LINK}}" padding="15px 30px">Reset password</mj-button>
8+
<mj-button align="center" font-size="18px" background-color="#009688" border-radius="8px" color="#fff" href="{{ link }}" padding="15px 30px">Reset password</mj-button>
99
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Or copy and paste the following link into your browser:</mj-text>
1010
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
1111
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">This password will expire in {{ valid_hours }} hours.</mj-text>

backend/app/tests/api/api_v1/test_login.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from app.utils import generate_password_reset_token
12
from fastapi.testclient import TestClient
23

34
from app.core.config import settings
@@ -64,13 +65,7 @@ def test_recovery_password_user_not_exits(
6465
def test_reset_password(
6566
client: TestClient, superuser_token_headers: dict[str, str]
6667
) -> None:
67-
login_data = {
68-
"username": settings.FIRST_SUPERUSER,
69-
"password": settings.FIRST_SUPERUSER_PASSWORD,
70-
}
71-
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
72-
token = r.json().get("access_token")
73-
68+
token = generate_password_reset_token(email=settings.FIRST_SUPERUSER)
7469
data = {"new_password": "changethis", "token": token}
7570
r = client.post(
7671
f"{settings.API_V1_STR}/reset-password/",

frontend/src/routes/reset-password.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
Input,
99
Text,
1010
} from '@chakra-ui/react'
11-
import { createFileRoute, redirect } from '@tanstack/react-router'
11+
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
1212
import { SubmitHandler, useForm } from 'react-hook-form'
1313
import { useMutation } from 'react-query'
1414

@@ -36,6 +36,7 @@ function ResetPassword() {
3636
register,
3737
handleSubmit,
3838
getValues,
39+
reset,
3940
formState: { errors },
4041
} = useForm<NewPasswordForm>({
4142
mode: 'onBlur',
@@ -45,6 +46,7 @@ function ResetPassword() {
4546
},
4647
})
4748
const showToast = useCustomToast()
49+
const navigate = useNavigate()
4850

4951
const resetPassword = async (data: NewPassword) => {
5052
const token = new URLSearchParams(window.location.search).get('token')
@@ -56,6 +58,8 @@ function ResetPassword() {
5658
const mutation = useMutation(resetPassword, {
5759
onSuccess: () => {
5860
showToast('Success!', 'Password updated.', 'success')
61+
reset()
62+
navigate({ to: '/login' })
5963
},
6064
onError: (err: ApiError) => {
6165
const errDetail = err.body.detail

0 commit comments

Comments
 (0)