├── .github └── workflows │ └── build-deploy.yaml ├── .gitignore ├── Dockerfile ├── Procfile ├── api ├── __init__.py ├── main.py ├── oauth2.py ├── routes │ ├── __init__.py │ ├── auth.py │ ├── blog_content.py │ ├── password_reset.py │ └── users.py ├── schemas.py ├── send_email.py ├── templates │ ├── email.html │ └── password_reset.html └── utils.py ├── docker-compose-dev.yml ├── docker-compose-prod.yml └── requirements.txt /.github/workflows/build-deploy.yaml: -------------------------------------------------------------------------------- 1 | # this can be any name 2 | name: BlogAPI test 3 | 4 | # specify the on which action this action should work on 5 | on: 6 | # specify the branches this action works on 7 | push: 8 | # this is also a valid syntax 9 | # branches: ["master", "main"] 10 | branches: 11 | - "main" 12 | - "master" 13 | 14 | jobs: 15 | # job name can be anything 16 | build: 17 | # specify the OS to run on. 18 | runs-on: ubuntu-latest 19 | # steps, things done. Each step has a name that is used to identify the step 20 | steps: 21 | - name: Pull repo 22 | # https://github.com/orgs/actions/repositories?type=all 23 | uses: actions/checkout@v3 24 | # - name: say hello 25 | # run: echo "hello world" 26 | 27 | # install python3 28 | - name: Install Python 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: "3.9" 32 | - name: Upgrade Pip 33 | run: python -m pip install --upgrade pip 34 | - name: Install all dependencies 35 | run: pip install -r requirements.txt 36 | 37 | heriku-deployment: 38 | runs-on: ubuntu-latest 39 | # cause git actions runs jobs in parallel 40 | needs: [build] 41 | 42 | # add environment variables 43 | environment: 44 | name: heroku_production 45 | # doing things manually 46 | # pull git repo 47 | # install heroku cli 48 | # heroku login 49 | # add git remote to heroku 50 | # git push heroku master 51 | 52 | steps: 53 | - name: Get actions 54 | uses: actions/checkout@v3 55 | - name: Deploying to heroku 56 | uses: akhileshns/heroku-deploy@v3.12.12 57 | with: 58 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 59 | heroku_app_name: ${{secrets.APP_NAME}} 60 | heroku_email: ${{secrets.MY_EMAIL}} 61 | # sometimes deploying the code can cause work but, the app crashes on herok 62 | # health check can alert you on this. 63 | # https://github.com/marketplace/actions/deploy-to-heroku 64 | # healthcheck: "https://${{secrets.APP_NAME}}.herokuapp.com/health" 65 | 66 | # https://docs.docker.com/language/golang/configure-ci-cd/ 67 | docker-deployment: 68 | runs-on: ubuntu-latest 69 | 70 | # specify the environment to us 71 | environment: 72 | name: heroku_production 73 | 74 | steps: 75 | - name: Login to Docker Hub 76 | uses: docker/login-action@v1 77 | with: 78 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 79 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 80 | 81 | - name: Set up Docker Buildx 82 | id: buildx 83 | uses: docker/setup-buildx-action@v1 84 | 85 | - name: Cache Docker layers 86 | uses: actions/cache@v2 87 | with: 88 | path: /tmp/.buildx-cache 89 | key: ${{ runner.os }}-buildx-${{ github.sha }} 90 | restore-keys: | 91 | ${{ runner.os }}-buildx- 92 | 93 | - name: Build and push 94 | id: docker_build 95 | uses: docker/build-push-action@v2 96 | with: 97 | builder: ${{ steps.buildx.outputs.name }} 98 | push: false 99 | load: true 100 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/blogapi:latest 101 | cache-from: type=local,src=/tmp/.buildx-cache 102 | cache-to: type=local,dest=/tmp/.buildx-cache 103 | 104 | - name: Image digest 105 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.12 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | # this is to help prevent installing requirements everytime we update our 8 | # source code, this is cause docker has a caching system. 9 | COPY . . 10 | 11 | # uvicorn app.main:app --host 0.0.0.0 --port 8000 12 | CMD [ "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000" ] 13 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn api.main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Princekrampah/Mongodb_fastapi_blog_API/c2ba6b619d2e8d3706f4305d24fbfd629d741c90/api/__init__.py -------------------------------------------------------------------------------- /api/main.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | from fastapi import FastAPI 3 | from fastapi.middleware.cors import CORSMiddleware 4 | 5 | # module imports 6 | from .routes import blog_content,users, auth, password_reset 7 | 8 | # initialize an app 9 | app = FastAPI() 10 | 11 | # Handle CORS protection 12 | origins = ["*"] 13 | 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_origins=origins, 17 | allow_credentials=True, 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | ) 21 | 22 | 23 | # register all the router endpoint 24 | app.include_router(blog_content.router) 25 | app.include_router(users.router) 26 | app.include_router(auth.router) 27 | app.include_router(password_reset.router) 28 | 29 | 30 | @app.get("/") 31 | def get(): 32 | return {"msg": "Hello world"} -------------------------------------------------------------------------------- /api/oauth2.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | from typing import Dict 3 | from jose import JWTError, jwt 4 | from datetime import datetime, timedelta 5 | from fastapi import Depends, status, HTTPException 6 | from fastapi.security import OAuth2PasswordBearer 7 | from dotenv import load_dotenv 8 | import os 9 | 10 | load_dotenv() 11 | 12 | # module imports 13 | from .schemas import TokenData, db 14 | 15 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') 16 | 17 | # get config values 18 | SECRET_KEY = os.getenv("SECRET_KEY") 19 | ALGORITHM = os.getenv("ALGORITHM") 20 | ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")) 21 | 22 | 23 | def create_access_token(payload: Dict): 24 | to_encode = payload.copy() 25 | expiration_time = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 26 | to_encode.update({"exp": expiration_time}) 27 | 28 | jw_token = jwt.encode(to_encode, key=SECRET_KEY, algorithm=ALGORITHM) 29 | 30 | return jw_token 31 | 32 | 33 | def verify_access_token(token: str, credential_exception: Dict): 34 | try: 35 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 36 | 37 | id: str = payload.get("id") 38 | 39 | if not id: 40 | raise credential_exception 41 | 42 | token_data = TokenData(id=id) 43 | return token_data 44 | except JWTError: 45 | raise credential_exception 46 | 47 | 48 | async def get_current_user(token: str = Depends(oauth2_scheme)): 49 | credential_exception = HTTPException( 50 | status_code=status.HTTP_401_UNAUTHORIZED, 51 | detail="Could not verify token, token expired", 52 | headers={"WWW-AUTHENTICATE": "Bearer", } 53 | ) 54 | 55 | current_user_id = verify_access_token( 56 | token=token, credential_exception=credential_exception).id 57 | 58 | current_user = await db["users"].find_one({"_id": current_user_id}) 59 | 60 | return current_user 61 | -------------------------------------------------------------------------------- /api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Princekrampah/Mongodb_fastapi_blog_API/c2ba6b619d2e8d3706f4305d24fbfd629d741c90/api/routes/__init__.py -------------------------------------------------------------------------------- /api/routes/auth.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | from fastapi import APIRouter, Depends, status, HTTPException 3 | from fastapi.security import OAuth2PasswordRequestForm 4 | 5 | # module imports 6 | from ..schemas import db, Token 7 | from .. import utils 8 | from .. import oauth2 9 | 10 | 11 | router = APIRouter( 12 | prefix="/login", 13 | tags=["Authentication"] 14 | ) 15 | 16 | 17 | @router.post("", response_model=Token, status_code=status.HTTP_200_OK) 18 | async def login(user_credentials: OAuth2PasswordRequestForm = Depends()): 19 | 20 | user = await db["users"].find_one({"name": user_credentials.username}) 21 | 22 | if user and utils.verify_password(user_credentials.password, user["password"]): 23 | access_token = oauth2.create_access_token(payload={ 24 | "id": user["_id"], 25 | }) 26 | return {"access_token": access_token, "token_type": "bearer"} 27 | else: 28 | raise HTTPException( 29 | status_code=status.HTTP_403_FORBIDDEN, 30 | detail="Invalid user credentials" 31 | ) 32 | -------------------------------------------------------------------------------- /api/routes/blog_content.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | from fastapi import APIRouter, HTTPException, status, Depends 3 | from fastapi.encoders import jsonable_encoder 4 | from fastapi.responses import JSONResponse 5 | from datetime import datetime 6 | from typing import List 7 | 8 | # module imports 9 | from ..schemas import BlogContent, BlogContentResponse, db 10 | from .. import oauth2 11 | 12 | router = APIRouter( 13 | prefix="/blog", 14 | tags=["Blog Content"] 15 | ) 16 | 17 | 18 | @router.post("/", response_description="Create Post Content", response_model=BlogContentResponse) 19 | async def read_item(blog_content: BlogContent, current_user=Depends(oauth2.get_current_user)): 20 | 21 | try: 22 | # jsonize the data 23 | blog_content = jsonable_encoder(blog_content) 24 | 25 | # add the additional info 26 | blog_content["auther_name"] = current_user["name"] 27 | blog_content["auther_id"] = current_user["_id"] 28 | blog_content["created_at"] = str(datetime.utcnow()) 29 | 30 | # create blogPost collection 31 | new_blog_content = await db["blogPost"].insert_one(blog_content) 32 | 33 | # get created post content 34 | created_blog_post = await db["blogPost"].find_one({"_id": new_blog_content.inserted_id}) 35 | 36 | print(new_blog_content.inserted_id) 37 | return created_blog_post 38 | 39 | except Exception as e: 40 | print(e) 41 | raise HTTPException( 42 | status_code=500, 43 | detail="Internal server error" 44 | ) 45 | 46 | 47 | @router.get("/", response_description="Get Blog Posts", response_model= List[BlogContentResponse]) 48 | async def get_blog_posts(limit: int = 4, orderby: str = "created_at"): 49 | try: 50 | blog_posts = await db["blogPost"].find({ "$query": {}, "$orderby": { orderby : -1 } }).to_list(limit) 51 | return blog_posts 52 | except Exception as e: 53 | print(e) 54 | raise HTTPException( 55 | status_code=500, 56 | detail="Internal server error" 57 | ) 58 | 59 | 60 | @router.get("/{id}", response_description="Get Blog Post", response_model= BlogContentResponse) 61 | async def get_blog_post(id: str): 62 | try: 63 | blog_post = await db["blogPost"].find_one({"_id": id}) 64 | return blog_post 65 | except Exception as e: 66 | print(e) 67 | raise HTTPException( 68 | status_code=500, 69 | detail="Internal server error" 70 | ) 71 | 72 | 73 | @router.put("/{id}", response_description="Update a blog Post", response_model=BlogContentResponse) 74 | async def update_blog_post(id: str, blog_content: BlogContent, current_user = Depends(oauth2.get_current_user)): 75 | 76 | if blog_post := await db["blogPost"].find_one({"_id": id}): 77 | print(blog_post) 78 | # check if the owner is the currently logged in user 79 | if blog_post["auther_id"] == current_user["_id"]: 80 | print("owner") 81 | try: 82 | blog_content = {k: v for k, v in blog_content.dict().items() if v is not None} 83 | 84 | if len(blog_content) >= 1: 85 | update_result = await db["blogPost"].update_one({"_id": id}, {"$set": blog_content}) 86 | 87 | if update_result.modified_count == 1: 88 | if (updated_blog_post := await db["blogPost"].find_one({"_id": id})) is not None: 89 | return updated_blog_post 90 | 91 | if (existing_blog_post := await db["blogPost"].find_one({"_id": id})) is not None: 92 | return existing_blog_post 93 | 94 | raise HTTPException(status_code=404, detail=f"Blog Post {id} not found") 95 | 96 | except Exception as e: 97 | print(e) 98 | raise HTTPException( 99 | status_code=500, 100 | detail="Internal server error" 101 | ) 102 | else: 103 | raise HTTPException(status_code=403, detail=f"You are not the owner of this blog post") 104 | 105 | 106 | @router.delete("/{id}", response_description="Get Blog Post", ) 107 | async def get_blog_post(id: str, current_user = Depends(oauth2.get_current_user)): 108 | 109 | if blog_post := await db["blogPost"].find_one({"_id": id}): 110 | 111 | # check if the owner is the currently logged in user 112 | if blog_post["auther_id"] == current_user["_id"]: 113 | try: 114 | delete_result = await db["blogPost"].delete_one({"_id": id}) 115 | 116 | if delete_result.deleted_count == 1: 117 | return JSONResponse(status_code=status.HTTP_204_NO_CONTENT) 118 | 119 | raise HTTPException(status_code=404, detail=f"Blog Post {id} not found") 120 | 121 | except Exception as e: 122 | print(e) 123 | raise HTTPException( 124 | status_code=500, 125 | detail="Internal server error" 126 | ) 127 | else: 128 | raise HTTPException(status_code=403, detail=f"You are not the owner of this blog post") -------------------------------------------------------------------------------- /api/routes/password_reset.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | from fastapi import APIRouter, HTTPException, status 3 | 4 | # module imports 5 | from ..schemas import PasswordReset, PasswordResetRequest, db 6 | from ..send_email import password_reset 7 | from ..oauth2 import create_access_token, get_current_user 8 | from ..utils import get_password_hash 9 | 10 | router = APIRouter( 11 | prefix="/password", 12 | tags=["Password Reset"] 13 | ) 14 | 15 | 16 | @router.post("/request/", response_description="Password reset request") 17 | async def reset_request(user_email: PasswordResetRequest): 18 | user = await db["users"].find_one({"email": user_email.email}) 19 | 20 | print(user) 21 | 22 | if user is not None: 23 | token = create_access_token({"id": user["_id"]}) 24 | 25 | reset_link = f"http://locahost:8000/reset?token={token}" 26 | 27 | print("Hello") 28 | 29 | await password_reset("Password Reset", user["email"], 30 | { 31 | "title": "Password Reset", 32 | "name": user["name"], 33 | "reset_link": reset_link 34 | } 35 | ) 36 | return {"msg": "Email has been sent with instructions to reset your password."} 37 | 38 | else: 39 | raise HTTPException( 40 | status_code=status.HTTP_404_NOT_FOUND, 41 | detail="Your details not found, invalid email address" 42 | ) 43 | 44 | 45 | @router.put("/reset/", response_description="Password reset") 46 | async def reset(token: str, new_password: PasswordReset): 47 | 48 | request_data = {k: v for k, v in new_password.dict().items() 49 | if v is not None} 50 | 51 | # get the hashed version of the password 52 | request_data["password"] = get_password_hash(request_data["password"]) 53 | 54 | if len(request_data) >= 1: 55 | # use token to get the current user 56 | user = await get_current_user(token) 57 | 58 | # update the password of the current user 59 | update_result = await db["users"].update_one({"_id": user["_id"]}, {"$set": request_data}) 60 | 61 | if update_result.modified_count == 1: 62 | # get the newly updated current user and return as a response 63 | updated_student = await db["users"].find_one({"_id": user["_id"]}) 64 | if(updated_student) is not None: 65 | return updated_student 66 | 67 | existing_user = await db["users"].find_one({"_id": user["_id"]}) 68 | if(existing_user) is not None: 69 | return existing_user 70 | 71 | # Raise error if the user can not be found in the database 72 | raise HTTPException(status_code=404, detail=f"User not found") 73 | -------------------------------------------------------------------------------- /api/routes/users.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | from fastapi import APIRouter, Depends, HTTPException, status 3 | from fastapi.encoders import jsonable_encoder 4 | from fastapi.responses import JSONResponse 5 | 6 | # module imports 7 | from api import oauth2 8 | from ..schemas import User, UserResponse, db 9 | from ..utils import get_password_hash 10 | from ..send_email import send_registration_mail 11 | import secrets 12 | 13 | 14 | router = APIRouter( 15 | prefix="/users", 16 | tags=["Users"] 17 | ) 18 | 19 | 20 | @router.post("/registration", response_description="Register New User", response_model=UserResponse, status_code=status.HTTP_201_CREATED) 21 | async def registration(user_info: User): 22 | user_info = jsonable_encoder(user_info) 23 | 24 | # check for duplications 25 | username_found = await db["users"].find_one({"name": user_info["name"]}) 26 | email_found = await db["users"].find_one({"email": user_info["email"]}) 27 | 28 | if username_found: 29 | raise HTTPException(status_code=status.HTTP_409_CONFLICT, 30 | detail="There already is a user by that name") 31 | 32 | if email_found: 33 | raise HTTPException(status_code=status.HTTP_409_CONFLICT, 34 | detail="There already is a user by that email") 35 | 36 | # hash the user password 37 | user_info["password"] = get_password_hash(user_info["password"]) 38 | # generate apiKey 39 | user_info["apiKey"] = secrets.token_hex(20) 40 | new_user = await db["users"].insert_one(user_info) 41 | created_user = await db["users"].find_one({"_id": new_user.inserted_id}) 42 | 43 | # send email 44 | await send_registration_mail("Registration successful", user_info["email"], 45 | { 46 | "title": "Registration successful", 47 | "name": user_info["name"] 48 | } 49 | ) 50 | 51 | return created_user 52 | 53 | 54 | @router.post("/details", response_description="Get user details", response_model=UserResponse) 55 | async def details(current_user=Depends(oauth2.get_current_user)): 56 | user = await db["users"].find_one({"_id": current_user["_id"]}) 57 | return user 58 | -------------------------------------------------------------------------------- /api/schemas.py: -------------------------------------------------------------------------------- 1 | # library imports 2 | import motor.motor_asyncio 3 | from pydantic import BaseModel, Field, EmailStr 4 | from pydantic import BaseModel 5 | from bson import ObjectId 6 | from typing import Optional 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | # do not specify the '.env' 11 | load_dotenv() 12 | 13 | # pip install pydantic[email] 14 | # python -m pip install motor 15 | # python3 -m pip install "pymongo[srv]" 16 | 17 | # connect to mongodb 18 | client = motor.motor_asyncio.AsyncIOMotorClient(os.getenv('MONGODB_URL')) 19 | 20 | # create the news_summary_users database 21 | db = client.news_summary_users 22 | 23 | 24 | # BSON and JSON compatibility addressed here 25 | class PyObjectId(ObjectId): 26 | @classmethod 27 | def __get_validators__(cls): 28 | yield cls.validate 29 | 30 | @classmethod 31 | def validate(cls, v): 32 | if not ObjectId.is_valid(v): 33 | raise ValueError("Invalid objectid") 34 | return ObjectId(v) 35 | 36 | @classmethod 37 | def __modify_schema__(cls, field_schema): 38 | field_schema.update(type="string") 39 | 40 | 41 | class User(BaseModel): 42 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 43 | name: str = Field(...) 44 | email: EmailStr = Field(...) 45 | password: str = Field(...) 46 | 47 | class Config: 48 | allow_population_by_field_name = True 49 | arbitrary_types_allowed = True 50 | json_encoders = {ObjectId: str} 51 | schema_extra = { 52 | "example": { 53 | "name": "John Doe", 54 | "email": "jdoe@example.com", 55 | "password": "secret_code" 56 | } 57 | } 58 | 59 | 60 | class UserResponse(BaseModel): 61 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 62 | name: str = Field(...) 63 | email: EmailStr = Field(...) 64 | 65 | class Config: 66 | allow_population_by_field_name = True 67 | arbitrary_types_allowed = True 68 | json_encoders = {ObjectId: str} 69 | schema_extra = { 70 | "example": { 71 | "name": "John Doe", 72 | "email": "jdoe@example.com" 73 | } 74 | } 75 | 76 | 77 | class BlogContent(BaseModel): 78 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 79 | title: str = Field(...) 80 | body: str = Field(...) 81 | 82 | class Config: 83 | arbitrary_types_allowed = True 84 | json_encoders = {ObjectId: str} 85 | schema_extra = { 86 | "example": { 87 | "title": "blog title", 88 | "body": "blog content" 89 | } 90 | } 91 | 92 | 93 | class BlogContentResponse(BaseModel): 94 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 95 | title: str = Field(...) 96 | body: str = Field(...) 97 | auther_name: str = Field(...) 98 | auther_id: str = Field(...) 99 | created_at: str = Field(...) 100 | 101 | class Config: 102 | arbitrary_types_allowed = True 103 | json_encoders = {ObjectId: str} 104 | schema_extra = { 105 | "example": { 106 | "title": "blog title", 107 | "body": "blog content", 108 | "auther_name": "name of the auther", 109 | "auther_id": "ID of the auther", 110 | "created_at": "Date of blog creation" 111 | } 112 | } 113 | 114 | 115 | class Token(BaseModel): 116 | access_token: str 117 | token_type: str 118 | 119 | 120 | class TokenData(BaseModel): 121 | id: Optional[str] = None 122 | 123 | 124 | class PasswordResetRequest(BaseModel): 125 | email: EmailStr = Field(...) 126 | 127 | 128 | class PasswordReset(BaseModel): 129 | password: str = Field(...) 130 | -------------------------------------------------------------------------------- /api/send_email.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import BackgroundTasks 3 | 4 | # pip install fastapi-mail python-dotenv python-multipart 5 | from fastapi_mail import FastMail, MessageSchema, ConnectionConfig 6 | from dotenv import load_dotenv 7 | 8 | # do not specify the '.env' 9 | load_dotenv() 10 | 11 | 12 | class Envs: 13 | MAIL_USERNAME = os.getenv('MAIL_USERNAME') 14 | MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') 15 | MAIL_FROM = os.getenv('MAIL_FROM') 16 | MAIL_PORT = os.getenv('MAIL_PORT') 17 | MAIL_SERVER = os.getenv('MAIL_SERVER') 18 | MAIL_FROM_NAME = os.getenv('MAIN_FROM_NAME') 19 | 20 | 21 | conf = ConnectionConfig( 22 | MAIL_USERNAME=Envs.MAIL_USERNAME, 23 | MAIL_PASSWORD=Envs.MAIL_PASSWORD, 24 | MAIL_FROM=Envs.MAIL_FROM, 25 | MAIL_PORT=Envs.MAIL_PORT, 26 | MAIL_SERVER=Envs.MAIL_SERVER, 27 | MAIL_FROM_NAME=Envs.MAIL_FROM_NAME, 28 | MAIL_TLS=True, 29 | MAIL_SSL=False, 30 | USE_CREDENTIALS=True, 31 | TEMPLATE_FOLDER="api/templates" 32 | ) 33 | 34 | 35 | # https://sabuhish.github.io/fastapi-mail/example/#using-jinja2-html-templates 36 | async def send_registration_mail(subject: str, email_to: str, body: dict): 37 | message = MessageSchema( 38 | subject=subject, 39 | recipients=[email_to], 40 | template_body=body, 41 | subtype='html', 42 | ) 43 | 44 | fm = FastMail(conf) 45 | await fm.send_message(message, template_name='email.html') 46 | 47 | 48 | async def password_reset(subject: str, email_to: str, body: dict): 49 | message = MessageSchema( 50 | subject=subject, 51 | recipients=[email_to], 52 | template_body=body, 53 | subtype='html', 54 | ) 55 | 56 | fm = FastMail(conf) 57 | await fm.send_message(message, template_name='password_reset.html') 58 | 59 | 60 | -------------------------------------------------------------------------------- /api/templates/email.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |Welcome to the community, your registration has been successful. Enjoy our services.
9 |9 | Request to reset password granted, use the link below to reset your user password. 10 | If you did not make this request kindly ignore this email and nothing will happen, thanks. 11 |
12 | 16 | Reset password 17 | 18 |