├── .env ├── .gitignore ├── Makefile ├── app ├── __init__.py ├── config.py ├── database.py ├── email.py ├── main.py ├── oauth2.py ├── routers │ ├── auth.py │ ├── post.py │ └── user.py ├── schemas.py ├── serializers │ ├── postSerializers.py │ └── userSerializers.py ├── templates │ ├── _styles.html │ ├── base.html │ └── verification.html └── utils.py ├── docker-compose.yml ├── readMe.md └── requirements.txt /.env: -------------------------------------------------------------------------------- 1 | MONGO_INITDB_ROOT_USERNAME=admin 2 | MONGO_INITDB_ROOT_PASSWORD=password123 3 | MONGO_INITDB_DATABASE=fastapi 4 | 5 | DATABASE_URL=mongodb://admin:password123@localhost:6000/fastapi?authSource=admin 6 | 7 | ACCESS_TOKEN_EXPIRES_IN=15 8 | REFRESH_TOKEN_EXPIRES_IN=60 9 | JWT_ALGORITHM=RS256 10 | 11 | CLIENT_ORIGIN=http://localhost:3000 12 | 13 | EMAIL_HOST=smtp.mailtrap.io 14 | EMAIL_PORT=587 15 | EMAIL_USERNAME=576913cd140734 16 | EMAIL_PASSWORD=9fcf4d01f98bb7 17 | EMAIL_FROM=admin@admin.com 18 | 19 | JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT2dJQkFBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVmlQWlJyVFpjd3l4RVhVRGpNaFZuCi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUUpBYm5LaENOQ0dOSFZGaHJPQ0RCU0IKdmZ2ckRWUzVpZXAwd2h2SGlBUEdjeWV6bjd0U2RweUZ0NEU0QTNXT3VQOXhqenNjTFZyb1pzRmVMUWlqT1JhUwp3UUloQU84MWl2b21iVGhjRkltTFZPbU16Vk52TGxWTW02WE5iS3B4bGh4TlpUTmhBaUVBbWRISlpGM3haWFE0Cm15QnNCeEhLQ3JqOTF6bVFxU0E4bHUvT1ZNTDNSak1DSVFEbDJxOUdtN0lMbS85b0EyaCtXdnZabGxZUlJPR3oKT21lV2lEclR5MUxaUVFJZ2ZGYUlaUWxMU0tkWjJvdXF4MHdwOWVEejBEWklLVzVWaSt6czdMZHRDdUVDSUVGYwo3d21VZ3pPblpzbnU1clBsTDJjZldLTGhFbWwrUVFzOCtkMFBGdXlnCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t 20 | JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVgppUFpSclRaY3d5eEVYVURqTWhWbi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ== -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv/ 3 | # .env -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | docker-compose up -d 3 | 4 | dev-down: 5 | docker-compose down 6 | 7 | start-server: 8 | uvicorn app.main:app --reload 9 | 10 | install-modules: 11 | pip install fastapi[all] fastapi-mail fastapi-jwt-auth[asymmetric] passlib[bcrypt] pymongo -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcodevo/fastapi_mongodb/b4b43fa002c3e6b8e8400c9426219fd33c5e184f/app/__init__.py -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings, EmailStr 2 | 3 | 4 | class Settings(BaseSettings): 5 | DATABASE_URL: str 6 | MONGO_INITDB_DATABASE: str 7 | 8 | JWT_PUBLIC_KEY: str 9 | JWT_PRIVATE_KEY: str 10 | REFRESH_TOKEN_EXPIRES_IN: int 11 | ACCESS_TOKEN_EXPIRES_IN: int 12 | JWT_ALGORITHM: str 13 | 14 | CLIENT_ORIGIN: str 15 | 16 | EMAIL_HOST: str 17 | EMAIL_PORT: int 18 | EMAIL_USERNAME: str 19 | EMAIL_PASSWORD: str 20 | EMAIL_FROM: EmailStr 21 | 22 | class Config: 23 | env_file = './.env' 24 | 25 | 26 | settings = Settings() 27 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | from pymongo import mongo_client 2 | import pymongo 3 | from app.config import settings 4 | 5 | client = mongo_client.MongoClient(settings.DATABASE_URL) 6 | print('Connected to MongoDB...') 7 | 8 | db = client[settings.MONGO_INITDB_DATABASE] 9 | User = db.users 10 | Post = db.posts 11 | User.create_index([("email", pymongo.ASCENDING)], unique=True) 12 | Post.create_index([("title", pymongo.ASCENDING)], unique=True) 13 | -------------------------------------------------------------------------------- /app/email.py: -------------------------------------------------------------------------------- 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 ' 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_STARTTLS=False, 35 | MAIL_SSL_TLS=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') 62 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from app.config import settings 5 | from app.routers import auth, user, post 6 | 7 | app = FastAPI() 8 | 9 | origins = [ 10 | settings.CLIENT_ORIGIN, 11 | ] 12 | 13 | app.add_middleware( 14 | CORSMiddleware, 15 | allow_origins=origins, 16 | allow_credentials=True, 17 | allow_methods=["*"], 18 | allow_headers=["*"], 19 | ) 20 | 21 | 22 | app.include_router(auth.router, tags=['Auth'], prefix='/api/auth') 23 | app.include_router(user.router, tags=['Users'], prefix='/api/users') 24 | app.include_router(post.router, tags=['Posts'], prefix='/api/posts') 25 | 26 | 27 | @app.get("/api/healthchecker") 28 | def root(): 29 | return {"message": "Welcome to FastAPI with MongoDB"} 30 | -------------------------------------------------------------------------------- /app/oauth2.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from typing import List 3 | from fastapi import Depends, HTTPException, status 4 | from fastapi_jwt_auth import AuthJWT 5 | from pydantic import BaseModel 6 | from bson.objectid import ObjectId 7 | 8 | from app.serializers.userSerializers import userEntity 9 | 10 | from .database import User 11 | from .config import settings 12 | 13 | 14 | class Settings(BaseModel): 15 | authjwt_algorithm: str = settings.JWT_ALGORITHM 16 | authjwt_decode_algorithms: List[str] = [settings.JWT_ALGORITHM] 17 | authjwt_token_location: set = {'cookies', 'headers'} 18 | authjwt_access_cookie_key: str = 'access_token' 19 | authjwt_refresh_cookie_key: str = 'refresh_token' 20 | authjwt_cookie_csrf_protect: bool = False 21 | authjwt_public_key: str = base64.b64decode( 22 | settings.JWT_PUBLIC_KEY).decode('utf-8') 23 | authjwt_private_key: str = base64.b64decode( 24 | settings.JWT_PRIVATE_KEY).decode('utf-8') 25 | 26 | 27 | @AuthJWT.load_config 28 | def get_config(): 29 | return Settings() 30 | 31 | 32 | class NotVerified(Exception): 33 | pass 34 | 35 | 36 | class UserNotFound(Exception): 37 | pass 38 | 39 | 40 | def require_user(Authorize: AuthJWT = Depends()): 41 | try: 42 | Authorize.jwt_required() 43 | user_id = Authorize.get_jwt_subject() 44 | user = userEntity(User.find_one({'_id': ObjectId(str(user_id))})) 45 | 46 | if not user: 47 | raise UserNotFound('User no longer exist') 48 | 49 | if not user["verified"]: 50 | raise NotVerified('You are not verified') 51 | 52 | except Exception as e: 53 | error = e.__class__.__name__ 54 | print(error) 55 | if error == 'MissingTokenError': 56 | raise HTTPException( 57 | status_code=status.HTTP_401_UNAUTHORIZED, detail='You are not logged in') 58 | if error == 'UserNotFound': 59 | raise HTTPException( 60 | status_code=status.HTTP_401_UNAUTHORIZED, detail='User no longer exist') 61 | if error == 'NotVerified': 62 | raise HTTPException( 63 | status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your account') 64 | raise HTTPException( 65 | status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired') 66 | return user_id 67 | -------------------------------------------------------------------------------- /app/routers/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import hashlib 3 | from random import randbytes 4 | from bson.objectid import ObjectId 5 | from fastapi import APIRouter, Request, Response, status, Depends, HTTPException 6 | from pydantic import EmailStr 7 | 8 | from app import oauth2 9 | from app.database import User 10 | from app.email import Email 11 | from app.serializers.userSerializers import userEntity 12 | from .. import schemas, utils 13 | from app.oauth2 import AuthJWT 14 | from ..config import settings 15 | 16 | 17 | router = APIRouter() 18 | ACCESS_TOKEN_EXPIRES_IN = settings.ACCESS_TOKEN_EXPIRES_IN 19 | REFRESH_TOKEN_EXPIRES_IN = settings.REFRESH_TOKEN_EXPIRES_IN 20 | 21 | 22 | @router.post('/register', status_code=status.HTTP_201_CREATED) 23 | async def create_user(payload: schemas.CreateUserSchema, request: Request): 24 | # Check if user already exist 25 | user = User.find_one({'email': payload.email.lower()}) 26 | if user: 27 | raise HTTPException(status_code=status.HTTP_409_CONFLICT, 28 | detail='Account already exist') 29 | # Compare password and passwordConfirm 30 | if payload.password != payload.passwordConfirm: 31 | raise HTTPException( 32 | status_code=status.HTTP_400_BAD_REQUEST, detail='Passwords do not match') 33 | # Hash the password 34 | payload.password = utils.hash_password(payload.password) 35 | del payload.passwordConfirm 36 | payload.role = 'user' 37 | payload.verified = False 38 | payload.email = payload.email.lower() 39 | payload.created_at = datetime.utcnow() 40 | payload.updated_at = payload.created_at 41 | 42 | result = User.insert_one(payload.dict()) 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'} 60 | 61 | 62 | @router.post('/login') 63 | def login(payload: schemas.LoginUserSchema, response: Response, Authorize: AuthJWT = Depends()): 64 | # Check if the user exist 65 | db_user = User.find_one({'email': payload.email.lower()}) 66 | if not db_user: 67 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 68 | detail='Incorrect Email or Password') 69 | user = userEntity(db_user) 70 | 71 | # Check if user verified his email 72 | if not user['verified']: 73 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 74 | detail='Please verify your email address') 75 | 76 | # Check if the password is valid 77 | if not utils.verify_password(payload.password, user['password']): 78 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 79 | detail='Incorrect Email or Password') 80 | 81 | # Create access token 82 | access_token = Authorize.create_access_token( 83 | subject=str(user["id"]), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN)) 84 | 85 | # Create refresh token 86 | refresh_token = Authorize.create_refresh_token( 87 | subject=str(user["id"]), expires_time=timedelta(minutes=REFRESH_TOKEN_EXPIRES_IN)) 88 | 89 | # Store refresh and access tokens in cookie 90 | response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, 91 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') 92 | response.set_cookie('refresh_token', refresh_token, 93 | REFRESH_TOKEN_EXPIRES_IN * 60, REFRESH_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') 94 | response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, 95 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') 96 | 97 | # Send both access 98 | return {'status': 'success', 'access_token': access_token} 99 | 100 | 101 | @router.get('/refresh') 102 | def refresh_token(response: Response, Authorize: AuthJWT = Depends()): 103 | try: 104 | Authorize.jwt_refresh_token_required() 105 | 106 | user_id = Authorize.get_jwt_subject() 107 | if not user_id: 108 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 109 | detail='Could not refresh access token') 110 | user = userEntity(User.find_one({'_id': ObjectId(str(user_id))})) 111 | if not user: 112 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 113 | detail='The user belonging to this token no logger exist') 114 | access_token = Authorize.create_access_token( 115 | subject=str(user["id"]), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN)) 116 | except Exception as e: 117 | error = e.__class__.__name__ 118 | if error == 'MissingTokenError': 119 | raise HTTPException( 120 | status_code=status.HTTP_400_BAD_REQUEST, detail='Please provide refresh token') 121 | raise HTTPException( 122 | status_code=status.HTTP_400_BAD_REQUEST, detail=error) 123 | 124 | response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, 125 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') 126 | response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, 127 | ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') 128 | return {'access_token': access_token} 129 | 130 | 131 | @router.get('/logout', status_code=status.HTTP_200_OK) 132 | def logout(response: Response, Authorize: AuthJWT = Depends(), user_id: str = Depends(oauth2.require_user)): 133 | Authorize.unset_jwt_cookies() 134 | response.set_cookie('logged_in', '', -1) 135 | 136 | return {'status': 'success'} 137 | 138 | 139 | @router.get('/verifyemail/{token}') 140 | def verify_me(token: str): 141 | hashedCode = hashlib.sha256() 142 | hashedCode.update(bytes.fromhex(token)) 143 | verification_code = hashedCode.hexdigest() 144 | result = User.find_one_and_update({"verification_code": verification_code}, { 145 | "$set": {"verification_code": None, "verified": True, "updated_at": datetime.utcnow()}}, new=True) 146 | if not result: 147 | raise HTTPException( 148 | status_code=status.HTTP_403_FORBIDDEN, detail='Invalid verification code or account already verified') 149 | return { 150 | "status": "success", 151 | "message": "Account verified successfully" 152 | } 153 | -------------------------------------------------------------------------------- /app/routers/post.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from fastapi import Depends, HTTPException, status, APIRouter, Response 3 | from pymongo.collection import ReturnDocument 4 | from app import schemas 5 | from app.database import Post 6 | from app.oauth2 import require_user 7 | from app.serializers.postSerializers import postEntity, postListEntity 8 | from bson.objectid import ObjectId 9 | from pymongo.errors import DuplicateKeyError 10 | 11 | router = APIRouter() 12 | 13 | 14 | @router.get('/') 15 | def get_posts(limit: int = 10, page: int = 1, search: str = '', user_id: str = Depends(require_user)): 16 | skip = (page - 1) * limit 17 | pipeline = [ 18 | {'$match': {}}, 19 | {'$lookup': {'from': 'users', 'localField': 'user', 20 | 'foreignField': '_id', 'as': 'user'}}, 21 | {'$unwind': '$user'}, 22 | { 23 | '$skip': skip 24 | }, { 25 | '$limit': limit 26 | } 27 | ] 28 | posts = postListEntity(Post.aggregate(pipeline)) 29 | return {'status': 'success', 'results': len(posts), 'posts': posts} 30 | 31 | 32 | @router.post('/', status_code=status.HTTP_201_CREATED) 33 | def create_post(post: schemas.CreatePostSchema, user_id: str = Depends(require_user)): 34 | post.user = ObjectId(user_id) 35 | post.created_at = datetime.utcnow() 36 | post.updated_at = post.created_at 37 | try: 38 | result = Post.insert_one(post.dict()) 39 | pipeline = [ 40 | {'$match': {'_id': result.inserted_id}}, 41 | {'$lookup': {'from': 'users', 'localField': 'user', 42 | 'foreignField': '_id', 'as': 'user'}}, 43 | {'$unwind': '$user'}, 44 | ] 45 | new_post = postListEntity(Post.aggregate(pipeline))[0] 46 | return new_post 47 | except DuplicateKeyError: 48 | raise HTTPException(status_code=status.HTTP_409_CONFLICT, 49 | detail=f"Post with title: '{post.title}' already exists") 50 | 51 | 52 | @router.put('/{id}') 53 | def update_post(id: str, payload: schemas.UpdatePostSchema, user_id: str = Depends(require_user)): 54 | if not ObjectId.is_valid(id): 55 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 56 | detail=f"Invalid id: {id}") 57 | updated_post = Post.find_one_and_update( 58 | {'_id': ObjectId(id)}, {'$set': payload.dict(exclude_none=True)}, return_document=ReturnDocument.AFTER) 59 | if not updated_post: 60 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, 61 | detail=f'No post with this id: {id} found') 62 | return postEntity(updated_post) 63 | 64 | 65 | @router.get('/{id}') 66 | def get_post(id: str, user_id: str = Depends(require_user)): 67 | if not ObjectId.is_valid(id): 68 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 69 | detail=f"Invalid id: {id}") 70 | pipeline = [ 71 | {'$match': {'_id': ObjectId(id)}}, 72 | {'$lookup': {'from': 'users', 'localField': 'user', 73 | 'foreignField': '_id', 'as': 'user'}}, 74 | {'$unwind': '$user'}, 75 | ] 76 | db_cursor = Post.aggregate(pipeline) 77 | results = list(db_cursor) 78 | 79 | if len(results) == 0: 80 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, 81 | detail=f"No post with this id: {id} found") 82 | 83 | post = postListEntity(results)[0] 84 | return post 85 | 86 | 87 | @router.delete('/{id}') 88 | def delete_post(id: str, user_id: str = Depends(require_user)): 89 | if not ObjectId.is_valid(id): 90 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 91 | detail=f"Invalid id: {id}") 92 | post = Post.find_one_and_delete({'_id': ObjectId(id)}) 93 | if not post: 94 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, 95 | detail=f'No post with this id: {id} found') 96 | return Response(status_code=status.HTTP_204_NO_CONTENT) 97 | -------------------------------------------------------------------------------- /app/routers/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from bson.objectid import ObjectId 3 | from app.serializers.userSerializers import userResponseEntity 4 | 5 | from app.database import User 6 | from .. import schemas, oauth2 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.get('/me', response_model=schemas.UserResponse) 12 | def get_me(user_id: str = Depends(oauth2.require_user)): 13 | user = userResponseEntity(User.find_one({'_id': ObjectId(str(user_id))})) 14 | return {"status": "success", "user": user} 15 | -------------------------------------------------------------------------------- /app/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | from pydantic import BaseModel, EmailStr, constr 4 | from bson.objectid import ObjectId 5 | 6 | 7 | class UserBaseSchema(BaseModel): 8 | name: str 9 | email: str 10 | photo: str 11 | role: str | None = None 12 | created_at: datetime | None = None 13 | updated_at: datetime | None = None 14 | 15 | class Config: 16 | orm_mode = True 17 | 18 | 19 | class CreateUserSchema(UserBaseSchema): 20 | password: constr(min_length=8) 21 | passwordConfirm: str 22 | verified: bool = False 23 | 24 | 25 | class LoginUserSchema(BaseModel): 26 | email: EmailStr 27 | password: constr(min_length=8) 28 | 29 | 30 | class UserResponseSchema(UserBaseSchema): 31 | id: str 32 | pass 33 | 34 | 35 | class UserResponse(BaseModel): 36 | status: str 37 | user: UserResponseSchema 38 | 39 | 40 | class FilteredUserResponse(UserBaseSchema): 41 | id: str 42 | 43 | 44 | class PostBaseSchema(BaseModel): 45 | title: str 46 | content: str 47 | category: str 48 | image: str 49 | created_at: datetime | None = None 50 | updated_at: datetime | None = None 51 | 52 | class Config: 53 | orm_mode = True 54 | allow_population_by_field_name = True 55 | arbitrary_types_allowed = True 56 | json_encoders = {ObjectId: str} 57 | 58 | 59 | class CreatePostSchema(PostBaseSchema): 60 | user: ObjectId | None = None 61 | pass 62 | 63 | 64 | class PostResponse(PostBaseSchema): 65 | id: str 66 | user: FilteredUserResponse 67 | created_at: datetime 68 | updated_at: datetime 69 | 70 | 71 | class UpdatePostSchema(BaseModel): 72 | title: str | None = None 73 | content: str | None = None 74 | category: str | None = None 75 | image: str | None = None 76 | user: str | None = None 77 | 78 | class Config: 79 | orm_mode = True 80 | allow_population_by_field_name = True 81 | arbitrary_types_allowed = True 82 | json_encoders = {ObjectId: str} 83 | 84 | 85 | class ListPostResponse(BaseModel): 86 | status: str 87 | results: int 88 | posts: List[PostResponse] 89 | -------------------------------------------------------------------------------- /app/serializers/postSerializers.py: -------------------------------------------------------------------------------- 1 | from app.serializers.userSerializers import embeddedUserResponse 2 | 3 | 4 | def postEntity(post) -> dict: 5 | return { 6 | "id": str(post["_id"]), 7 | "title": post["title"], 8 | "category": post["category"], 9 | "content": post["content"], 10 | "image": post["image"], 11 | "user": str(post["user"]), 12 | "created_at": post["created_at"], 13 | "updated_at": post["updated_at"] 14 | } 15 | 16 | 17 | def populatedPostEntity(post) -> dict: 18 | return { 19 | "id": str(post["_id"]), 20 | "title": post["title"], 21 | "category": post["category"], 22 | "content": post["content"], 23 | "image": post["image"], 24 | "user": embeddedUserResponse(post["user"]), 25 | "created_at": post["created_at"], 26 | "updated_at": post["updated_at"] 27 | } 28 | 29 | 30 | def postListEntity(posts) -> list: 31 | return [populatedPostEntity(post) for post in posts] 32 | -------------------------------------------------------------------------------- /app/serializers/userSerializers.py: -------------------------------------------------------------------------------- 1 | def userEntity(user) -> dict: 2 | return { 3 | "id": str(user["_id"]), 4 | "name": user["name"], 5 | "email": user["email"], 6 | "role": user["role"], 7 | "photo": user["photo"], 8 | "verified": user["verified"], 9 | "password": user["password"], 10 | "created_at": user["created_at"], 11 | "updated_at": user["updated_at"] 12 | } 13 | 14 | 15 | def userResponseEntity(user) -> dict: 16 | return { 17 | "id": str(user["_id"]), 18 | "name": user["name"], 19 | "email": user["email"], 20 | "role": user["role"], 21 | "photo": user["photo"], 22 | "created_at": user["created_at"], 23 | "updated_at": user["updated_at"] 24 | } 25 | 26 | 27 | def embeddedUserResponse(user) -> dict: 28 | return { 29 | "id": str(user["_id"]), 30 | "name": user["name"], 31 | "email": user["email"], 32 | "photo": user["photo"] 33 | } 34 | 35 | 36 | def userListEntity(users) -> list: 37 | return [userEntity(user) for user in users] 38 | -------------------------------------------------------------------------------- /app/templates/_styles.html: -------------------------------------------------------------------------------- 1 | 330 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{subject}} 7 | {% include '_styles.html' %} 8 | 9 | 10 | 17 | 18 | 19 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/templates/verification.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} {% block content %} 2 | 3 |

