Skip to content

Commit f1bc70b

Browse files
committed
Add email templates
1 parent f4bda48 commit f1bc70b

File tree

11 files changed

+175
-203
lines changed

11 files changed

+175
-203
lines changed

__pycache__/main.cpython-39.pyc

32 Bytes
Binary file not shown.

accounts/main.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,65 @@
1-
from fastapi import APIRouter
1+
from typing import Any, List
22

3-
from accounts.models import UserIn_Pydantic, UserOut_Pydantic
3+
from core.dependencies import (get_current_active_superuser,
4+
get_current_active_user, get_session)
5+
from fastapi import APIRouter, Body, Depends
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from accounts.crud import user
9+
from accounts.models import User
10+
from accounts.schemas import UserCreate, UserUpdate
411

512
router = APIRouter()
613

714

15+
@router.get("/users/", response_model=List[User])
16+
async def get_all_users(
17+
db: AsyncSession = Depends(get_session),
18+
limit: int = 100, offset: int = 0,
19+
current_user: User = Depends(get_current_active_superuser)) -> Any:
20+
"""Return all users."""
21+
return await user.get_multiple(db=db, offset=offset, limit=limit)
22+
23+
24+
@router.post("/users/", response_model=User, status_code=201)
25+
async def create_user(
26+
user_in: UserCreate,
27+
db: AsyncSession = Depends(get_session),
28+
current_user: User = Depends(get_current_active_superuser)) -> Any:
29+
"""
30+
Create new user.
31+
"""
32+
return await user.create(obj_in=user_in, db=db)
833

34+
935

10-
@router.post("/signup/", response_model=UserOut_Pydantic)
11-
async def signup(user: UserIn_Pydantic):
36+
@router.put("/me", response_model=User)
37+
def update_user_me(
38+
*,
39+
db: AsyncSession = Depends(get_session),
40+
password: str = Body(None),
41+
full_name: str = Body(None),
42+
email: EmailStr = Body(None),
43+
current_user: User = Depends(get_current_active_user),
44+
) -> Any:
45+
"""
46+
Update own user.
47+
"""
48+
current_user_data = jsonable_encoder(current_user)
49+
user_in = schemas.UserUpdate(**current_user_data)
50+
if password is not None:
51+
user_in.password = password
52+
if full_name is not None:
53+
user_in.full_name = full_name
54+
if email is not None:
55+
user_in.email = email
56+
user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
57+
return user
58+
59+
1260

13-
return {"User": "Name"}
61+
62+
63+
64+
65+
1.37 KB
Binary file not shown.
-2 Bytes
Binary file not shown.
-70 Bytes
Binary file not shown.

core/settings.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import secrets
22
from functools import lru_cache
3+
from pathlib import Path
34
from typing import Any, Dict, List, Optional, Union
45

56
from pydantic import (AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn,
67
validator)
78

89

910
class Settings(BaseSettings):
10-
PROJECT_NAME: str = "Archangel Macsika"
11+
PROJECT_NAME: str = "Tech Blog"
1112
API_V1_STR: str = "/api/v1"
1213
SECRET_KEY: str = secrets.token_urlsafe(32)
1314
# 60 minutes * 24 hours * 8 days = 8 days
@@ -57,12 +58,13 @@ class Settings(BaseSettings):
5758
EMAILS_FROM_EMAIL: Optional[EmailStr] = None
5859
EMAILS_FROM_NAME: Optional[str] = None
5960

60-
@validator("EMAILS_FROM_NAME")
61+
@validator("EMAILS_FROM_NAME", allow_reuse=True)
6162
def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
6263
if not v:
6364
return values["PROJECT_NAME"]
6465
return v
6566

67+
6668
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
6769
EMAIL_TEMPLATES_DIR: str = "/templates/build"
6870
EMAILS_ENABLED: bool = False
@@ -76,8 +78,8 @@ def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool:
7678
)
7779

7880
EMAIL_TEST_USER: EmailStr = "test@example.com" # type: ignore
79-
FIRST_SUPERUSER: EmailStr
80-
FIRST_SUPERUSER_PASSWORD: str
81+
FIRST_SUPERUSER: EmailStr = "admin@example.com"
82+
FIRST_SUPERUSER_PASSWORD: str = "admin"
8183
USERS_OPEN_REGISTRATION: bool = False
8284

8385

core/utils.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import base64
2+
import logging
23
import math
34
import random
45
import re
56
import string
67
import unicodedata
78
from datetime import datetime, timedelta
8-
from typing import Any, Union
9+
from pathlib import Path
10+
from typing import Any, Dict, Optional, Union
911

