11import base64
2+ import logging
23import math
34import random
45import re
56import string
67import unicodedata
78from 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
1013import pyotp
14+ from emails .template import JinjaTemplate
1115from fastapi import Depends , HTTPException
1216from jose import jwt
1317from 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+
47153async 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
66172def 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
72181def create_otp (email : str , counter : int ):
182+ """This creates an OTP."""
73183 hotp = generate_hotp (email )
74184 return hotp .at (counter )
75185
76186
77187def 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
0 commit comments