Hi {{first_name}},

4 |

5 | Thanks for creating an account with us. Please verify your email address by 6 | clicking the button below. 7 |

8 | 15 | 16 | 17 | 28 | 29 | 30 | 31 |

Good luck! Hope it works.

32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 4 | 5 | 6 | def hash_password(password: str): 7 | return pwd_context.hash(password) 8 | 9 | 10 | def verify_password(password: str, hashed_password: str): 11 | return pwd_context.verify(password, hashed_password) 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongo: 4 | image: mongo:latest 5 | container_name: mongo 6 | env_file: 7 | - ./.env 8 | environment: 9 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} 10 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} 11 | MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE} 12 | volumes: 13 | - mongo:/data/db 14 | ports: 15 | - '6000:27017' 16 | 17 | volumes: 18 | mongo: 19 | -------------------------------------------------------------------------------- /readMe.md: -------------------------------------------------------------------------------- 1 | # RESTful API with Python, FastAPI, Pydantic, and MongoDB 2 | 3 | ## 1. API with Python, FastAPI, and MongoDB: JWT Authentication 4 | 5 | This article will teach you how to add JSON Web Token (JWT) authentication to your FastAPI app using PyMongo, Pydantic, FastAPI JWT Auth package, and Docker-compose. 6 | 7 | ![API with Python, FastAPI, and MongoDB: JWT Authentication](https://codevoweb.com/wp-content/uploads/2022/07/API-with-Python-FastAPI-and-MongoDB-JWT-Authentication.webp) 8 | 9 | ### Topics Covered 10 | 11 | - How to Setup FastAPI with MongoDB 12 | - Starting the FastAPI Server 13 | - Set up Environment Variables with Pydantic 14 | - Connect to the MongoDB Database 15 | - Creating the Schemas with Pydantic 16 | - Create Serializers for the MongoDB BSON Documents 17 | - Password Management in FastAPI 18 | - Creating Utility Functions to Sign and Verify JWTs 19 | - Creating the Authentication Controllers in FastAPI 20 | - User Registration Handler 21 | - User Sign-in Handler 22 | - Refresh Access Token Handler 23 | - Sign out User Handler 24 | - How to Protect Private Routes 25 | - Creating a User Handler 26 | - Adding the API Routes and CORS 27 | - Testing the API with Postman 28 | 29 | Read the entire article here: [https://codevoweb.com/api-with-python-fastapi-and-mongodb-jwt-authentication](https://codevoweb.com/api-with-python-fastapi-and-mongodb-jwt-authentication) 30 | 31 | ## 2. Build API with Python & FastAPI: SignUp User and Verify Email 32 | 33 | This article will teach you how to send HTML Emails with Python, FastAPI, PyMongo, MongoDB, Jinja2, and Docker. Also, you will learn how to use Jinja2 to generate different HTML templates. 34 | 35 | ![Build API with Python & FastAPI: SignUp User and Verify Email](https://codevoweb.com/wp-content/uploads/2022/07/Build-API-with-Python-FastAPI-SignUp-User-and-Verify-Email.webp) 36 | 37 | ### Topics Covered 38 | 39 | - Send HTML Emails with Jinja2 & FastAPI Example 40 | - Creating the SMTP Provider Account 41 | - Edit the Environment Variables File 42 | - Validating the Environment Variables with Pydantic 43 | - Creating the HTML Email Templates in FastAPI 44 | - Creating the SMTP Email Sender 45 | - Sending the HTML Emails in FastAPI 46 | - Update the SignUp Controller 47 | - Create a Handler to Validate the Verification Code 48 | 49 | Read the entire article here: [https://codevoweb.com/api-with-python-fastapi-signup-user-and-verify-email](https://codevoweb.com/api-with-python-fastapi-signup-user-and-verify-email) 50 | 51 | ## 3. CRUD RESTful API Server with Python, FastAPI, and MongoDB 52 | 53 | This article will teach you how to create a CRUD RESTful API with Python, FastAPI, PyMongo, MongoDB, and Docker-compose to perform the basic Create/Read/Update/Delete operations against a database. 54 | 55 | ![CRUD RESTful API Server with Python, FastAPI, and MongoDB](https://codevoweb.com/wp-content/uploads/2022/07/CRUD-RESTful-API-Server-with-Python-FastAPI-and-MongoDB.webp) 56 | 57 | ### Topics Covered 58 | 59 | - Python, FastAPI, MongoDB CRUD API Overview 60 | - Setting up FastAPI with MongoDB 61 | - Installing FastAPI 62 | - Running the FastAPI Server 63 | - Loading Environment Variables with Pydantic 64 | - Connecting to the MongoDB Database Server 65 | - Creating the Schemas with Pydantic 66 | - Serializers for the MongoDB Documents 67 | - Creating the API Route Controllers 68 | - Get All Posts Controller 69 | - Create New Post Controller 70 | - Update Post Controller 71 | - Get Single Post Controller 72 | - Delete Post Controller 73 | - Add the Routes to the FastAPI Middleware Stack 74 | 75 | Read the entire article here: [https://codevoweb.com/crud-restful-api-server-with-python-fastapi-and-mongodb](https://codevoweb.com/crud-restful-api-server-with-python-fastapi-and-mongodb) 76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiosmtplib==2.0.0 2 | anyio==3.6.2 3 | bcrypt==4.0.1 4 | black==22.12.0 5 | blinker==1.5 6 | certifi==2022.12.7 7 | cffi==1.15.1 8 | click==8.1.3 9 | colorama==0.4.6 10 | cryptography==3.4.8 11 | dnspython==2.2.1 12 | email-validator==1.3.0 13 | fastapi==0.88.0 14 | fastapi-jwt-auth==0.5.0 15 | fastapi-mail==1.2.4 16 | h11==0.14.0 17 | httpcore==0.16.3 18 | httptools==0.5.0 19 | httpx==0.23.1 20 | idna==3.4 21 | itsdangerous==2.1.2 22 | Jinja2==3.1.2 23 | MarkupSafe==2.1.1 24 | mypy-extensions==0.4.3 25 | orjson==3.8.3 26 | passlib==1.7.4 27 | pathspec==0.10.3 28 | platformdirs==2.6.2 29 | pycparser==2.21 30 | pydantic==1.10.3 31 | PyJWT==1.7.1 32 | pymongo==4.3.3 33 | python-dotenv==0.21.0 34 | python-multipart==0.0.5 35 | PyYAML==6.0 36 | rfc3986==1.5.0 37 | six==1.16.0 38 | sniffio==1.3.0 39 | starlette==0.22.0 40 | typing_extensions==4.4.0 41 | ujson==5.6.0 42 | uvicorn==0.20.0 43 | watchfiles==0.18.1 44 | websockets==10.4 45 | --------------------------------------------------------------------------------