Skip to content

Commit 0ef838f

Browse files
committed
updated
1 parent 44bf1c2 commit 0ef838f

File tree

8 files changed

+530
-9
lines changed

8 files changed

+530
-9
lines changed

.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ JWT_ALGORITHM=RS256
1010

1111
CLIENT_ORIGIN=http://localhost:3000
1212

13+
EMAIL_HOST=smtp.mailtrap.io
14+
EMAIL_PORT=587
15+
EMAIL_USERNAME=90cf952fb44469
16+
EMAIL_PASSWORD=0524531956c552
17+
EMAIL_FROM=admin@admin.com
1318

1419
JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT2dJQkFBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVmlQWlJyVFpjd3l4RVhVRGpNaFZuCi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUUpBYm5LaENOQ0dOSFZGaHJPQ0RCU0IKdmZ2ckRWUzVpZXAwd2h2SGlBUEdjeWV6bjd0U2RweUZ0NEU0QTNXT3VQOXhqenNjTFZyb1pzRmVMUWlqT1JhUwp3UUloQU84MWl2b21iVGhjRkltTFZPbU16Vk52TGxWTW02WE5iS3B4bGh4TlpUTmhBaUVBbWRISlpGM3haWFE0Cm15QnNCeEhLQ3JqOTF6bVFxU0E4bHUvT1ZNTDNSak1DSVFEbDJxOUdtN0lMbS85b0EyaCtXdnZabGxZUlJPR3oKT21lV2lEclR5MUxaUVFJZ2ZGYUlaUWxMU0tkWjJvdXF4MHdwOWVEejBEWklLVzVWaSt6czdMZHRDdUVDSUVGYwo3d21VZ3pPblpzbnU1clBsTDJjZldLTGhFbWwrUVFzOCtkMFBGdXlnCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t
1520
JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVgppUFpSclRaY3d5eEVYVURqTWhWbi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==

app/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pydantic import BaseSettings
1+
from pydantic import BaseSettings, EmailStr
22

33

44
class Settings(BaseSettings):
@@ -13,6 +13,12 @@ class Settings(BaseSettings):
1313

1414
CLIENT_ORIGIN: str
1515

16+
EMAIL_HOST: str
17+
EMAIL_PORT: int
18+
EMAIL_USERNAME: str
19+
EMAIL_PASSWORD: str
20+
EMAIL_FROM: EmailStr
21+
1622
class Config:
1723
env_file = './.env'
1824

app/email.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from typing import List
2+
from fastapi_mail import FastMail, MessageSchema, ConnectionConfig
3+
from pydantic import EmailStr, BaseModel
4+
from .config import settings
5+
from jinja2 import Environment, select_autoescape, PackageLoader
6+
7+
8+
env = Environment(
9+
loader=PackageLoader('app', 'templates'),
10+
autoescape=select_autoescape(['html', 'xml'])
11+
)
12+
13+
14+
class EmailSchema(BaseModel):
15+
email: List[EmailStr]
16+
17+
18+
class Email:
19+
def __init__(self, user: dict, url: str, email: List[EmailStr]):
20+
self.name = user['name']
21+
self.sender = 'Codevo <admin@admin.com>'
22+
self.email = email
23+
self.url = url
24+
pass
25+
26+
async def sendMail(self, subject, template):
27+
# Define the config
28+
conf = ConnectionConfig(
29+
MAIL_USERNAME=settings.EMAIL_USERNAME,
30+
MAIL_PASSWORD=settings.EMAIL_PASSWORD,
31+
MAIL_FROM=settings.EMAIL_FROM,
32+
MAIL_PORT=settings.EMAIL_PORT,
33+
MAIL_SERVER=settings.EMAIL_HOST,
34+
MAIL_TLS=True,
35+
MAIL_SSL=False,
36+
USE_CREDENTIALS=True,
37+
VALIDATE_CERTS=True
38+
)
39+
# Generate the HTML template base on the template name
40+
template = env.get_template(f'{template}.html')
41+
42+
html = template.render(
43+
url=self.url,
44+
first_name=self.name,
45+
subject=subject
46+
)
47+
48+
# Define the message options
49+
message = MessageSchema(
50+
subject=subject,
51+
recipients=self.email,
52+
body=html,
53+
subtype="html"
54+
)
55+
56+
# Send the email
57+
fm = FastMail(conf)
58+
await fm.send_message(message)
59+
60+
async def sendVerificationCode(self):
61+
await self.sendMail('Your verification code (Valid for 10min)', 'verification')

