Skip to content

Commit 1767f1b

Browse files
committed
feature: user activation route & email sending functionality added
1 parent 7356fb2 commit 1767f1b

File tree

4 files changed

+199
-2
lines changed

4 files changed

+199
-2
lines changed

src/auth/routes.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .schemas import UserCreateResponseSchema, UserCreateSchema
1414
from .service import UserService
15-
from .utils import generate_url_safe_token
15+
from .utils import decode_url_safe_token, generate_url_safe_token
1616

1717
auth_router = APIRouter()
1818
user_service = UserService()
@@ -35,7 +35,7 @@ async def register_user(
3535
}
3636
)
3737
user_activation_link = (
38-
f"http://{Config.DOMAIN}/auth/activate/{user_activation_token}"
38+
f"http://{Config.DOMAIN}/api/v1/auth/activate/{user_activation_token}"
3939
)
4040

4141
await send_email(
@@ -54,3 +54,52 @@ async def register_user(
5454
).model_dump(),
5555
},
5656
)
57+
58+
59+
@auth_router.post("/activate/{activation_token}", status_code=status.HTTP_200_OK)
60+
async def activate_user(
61+
activation_token: str,
62+
session: AsyncSession = Depends(get_session),
63+
):
64+
data = decode_url_safe_token(activation_token)
65+
66+
user_uid = data.get("user_uid")
67+
expires_at = data.get("expires_at")
68+
69+
if not user_uid or not expires_at:
70+
return JSONResponse(
71+
status_code=status.HTTP_400_BAD_REQUEST,
72+
content={"message": "Invalid activation token"},
73+
)
74+
75+
if datetime.now().timestamp() > expires_at:
76+
return JSONResponse(
77+
status_code=status.HTTP_400_BAD_REQUEST,
78+
content={"message": "Activation token expired"},
79+
)
80+
81+
user = await user_service.get_user_by_uid(user_uid, session)
82+
if user.is_verified or user.is_active:
83+
return JSONResponse(
84+
status_code=status.HTTP_400_BAD_REQUEST,
85+
content={"message": "User already activated"},
86+
)
87+
88+
await user_service.activate_user(user_uid, session)
89+
90+
await send_email(
91+
[user.email],
92+
"Acount Activated Successfully",
93+
"auth/activation_success_email.html",
94+
{"first_name": user.first_name},
95+
)
96+
97+
return JSONResponse(
98+
status_code=status.HTTP_200_OK,
99+
content={
100+
"message": "User activated successfully",
101+
"user": UserCreateResponseSchema(
102+
**json.loads(user.model_dump_json())
103+
).model_dump(),
104+
},
105+
)

src/auth/service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,22 @@ async def get_user_by_email(self, email: str, session: AsyncSession) -> User:
2929
user = result.scalars().first()
3030
return user
3131

32+
async def get_user_by_uid(self, user_uid: str, session: AsyncSession) -> User:
33+
user = await session.get(User, user_uid)
34+
return user
35+
3236
async def user_exists(self, email: str, session: AsyncSession) -> bool:
3337
user = await self.get_user_by_email(email, session)
3438
return True if user else False
3539

40+
async def activate_user(self, user_uid: str, session: AsyncSession) -> None:
41+
user = await session.get(User, user_uid)
42+
user.is_verified = True
43+
user.is_active = True
44+
45+
await session.commit()
46+
await session.refresh(user)
47+
3648
async def update_user(
3749
self, user: User, user_data: dict, session: AsyncSession
3850
) -> User:

src/auth/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ def verify_password(password: str, hashed_password: str) -> bool:
1919

2020
def generate_url_safe_token(data: dict) -> str:
2121
return url_safe_timed_serializer.dumps(data)
22+
23+
24+
def decode_url_safe_token(token: str) -> dict:
25+
return url_safe_timed_serializer.loads(token)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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>Account Activated - 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: #f4f7fa;
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(0, 0, 0, 0.08);
25+
overflow: hidden;
26+
}
27+
.header {
28+
background-color: #10b981;
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: #1f2937;
46+
margin-bottom: 24px;
47+
}
48+
p {
49+
font-size: 16px;
50+
line-height: 1.6;
51+
color: #4b5563;
52+
}
53+
a {
54+
color: #10b981;
55+
text-decoration: none;
56+
font-weight: 600;
57+
}
58+
.button {
59+
display: inline-block;
60+
background-color: #10b981;
61+
color: white;
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: #059669;
72+
transform: translateY(-2px);
73+
}
74+
.note {
75+
background-color: #ecfdf5;
76+
border-left: 4px solid #10b981;
77+
padding: 16px;
78+
margin-top: 26px;
79+
border-radius: 0 8px 8px 0;
80+
}
81+
.footer {
82+
background-color: #f9fafb;
83+
padding: 24px 40px;
84+
text-align: center;
85+
font-size: 14px;
86+
color: #6b7280;
87+
}
88+
.footer a {
89+
color: #4b5563;
90+
}
91+
.divider {
92+
height: 1px;
93+
background-color: #e5e7eb;
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>Account Activated Successfully, {{first_name}}!</h1>
117+
<p>Great news! Your Bookly account has been successfully activated. We're excited to have you as part of our community of book enthusiasts.</p>
118+
<p>You now have full access to all of Bookly's features, including:</p>
119+
<ul>
120+
<li>Browsing our extensive collection of books</li>
121+
<li>Creating personalized reading lists</li>
122+
<li>Connecting with fellow readers</li>
123+
<li>Participating in book discussions and reviews</li>
124+
</ul>
125+
</div>
126+
<div class="footer">
127+
<p>&copy; 2024 Bookly - Rohit Vilas Ingole (DataRohit). All rights reserved.</p>
128+
<a href="https://github.com/datarohit">Visit our GitHub</a>
129+
</div>
130+
</div>
131+
</body>
132+
</html>

0 commit comments

Comments
 (0)