Skip to content

Commit 19e5cbd

Browse files
committed
feature: user reset-password route & email sending functionality added
1 parent 49c4583 commit 19e5cbd

File tree

4 files changed

+208
-2
lines changed

4 files changed

+208
-2
lines changed

src/auth/routes.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
UserCreateResponseSchema,
1515
UserCreateSchema,
1616
UserForgotPasswordSchema,
17+
UserResetPasswordSchema,
1718
)
1819
from .service import UserService
19-
from .utils import decode_url_safe_token, generate_url_safe_token
20+
from .utils import (
21+
decode_url_safe_token,
22+
generate_password_hash,
23+
generate_url_safe_token,
24+
)
2025

2126
auth_router = APIRouter()
2227
user_service = UserService()
@@ -143,3 +148,59 @@ async def forgot_password(
143148
status_code=status.HTTP_200_OK,
144149
content={"message": "Password reset link sent to your email"},
145150
)
151+
152+
153+
@auth_router.post(
154+
"/reset-password/{password_reset_token}", status_code=status.HTTP_200_OK
155+
)
156+
async def reset_password(
157+
password_reset_token: str,
158+
user_data: UserResetPasswordSchema,
159+
session: AsyncSession = Depends(get_session),
160+
):
161+
data = decode_url_safe_token(password_reset_token)
162+
163+
user_uid = data.get("user_uid")
164+
expires_at = data.get("expires_at")
165+
166+
if not user_uid or not expires_at:
167+
return JSONResponse(
168+
status_code=status.HTTP_400_BAD_REQUEST,
169+
content={"message": "Invalid password reset token"},
170+
)
171+
172+
if datetime.now().timestamp() > expires_at:
173+
return JSONResponse(
174+
status_code=status.HTTP_400_BAD_REQUEST,
175+
content={"message": "Password reset token expired"},
176+
)
177+
178+
user = await user_service.get_user_by_uid(user_uid, session)
179+
if not user:
180+
return JSONResponse(
181+
status_code=status.HTTP_404_NOT_FOUND,
182+
content={"message": "User not found"},
183+
)
184+
185+
if user_data.password != user_data.confirm_password:
186+
return JSONResponse(
187+
status_code=status.HTTP_400_BAD_REQUEST,
188+
content={"message": "Passwords do not match"},
189+
)
190+
191+
user.hashed_password = generate_password_hash(user_data.password)
192+
193+
await session.commit()
194+
await session.refresh(user)
195+
196+
await send_email(
197+
[user.email],
198+
"Password Reset Successfully",
199+
"auth/reset_password_success_email.html",
200+
{"first_name": user.first_name},
201+
)
202+
203+
return JSONResponse(
204+
status_code=status.HTTP_200_OK,
205+
content={"message": "Password reset successfully"},
206+
)

src/auth/schemas.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,17 @@ class UserForgotPasswordSchema(BaseModel):
5555
email: str = Field(max_length=50)
5656

5757
model_config = {"json_schema_extra": {"example": {"email": "johndoe@example.com"}}}
58+
59+
60+
class UserResetPasswordSchema(BaseModel):
61+
password: str = Field(min_length=8, max_length=50)
62+
confirm_password: str = Field(min_length=8, max_length=50)
63+
64+
model_config = {
65+
"json_schema_extra": {
66+
"example": {
67+
"password": "JohnDoe@Password123",
68+
"confirm_password": "JohnDoe@Password123",
69+
}
70+
}
71+
}