app/routers/auth.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from datetime import datetime, timedelta
2+
import hashlib
3+
from random import randbytes
24
from bson.objectid import ObjectId
35
from fastapi import APIRouter, Request, Response, status, Depends, HTTPException
46
from pydantic import EmailStr
57

68
from app import oauth2
79
from app.database import User
8-
from app.serializers import userEntity, userResponseEntity
10+
from app.email import Email
11+
from app.serializers import userEntity
912
from .. import schemas, utils
1013
from app.oauth2 import AuthJWT
1114
from ..config import settings
@@ -16,8 +19,8 @@
1619
REFRESH_TOKEN_EXPIRES_IN = settings.REFRESH_TOKEN_EXPIRES_IN
1720

1821

19-
@router.post('/register', status_code=status.HTTP_201_CREATED, response_model=schemas.UserResponse)
20-
async def create_user(payload: schemas.CreateUserSchema):
22+
@router.post('/register', status_code=status.HTTP_201_CREATED)
23+
async def create_user(payload: schemas.CreateUserSchema, request: Request):
2124
# Check if user already exist
2225
user = User.find_one({'email': payload.email.lower()})
2326
if user:
@@ -31,13 +34,29 @@ async def create_user(payload: schemas.CreateUserSchema):
3134
payload.password = utils.hash_password(payload.password)
3235
del payload.passwordConfirm
3336
payload.role = 'user'
34-
payload.verified = True
37+
payload.verified = False
3538
payload.email = payload.email.lower()
3639
payload.created_at = datetime.utcnow()
3740
payload.updated_at = payload.created_at
41+
3842
result = User.insert_one(payload.dict())
39-
new_user = userResponseEntity(User.find_one({'_id': result.inserted_id}))
40-
return {"status": "success", "user": new_user}
43+
new_user = User.find_one({'_id': result.inserted_id})
44+
try:
45+
token = randbytes(10)
46+
hashedCode = hashlib.sha256()
47+
hashedCode.update(token)
48+
verification_code = hashedCode.hexdigest()
49+
User.find_one_and_update({"_id": result.inserted_id}, {
50+
"$set": {"verification_code": verification_code, "updated_at": datetime.utcnow()}})
51+
52+
url = f"{request.url.scheme}://{request.client.host}:{request.url.port}/api/auth/verifyemail/{token.hex()}"
53+
await Email(userEntity(new_user), url, [EmailStr(payload.email)]).sendVerificationCode()
54+
except Exception as error:
55+
User.find_one_and_update({"_id": result.inserted_id}, {
56+
"$set": {"verification_code": None, "updated_at": datetime.utcnow()}})
57+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
58+
detail='There was an error sending email')
59+
return {'status': 'success', 'message': 'Verification token successfully sent to your email'}
4160

4261

4362
@router.post('/login')
@@ -114,3 +133,19 @@ def logout(response: Response, Authorize: AuthJWT = Depends(), user_id: str = De
114133
response.set_cookie('logged_in', '', -1)
115134

116135
return {'status': 'success'}
136+
137+
138+
@router.get('/verifyemail/{token}')
139+
def verify_me(token: str):
140+
hashedCode = hashlib.sha256()
141+
hashedCode.update(bytes.fromhex(token))
142+
verification_code = hashedCode.hexdigest()
143+
result = User.find_one_and_update({"verification_code": verification_code}, {
144+
"$set": {"verification_code": None, "verified": True, "updated_at": datetime.utcnow()}}, new=True)
145+
if not result:
146+
raise HTTPException(
147+
status_code=status.HTTP_403_FORBIDDEN, detail='Invalid verification code or account already verified')
148+
return {
149+
"status": "success",
150+
"message": "Account verified successfully"
151+
}

app/schemas.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44

55
class UserBaseSchema(BaseModel):
6-
id: str | None = None
76
name: str
87
email: str
98
photo: str
@@ -26,6 +25,11 @@ class LoginUserSchema(BaseModel):
2625
password: constr(min_length=8)
2726

2827

28+
class UserResponseSchema(UserBaseSchema):
29+
id: str
30+
pass
31+
32+
2933
class UserResponse(BaseModel):
3034
status: str
31-
user: UserBaseSchema
35+
user: UserResponseSchema

0 commit comments

Comments
 (0)