├── .gitignore ├── README.md ├── blog.db ├── blog ├── OAuth2.py ├── __init__.py ├── blog.db ├── database.py ├── main.py ├── models │ ├── __init__.py │ ├── blog.py │ └── user.py ├── repository │ ├── auth.py │ ├── blog.py │ └── user.py ├── routers │ ├── __init__.py │ ├── authentication.py │ ├── blog.py │ └── user.py ├── schemas.py └── utils │ ├── __init__.py │ └── hashing.py ├── images └── screenshot1.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | blog/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blogs-app-fastapi 2 | Simple Blogs app made using [FastAPI](https://github.com/tiangolo/fastapi) 3 | 4 | ## ScreenShot 5 | 6 |
7 | 8 |
9 | 10 | ## Web Routes 11 | 12 | All routes are available on ``/docs`` or ``/redoc`` paths with Swagger or ReDoc. 13 | 14 | ## Features 15 | 16 | - API Docs 17 | - ORM using SQLAlchemy 18 | - JWT Authentication 19 | - Pydantic Schemas 20 | 21 | ## Quick Start 22 | 23 | First, create a virtual env: 24 | 25 | ```bash 26 | python3 -m venv env 27 | ``` 28 | 29 | Second activate the virtual env: 30 | 31 | ```bash 32 | source env/bin/activate 33 | ``` 34 | 35 | Next, install requirements from requirements.txt: 36 | 37 | ```bash 38 | pip3 install -r requirements.txt 39 | ``` 40 | 41 | For running the app in debug mode: 42 | 43 | ```bash 44 | uvicorn blog.main:app --reload 45 | ``` 46 | -------------------------------------------------------------------------------- /blog.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/blog.db -------------------------------------------------------------------------------- /blog/OAuth2.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, HTTPException, status 2 | from fastapi.security import OAuth2PasswordBearer 3 | from .repository import auth as authRepository 4 | from .repository import user as userRepository 5 | from sqlalchemy.orm import Session 6 | from .database import get_db 7 | 8 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") 9 | 10 | async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): 11 | credentials_exception = HTTPException( 12 | status_code=status.HTTP_401_UNAUTHORIZED, 13 | detail="Could not validate credentials", 14 | headers={"WWW-Authenticate": "Bearer"}, 15 | ) 16 | token_data = authRepository.verify(token, credentials_exception) 17 | user = userRepository.get(token_data.username, db) 18 | if user is None: 19 | raise credentials_exception 20 | return user -------------------------------------------------------------------------------- /blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/blog/__init__.py -------------------------------------------------------------------------------- /blog/blog.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/blog/blog.db -------------------------------------------------------------------------------- /blog/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | DATABASE_URL = 'sqlite:///./blog.db' 6 | 7 | engine = create_engine(DATABASE_URL, connect_args = {"check_same_thread": False}) 8 | 9 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | 11 | Base = declarative_base() 12 | 13 | def get_db(): 14 | db = SessionLocal() 15 | try: 16 | yield db 17 | finally: 18 | db.close() -------------------------------------------------------------------------------- /blog/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from blog.models import user 3 | from .database import engine 4 | from .models import blog 5 | from blog import database 6 | from .routers import blog as blogRouter 7 | from .routers import user as userRouter 8 | from .routers import authentication as authRouter 9 | 10 | 11 | app = FastAPI() 12 | 13 | blog.Base.metadata.create_all(bind = engine) 14 | user.Base.metadata.create_all(bind = engine) 15 | 16 | get_db = database.get_db 17 | 18 | app.include_router(blogRouter.router) 19 | app.include_router(userRouter.router) 20 | app.include_router(authRouter.router) 21 | -------------------------------------------------------------------------------- /blog/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/blog/models/__init__.py -------------------------------------------------------------------------------- /blog/models/blog.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import relationship 2 | from sqlalchemy.sql.schema import ForeignKey 3 | from ..database import Base 4 | from sqlalchemy import Column, Integer, String, Boolean 5 | 6 | class Blog(Base): 7 | __tablename__ = 'blogs' 8 | 9 | id = Column(Integer, primary_key= True, index= True) 10 | title = Column(String) 11 | body = Column(String) 12 | published = Column(Boolean, default= False) 13 | 14 | username = Column(String, ForeignKey('users.username')) 15 | 16 | creator = relationship('User', back_populates="blogs") -------------------------------------------------------------------------------- /blog/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import relationship 2 | from ..database import Base 3 | from sqlalchemy import Column, String 4 | 5 | class User(Base): 6 | __tablename__ = 'users' 7 | 8 | username = Column(String, primary_key= True, index=True) 9 | name = Column(String) 10 | password = Column(String) 11 | 12 | blogs = relationship('Blog', back_populates="creator") -------------------------------------------------------------------------------- /blog/repository/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from fastapi import HTTPException, status 3 | from sqlalchemy.orm.session import Session 4 | from ..models.user import User 5 | from .. import schemas 6 | from ..utils.hashing import Hashing 7 | from typing import Optional 8 | from jose import jwt, JWTError 9 | 10 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 11 | SECRET_KEY = '09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7' 12 | ALGORITHM = 'HS256' 13 | 14 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 15 | to_encode = data.copy() 16 | if expires_delta: 17 | expire = datetime.utcnow() + expires_delta 18 | else: 19 | expire = datetime.utcnow() + timedelta(minutes=15) 20 | to_encode.update({"exp": expire}) 21 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 22 | return encoded_jwt 23 | 24 | def login(login: schemas.Login, db: Session): 25 | user = db.query(User).filter(User.username == login.username).first() 26 | if not user: 27 | raise HTTPException(status_code= status.HTTP_404_NOT_FOUND, detail="Invalid Credentials") 28 | if not Hashing.verify(login.password, user.password): 29 | raise HTTPException(status_code= status.HTTP_404_NOT_FOUND, detail="Invalid Credentials") 30 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 31 | access_token = create_access_token( 32 | data={"sub": user.username}, expires_delta=access_token_expires 33 | ) 34 | return {"access_token": access_token, "token_type": "bearer"} 35 | 36 | def verify(token: str, credentials_exception): 37 | try: 38 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 39 | username: str = payload.get("sub") 40 | if username is None: 41 | raise credentials_exception 42 | return schemas.TokenData(username=username) 43 | except JWTError: 44 | raise credentials_exception 45 | -------------------------------------------------------------------------------- /blog/repository/blog.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | from sqlalchemy.orm.session import Session 3 | from ..models.blog import Blog 4 | from .. import schemas 5 | 6 | 7 | def get_all(db: Session, user: schemas.User): 8 | blogs = db.query(Blog).filter(Blog.username == user.username).all() 9 | return blogs 10 | 11 | def create(blog: schemas.Blog, db: Session): 12 | new_blog = Blog(title = blog.title, body = blog.body, username= "user1") 13 | db.add(new_blog) 14 | db.commit() 15 | db.refresh(new_blog) 16 | return new_blog 17 | 18 | def get(blog_id: int, db: Session): 19 | blog = db.query(Blog).filter(Blog.id == blog_id).first() 20 | if not blog: 21 | raise HTTPException(status_code= status.HTTP_404_NOT_FOUND, detail="no blog with given id found") 22 | return blog 23 | 24 | def delete(blog_id: int, db: Session): 25 | db.query(Blog).filter(Blog.id == blog_id).delete(synchronize_session= False) 26 | db.commit() 27 | return 'deleted' 28 | 29 | def update(blog_id: int, blog: schemas.Blog, db: Session): 30 | blog = db.query(Blog).filter(Blog.id == blog_id).update(blog.dict()) 31 | db.commit() 32 | return 'updated' -------------------------------------------------------------------------------- /blog/repository/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | from sqlalchemy.orm.session import Session 3 | from ..models.user import User 4 | from .. import schemas 5 | from ..utils.hashing import Hashing 6 | 7 | 8 | def create(user: schemas.User, db: Session): 9 | hashed_pswd = Hashing.get_hashed_password(user.password) 10 | new_user = User(name= user.name, username = user.username, password= hashed_pswd) 11 | try: 12 | db.add(new_user) 13 | db.commit() 14 | db.refresh(new_user) 15 | return new_user 16 | except Exception as e: 17 | if e.__class__.__name__ == "IntegrityError": 18 | raise HTTPException(status_code= status.HTTP_422_UNPROCESSABLE_ENTITY, detail="username already taken") 19 | raise HTTPException(status_code= status.HTTP_500_INTERNAL_SERVER_ERROR, detail='something went wrong') 20 | 21 | def get(username: str, db: Session): 22 | user = db.query(User).filter(User.username == username).first() 23 | if not user: 24 | raise HTTPException(status_code= status.HTTP_404_NOT_FOUND, detail="user with given username not found") 25 | return user -------------------------------------------------------------------------------- /blog/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/blog/routers/__init__.py -------------------------------------------------------------------------------- /blog/routers/authentication.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from fastapi import APIRouter, Depends 3 | from sqlalchemy.orm import Session 4 | from blog import schemas 5 | from ..database import get_db 6 | from ..repository import auth as authRepository 7 | from fastapi.security import OAuth2PasswordRequestForm 8 | 9 | 10 | router = APIRouter(tags=['Authentication']) 11 | 12 | 13 | @router.post('/login') 14 | def login(login: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): 15 | return authRepository.login(login, db) 16 | -------------------------------------------------------------------------------- /blog/routers/blog.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter, Depends, status, HTTPException 3 | from sqlalchemy.orm import Session 4 | from blog import schemas 5 | from ..database import get_db 6 | from ..repository import blog as blogRepository 7 | from ..OAuth2 import get_current_user 8 | 9 | 10 | router = APIRouter(prefix='/blogs', 11 | tags=['Blogs']) 12 | 13 | @router.get("/", response_model=List[schemas.ShowBlog]) 14 | def get_all(db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)): 15 | return blogRepository.get_all(db, current_user) 16 | 17 | @router.post('/', status_code= status.HTTP_201_CREATED, response_model=schemas.ShowBlog) 18 | def create_blog(blog: schemas.Blog, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)): 19 | return blogRepository.create(blog, db) 20 | 21 | 22 | @router.get("/{blog_id}", response_model=schemas.ShowBlog) 23 | def get_blog(blog_id: int, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)): 24 | return blogRepository.get(blog_id, db) 25 | 26 | @router.put("/{blog_id}", status_code= status.HTTP_202_ACCEPTED) 27 | def update_blog(blog_id: int, blog: schemas.Blog, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)): 28 | return blogRepository.update(blog_id, blog, db) 29 | 30 | @router.delete("/{blog_id}", status_code= status.HTTP_204_NO_CONTENT) 31 | def delete_blog(blog_id: int, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)): 32 | return blogRepository.delete(blog_id, db) 33 | -------------------------------------------------------------------------------- /blog/routers/user.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import APIRouter, Depends, status, HTTPException 3 | from sqlalchemy.orm import Session 4 | from blog import schemas 5 | from ..database import get_db 6 | from ..models.user import User 7 | from ..utils.hashing import Hashing 8 | from ..repository import user as userRepository 9 | 10 | 11 | router = APIRouter(tags=['Users']) 12 | 13 | @router.post('/users', status_code= status.HTTP_201_CREATED) 14 | def create_user(user: schemas.User, db: Session = Depends(get_db)): 15 | return userRepository.create(user, db) 16 | 17 | @router.get('/users/{username}', response_model=schemas.ShowUser) 18 | def get_user_by_username(username: str, db: Session = Depends(get_db)): 19 | return userRepository.get(username, db) -------------------------------------------------------------------------------- /blog/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pydantic import BaseModel 3 | 4 | class User(BaseModel): 5 | name: str 6 | username: str 7 | password: str 8 | 9 | class Config(): 10 | orm_mode = True 11 | 12 | class Blog(BaseModel): 13 | title: str 14 | body: str 15 | published: Optional[bool] = False 16 | 17 | class Config(): 18 | orm_mode = True 19 | 20 | class ShowUser(BaseModel): 21 | name: str 22 | username: str 23 | blogs: List[Blog] = [] 24 | 25 | class Config(): 26 | orm_mode = True 27 | 28 | 29 | class ShowBlog(BaseModel): 30 | title: str 31 | body: str 32 | creator: ShowUser 33 | 34 | class Config(): 35 | orm_mode = True 36 | 37 | class Login(BaseModel): 38 | username: str 39 | password: str 40 | 41 | class Config(): 42 | orm_mode = True 43 | 44 | class TokenData(BaseModel): 45 | username: Optional[str] = None 46 | 47 | 48 | -------------------------------------------------------------------------------- /blog/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/blog/utils/__init__.py -------------------------------------------------------------------------------- /blog/utils/hashing.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pswd_cxt = CryptContext(schemes=["bcrypt"], deprecated="auto") 4 | 5 | class Hashing(): 6 | def get_hashed_password(password: str): 7 | return pswd_cxt.hash(password) 8 | 9 | def verify(plain_password: str, hashed_password: str): 10 | return pswd_cxt.verify(plain_password, hashed_password) -------------------------------------------------------------------------------- /images/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkey531/blogs-app-fastapi/3b99de96f626ebe3316782f0a14dce9a798e5eef/images/screenshot1.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | sqlalchemy 4 | passlib 5 | bcrypt 6 | python-jose 7 | python-multipart --------------------------------------------------------------------------------