templates/auth/forgot_password_email.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
<body>
111111
<div class="container">
112112
<div class="header">
113-
<img src="/api/placeholder/200/80" alt="Bookly Logo" class="logo"/>
113+
<img src="https://svgshare.com/i/1BF7.svg" alt="Bookly Logo" class="logo"/>
114114
</div>
115115
<div class="content">
116116
<h1>Reset Your Password, {{first_name}}</h1>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Password Reset Successful - Bookly</title>
7+
<style>
8+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
9+
10+
body {
11+
background-color: #f0fdf4;
12+
font-family: 'Inter', sans-serif;
13+
margin: 0;
14+
padding: 0;
15+
-webkit-font-smoothing: antialiased;
16+
-moz-osx-font-smoothing: grayscale;
17+
}
18+
.container {
19+
width: 100%;
20+
max-width: 600px;
21+
margin: 40px auto;
22+
background-color: #ffffff;
23+
border-radius: 16px;
24+
box-shadow: 0 4px 24px rgba(22, 163, 74, 0.15);
25+
overflow: hidden;
26+
}
27+
.header {
28+
background-color: #22c55e;
29+
padding-top: 32px;
30+
padding-bottom: 22px;
31+
text-align: center;
32+
}
33+
.logo {
34+
height: 56px;
35+
width: auto;
36+
}
37+
.content {
38+
padding: 48px 40px;
39+
padding-bottom: 20px;
40+
text-align: left;
41+
}
42+
h1 {
43+
font-size: 28px;
44+
font-weight: 700;
45+
color: #166534;
46+
margin-bottom: 24px;
47+
}
48+
p {
49+
font-size: 16px;
50+
line-height: 1.6;
51+
color: #14532d;
52+
}
53+
a {
54+
color: #16a34a;
55+
text-decoration: none;
56+
font-weight: 600;
57+
}
58+
.button {
59+
display: inline-block;
60+
background-color: #22c55e;
61+
color: #ffffff;
62+
padding: 14px 32px;
63+
border-radius: 8px;
64+
text-decoration: none;
65+
font-weight: 600;
66+
font-size: 16px;
67+
margin: 12px 0;
68+
transition: background-color 0.3s ease, transform 0.2s ease;
69+
}
70+
.button:hover {
71+
background-color: #16a34a;
72+
transform: translateY(-2px);
73+
}
74+
.note {
75+
background-color: #dcfce7;
76+
border-left: 4px solid #22c55e;
77+
padding: 16px;
78+
margin-top: 26px;
79+
border-radius: 0 8px 8px 0;
80+
}
81+
.footer {
82+
background-color: #f0fdf4;
83+
padding: 24px 40px;
84+
text-align: center;
85+
font-size: 14px;
86+
color: #15803d;
87+
}
88+
.footer a {
89+
color: #166534;
90+
}
91+
.divider {
92+
height: 1px;
93+
background-color: #bbf7d0;
94+
margin: 32px 0;
95+
}
96+
@media (max-width: 600px) {
97+
.container {
98+
margin: 0;
99+
border-radius: 0;
100+
}
101+
.content {
102+
padding: 32px 24px;
103+
}
104+
.footer {
105+
padding: 24px;
106+
}
107+
}
108+
</style>
109+
</head>
110+
<body>
111+
<div class="container">
112+
<div class="header">
113+
<img src="https://svgshare.com/i/1BF7.svg" alt="Bookly Logo" class="logo"/>
114+
</div>
115+
<div class="content">
116+
<h1>Password Reset Successful, {{first_name}}!</h1>
117+
<p>Great news! Your Bookly account password has been successfully reset.</p>
118+
<p>You can now log in to your account using your new password. If you didn't make this change or if you believe an unauthorized person has accessed your account, please contact our support team immediately.</p>
119+
<div class="note">
120+
<p><strong>Security Tip:</strong> To keep your account safe, we recommend using a unique, strong password and enabling two-factor authentication if available.</p>
121+
</div>
122+
<div class="divider"></div>
123+
<p>If you have any questions or concerns, please don't hesitate to reach out to our support team. We're here to help!</p>
124+
</div>
125+
<div class="footer">
126+
<p>&copy; 2024 Bookly - Rohit Vilas Ingole (DataRohit). All rights reserved.</p>
127+
<a href="https://github.com/datarohit">Visit our GitHub</a>
128+
</div>
129+
</div>
130+
</body>
131+
</html>

0 commit comments

Comments
 (0)