Skip to content

Commit 7356fb2

Browse files
committed
feature: user registration email sending functionality added
1 parent cfa131c commit 7356fb2

File tree

8 files changed

+187
-12
lines changed

8 files changed

+187
-12
lines changed

.envs/.backend.env

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ MAIL_PORT=1025
1515
MAIL_FROM="no-reply@bookly.serveo.net"
1616

1717
JWT_SECRET="0d6dfd6448e6a1ee48dcb8e090618cf8c37dc046e8169698d22383510c8e70f3"
18-
JWT_ALGORITHM="HS256"
18+
JWT_SALT="4f20b6d57382c312f523fb5fba4dd25f6efaa766773a20b7e4606b51c9f81b6b"
19+
JWT_ALGORITHM="HS256"
20+
21+
DOMAIN="localhost:8000"

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,7 @@ cython_debug/
159159
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160160
# and can be added to the global gitignore or merged into this file. For a more nuclear
161161
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162-
#.idea/
162+
#.idea/
163+
164+
# SSH keys
165+
*.pem

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ services:
2121
postgres:
2222
image: postgres:16
2323
container_name: postgres
24+
ports:
25+
- "5432:5432"
2426
volumes:
2527
- postgres_data:/var/lib/postgresql/data
2628
env_file:

pkg/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ class Settings(BaseSettings):
1616
MAIL_PASSWORD: str = Field(..., env="MAIL_PASSWORD")
1717
MAIL_SERVER: str = Field(..., env="MAIL_SERVER")
1818
MAIL_PORT: int = Field(..., env="MAIL_PORT")
19-
2019
MAIL_FROM: str = Field(..., env="MAIL_FROM")
2120

2221
JWT_SECRET: str = Field(..., env="JWT_SECRET")
22+
JWT_SALT: str = Field(..., env="JWT_SALT")
2323
JWT_ALGORITHM: str = Field(..., env="JWT_ALGORITHM")
2424

25+
DOMAIN: str = Field(..., env="DOMAIN")
26+
2527
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
2628

2729

pkg/mail.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
from email.mime.text import MIMEText
33
from pathlib import Path
44

5-
from aiosmtplib import SMTP
5+
import aiosmtplib
66
from jinja2 import Environment, FileSystemLoader
77

88
from pkg.config import Config
99

10-
BASE_DIR = Path(__file__).resolve().parent
10+
BASE_DIR = Path(__file__).resolve().parent.parent
1111
TEMPLATE_FOLDER = Path(BASE_DIR, "templates")
1212

1313
env = Environment(loader=FileSystemLoader(str(TEMPLATE_FOLDER)))
@@ -35,9 +35,13 @@ async def send_email(
3535
message = await create_message(recipients, subject, template_name, context)
3636

3737
try:
38-
async with SMTP(host=Config.MAIL_SERVER, port=Config.MAIL_PORT) as server:
39-
await server.starttls()
40-
await server.login(Config.MAIL_USER, Config.MAIL_PASSWORD)
41-
await server.send_message(message)
38+
await aiosmtplib.send(
39+
message,
40+
sender=Config.MAIL_USER,
41+
recipients=recipients,
42+
hostname=Config.MAIL_SERVER,
43+
port=Config.MAIL_PORT,
44+
use_tls=False,
45+
)
4246
except Exception as e:
4347
print(f"Error sending email: {e}")

src/auth/routes.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import json
2+
from datetime import datetime, timedelta
23

3-
from fastapi import APIRouter, BackgroundTasks, Depends, status
4+
from fastapi import APIRouter, Depends, status
45
from fastapi.responses import JSONResponse
56
from sqlmodel.ext.asyncio.session import AsyncSession
67

8+
from pkg.config import Config
79
from pkg.db import get_session
810
from pkg.errors import UserAlreadyExists
11+
from pkg.mail import send_email
912

1013
from .schemas import UserCreateResponseSchema, UserCreateSchema
1114
from .service import UserService
15+
from .utils import generate_url_safe_token
1216

1317
auth_router = APIRouter()
1418
user_service = UserService()
@@ -17,14 +21,30 @@
1721
@auth_router.post("/register", status_code=status.HTTP_201_CREATED)
1822
async def register_user(
1923
user_data: UserCreateSchema,
20-
background_tasks: BackgroundTasks,
2124
session: AsyncSession = Depends(get_session),
2225
):
2326
if await user_service.user_exists(user_data.email, session):
2427
raise UserAlreadyExists
2528

2629
user = await user_service.create_user(user_data, session)
2730

31+
user_activation_token = generate_url_safe_token(
32+
{
33+
"user_uid": str(user.uid),
34+
"expires_at": (datetime.now() + timedelta(minutes=15)).timestamp(),
35+
}
36+
)
37+
user_activation_link = (
38+
f"http://{Config.DOMAIN}/auth/activate/{user_activation_token}"
39+
)
40+
41+
await send_email(
42+
[user.email],
43+
"Activate your account",
44+
"auth/activation_email.html",
45+
{"first_name": user.first_name, "activation_link": user_activation_link},
46+
)
47+
2848
return JSONResponse(
2949
status_code=status.HTTP_201_CREATED,
3050
content={

src/auth/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import bcrypt
2+
from itsdangerous import URLSafeTimedSerializer
23

34
from pkg.config import Config
45

6+
url_safe_timed_serializer = URLSafeTimedSerializer(
7+
secret_key=Config.JWT_SECRET, salt=Config.JWT_SALT
8+
)
9+
510

611
def generate_password_hash(password: str) -> str:
712
salt = bcrypt.gensalt(rounds=Config.BCRYPT_ROUND)
813
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
914

1015

11-
def veryfy_password(password: str, hashed_password: str) -> bool:
16+
def verify_password(password: str, hashed_password: str) -> bool:
1217
return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))
18+
19+
20+
def generate_url_safe_token(data: dict) -> str:
21+
return url_safe_timed_serializer.dumps(data)
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>Welcome to 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: #3b82f6;
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: #3b82f6;
55+
text-decoration: none;
56+
font-weight: 600;
57+
}
58+
.button {
59+
display: inline-block;
60+
background-color: #3b82f6;
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: #2563eb;
72+
transform: translateY(-2px);
73+
}
74+
.note {
75+
background-color: #f3f4f6;
76+
border-left: 4px solid #3b82f6;
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>Welcome to Bookly, {{first_name}}!</h1>
117+
<p>Thank you for joining our community of book lovers. We're thrilled to have you on board and can't wait to start this literary journey together.</p>
118+
<p>To begin exploring our vast collection of books and connect with fellow readers, please activate your account:</p>
119+
<a href="{{activation_link}}" class="button">Activate Your Account</a>
120+
<div class="note">
121+
<p><strong>Important:</strong> This activation link is valid for 15 minutes. If you miss this window, you'll need to register again.</p>
122+
</div>
123+
<div class="divider"></div>
124+
<p>If you didn't create an account with Bookly, please disregard this email. Your privacy and security are our top priorities.</p>
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)