12+
import emails
1013
import pyotp
14+
from emails.template import JinjaTemplate
1115
from fastapi import Depends, HTTPException
1216
from jose import jwt
1317
from passlib.context import CryptContext
@@ -44,6 +48,108 @@ def get_password_hash(password: str) -> str:
4448
return pwd_context.hash(password)
4549

4650

51+
def send_email(
52+
email_to: str,
53+
subject_template: str = "",
54+
html_template: str = "",
55+
environment: Dict[str, Any] = {},
56+
) -> None:
57+
"""Send an email"""
58+
assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
59+
message = emails.Message(
60+
subject=JinjaTemplate(subject_template),
61+
html=JinjaTemplate(html_template),
62+
mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
63+
)
64+
smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT}
65+
if settings.SMTP_TLS:
66+
smtp_options["tls"] = True
67+
if settings.SMTP_USER:
68+
smtp_options["user"] = settings.SMTP_USER
69+
if settings.SMTP_PASSWORD:
70+
smtp_options["password"] = settings.SMTP_PASSWORD
71+
response = message.send(to=email_to, render=environment, smtp=smtp_options)
72+
logging.info(f"send email result: {response}")
73+
74+
75+
def send_test_email(email_to: str) -> None:
76+
"""Send test emails"""
77+
project_name = settings.PROJECT_NAME
78+
subject = f"{project_name} - Test email"
79+
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
80+
template_str = f.read()
81+
send_email(
82+
email_to=email_to,
83+
subject_template=subject,
84+
html_template=template_str,
85+
environment={"project_name": settings.PROJECT_NAME, "email": email_to},
86+
)
87+
88+
89+
def send_reset_password_email(email_to: str, email: str, token: str) -> None:
90+
"""Send email for password reset"""
91+
project_name = settings.PROJECT_NAME
92+
subject = f"{project_name} - Password recovery for user {email}"
93+
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
94+
template_str = f.read()
95+
server_host = settings.SERVER_HOST
96+
link = f"{server_host}/reset-password?token={token}"
97+
send_email(
98+
email_to=email_to,
99+
subject_template=subject,
100+
html_template=template_str,
101+
environment={
102+
"project_name": settings.PROJECT_NAME,
103+
"username": email,
104+
"email": email_to,
105+
"valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
106+
"link": link,
107+
},
108+
)
109+
110+
111+
def send_new_account_email(email_to: str, username: str, password: str) -> None:
112+
"""Send email for new user account registration"""
113+
project_name = settings.PROJECT_NAME
114+
subject = f"{project_name} - New account for user {username}"
115+
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
116+
template_str = f.read()
117+
link = settings.SERVER_HOST
118+
send_email(
119+
email_to=email_to,
120+
subject_template=subject,
121+
html_template=template_str,
122+
environment={
123+
"project_name": settings.PROJECT_NAME,
124+
"username": username,
125+
"password": password,
126+
"email": email_to,
127+
"link": link,
128+
},
129+
)
130+
131+
132+
def generate_password_reset_token(email: str) -> str:
133+
"""Generate new password reset token"""
134+
delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
135+
now = datetime.utcnow()
136+
expires = now + delta
137+
exp = expires.timestamp()
138+
encoded_jwt = jwt.encode(
139+
{"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256",
140+
)
141+
return encoded_jwt
142+
143+
144+
def verify_password_reset_token(token: str) -> Optional[str]:
145+
"""Verify the password reset token"""
146+
try:
147+
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
148+
return decoded_token["email"]
149+
except jwt.JWTError:
150+
return None
151+
152+
47153
async def check_existing_row_by_slug(cls, slug: str, db: AsyncSession, status_code: int = None, msg: str = None, **kwargs) -> Any:
48154
"""
49155
Precheck function for checking the existence of a row using a slug
@@ -64,17 +170,22 @@ async def check_existing_row_by_slug(cls, slug: str, db: AsyncSession, status_co
64170

65171

66172
def generate_hotp(email: str, settings: settings = Depends(settings)):
173+
"""
174+
If token genaration is otp based, this generates an 6-digit OTP.
175+
"""
67176
keygen = email+str(datetime.date(datetime.now()))+settings.SECRET_KEY
68177
key = base64.b32encode(keygen.encode())
69178
return pyotp.HOTP(key)
70179

71180

72181
def create_otp(email: str, counter: int):
182+
"""This creates an OTP."""
73183
hotp = generate_hotp(email)
74184
return hotp.at(counter)
75185

76186

77187
def verify_otp(email: str, token: str, counter: int):
188+
"""This verifies an otp."""
78189
hotp = generate_hotp(email)
79190
return hotp.verify(token, counter) # => True
80191

migrations/versions/3bf88ef219e3_category_updated_field.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

migrations/versions/9e56356a845a_category_updated_field.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)