-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
300398b
commit 951d251
Showing
15 changed files
with
393 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.git |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
VERSION=unknown | ||
APP_ENV=local | ||
POSTGRES_HOST_AUTH_METHOD = changeit | ||
POSTGRES_PASSWORD = mercury | ||
POSTGRES_HOST = mercury_db | ||
POSTGRES_PORT = 5432 | ||
POSTGRES_USER = mercury | ||
POSTGRES_DB = mercury | ||
POSTGRES_HOST_AUTH_METHOD = trust | ||
JWT_SECRET_KEY = "mysecretkey" | ||
JWT_ALGORITHM = "HS256" | ||
ACCESS_TOKEN_EXPIRE_MINUTES = 30 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
ARG PYTHON_VERSION=3.9.16 | ||
|
||
FROM python:${PYTHON_VERSION} as api | ||
|
||
ENV WERKZEUNG_RUN_MAIN=true \ | ||
PYTHONUNBUFFERED=1 \ | ||
PYTHONIOENCODING=UTF-8 \ | ||
LISTEN_ADDR="0.0.0.0" \ | ||
LISTEN_PORT=8000 | ||
|
||
WORKDIR /app | ||
|
||
COPY ./requirements.txt /app/requirements.txt | ||
|
||
RUN find . -name '*.pyc' -type f -delete && \ | ||
pip install --upgrade pip && \ | ||
pip install --no-cache-dir -r requirements.txt | ||
|
||
COPY . /app/ | ||
|
||
EXPOSE 8000 | ||
|
||
CMD ["python", "src/app.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,31 @@ | ||
# ⚡️ Mercury ⚡️ | ||
A FastApi template with PostgreSQL database. | ||
A FastApi template with PostgreSQL & Redis. | ||
|
||
## Prepare configurations | ||
|
||
```shell | ||
cp .env.dist .env # replace your variables in the .env file you just created | ||
``` | ||
|
||
## Run the containers | ||
|
||
```shell | ||
docker-compose up --build --force-recreate | ||
``` | ||
|
||
## Test the database | ||
|
||
```shell | ||
$ docker exec -it mercury_db psql -U mercury mercury | ||
psql (13.9 (Debian 13.9-1.pgdg110+1)) | ||
Type "help" for help. | ||
``` | ||
|
||
## Test the API | ||
|
||
You can print the Swagger doc here: http://localhost:8000 | ||
|
||
```shell | ||
$ curl localhost:5002/v1/health | ||
{"alive":true,"ip":"172.21.0.1","status":"ok"} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
version: '3.3' | ||
services: | ||
mercury_api: | ||
restart: always | ||
image: mercury_api:latest | ||
networks: | ||
- mercury_api | ||
build: | ||
context: . | ||
dockerfile: ./Dockerfile | ||
target: api | ||
env_file: | ||
- .env | ||
ports: | ||
- "8000:8000" | ||
depends_on: | ||
- mercury_cache | ||
- mercury_db | ||
- mercury_migrate | ||
mercury_cache: | ||
image: redis:6.2.6 | ||
restart: always | ||
container_name: mercury_cache | ||
networks: | ||
- mercury_api | ||
mercury_migrate: | ||
image: flyway/flyway:7.15.0 | ||
container_name: mercury_migrate | ||
command: -mixed=true -url="jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -user="${POSTGRES_USER}" -password="${POSTGRES_PASSWORD}" migrate | ||
volumes: | ||
- ./src/migrations:/flyway/sql | ||
env_file: | ||
- .env | ||
depends_on: | ||
- mercury_db | ||
networks: | ||
- mercury_api | ||
mercury_db: | ||
image: postgres:13 | ||
container_name: mercury_db | ||
ports: | ||
- 5432:5432 | ||
restart: always | ||
volumes: | ||
- mercurydb:/var/lib/postgresql/data | ||
env_file: | ||
- .env | ||
networks: | ||
- mercury_api | ||
|
||
volumes: | ||
mercurydb: | ||
|
||
networks: | ||
mercury_api: | ||
driver: bridge |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
fastapi[all] | ||
requests | ||
uvicorn[standard] | ||
redis | ||
SQLAlchemy | ||
fastapi-utils | ||
psycopg2 | ||
bcrypt | ||
python-jose[cryptography] | ||
passlib[bcrypt] | ||
python-multipart |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
from datetime import datetime, timedelta | ||
from typing import Annotated, Union | ||
import models | ||
import schemas | ||
from sqlalchemy.orm import Session | ||
from fastapi import Depends, HTTPException, status, APIRouter | ||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm | ||
from database import get_db | ||
import os | ||
from passlib.context import CryptContext | ||
from jose import JWTError, jwt | ||
|
||
router = APIRouter() | ||
|
||
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY') | ||
JWT_ALGORITHM = os.getenv('JWT_ALGORITHM') | ||
ACCESS_TOKEN_EXPIRE_MINUTES = os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES') | ||
|
||
pwd_context = CryptContext(schemes=["sha256_crypt"], deprecated="auto") | ||
|
||
def verify_password(plain_password, hashed_password): | ||
return pwd_context.verify(plain_password, hashed_password) | ||
|
||
def get_password_hash(password): | ||
return pwd_context.hash(password) | ||
|
||
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): | ||
to_encode = data.copy() | ||
if expires_delta: | ||
expire = datetime.utcnow() + expires_delta | ||
else: | ||
expire = datetime.utcnow() + timedelta(minutes=15) | ||
to_encode.update({"exp": expire}) | ||
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) | ||
return encoded_jwt | ||
|
||
def authenticate_user(payload, db): | ||
user = db.query(models.User).filter(models.User.email == payload.email).first() | ||
if not user: | ||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid credentials") | ||
if not verify_password(payload.password, user.password): | ||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Incorrect password") | ||
return user | ||
|
||
@router.post("/register", status_code=status.HTTP_201_CREATED) | ||
def register_user(payload: schemas.UserSchema, db: Session = Depends(get_db)): | ||
user = db.query(models.User).filter(models.User.email == payload.email).first() | ||
if user: | ||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered") | ||
payload.password = get_password_hash(payload.password) | ||
new_user = models.User(**payload.dict()) | ||
db.add(new_user) | ||
db.commit() | ||
db.refresh(new_user) | ||
return {"user": new_user} | ||
|
||
@router.post("/login", status_code=status.HTTP_200_OK) | ||
def login_user(payload: schemas.UserLoginSchema, db: Session = Depends(get_db)): | ||
user = authenticate_user(payload, db) | ||
access_token_expires = timedelta(minutes=int(ACCESS_TOKEN_EXPIRE_MINUTES)) | ||
access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires) | ||
return {"user": user, "token": {"access_token": access_token, "token_type": "bearer"}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import uvicorn | ||
import os | ||
|
||
if __name__ == "__main__": | ||
uvicorn.run("main:app", host=os.environ['LISTEN_ADDR'], port=int(os.environ['LISTEN_PORT']), reload=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from datetime import timedelta | ||
from typing import Annotated | ||
from api.user import authenticate_user, create_access_token, verify_password | ||
import models | ||
import schemas | ||
from sqlalchemy.orm import Session | ||
from fastapi import Depends, HTTPException, status | ||
from database import get_db | ||
import os | ||
from jose import JWTError, jwt | ||
from fastapi import Depends, HTTPException, status, APIRouter | ||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm | ||
|
||
router = APIRouter() | ||
|
||
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY') | ||
JWT_ALGORITHM = os.getenv('JWT_ALGORITHM') | ||
ACCESS_TOKEN_EXPIRE_MINUTES = os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES') | ||
|
||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="v1/token") | ||
|
||
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): | ||
credentials_exception = HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Could not validate credentials", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) | ||
try: | ||
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) | ||
email: str = payload.get("sub") | ||
if email is None: | ||
raise credentials_exception | ||
token_data = schemas.TokenData(email=email) | ||
except JWTError: | ||
raise credentials_exception | ||
user = db.query(models.User).filter(models.User.email == token_data.email).first() | ||
if user is None: | ||
raise credentials_exception | ||
return user | ||
|
||
async def get_current_active_user(current_user: schemas.UserSchema = Depends(get_current_user)): | ||
if current_user.disabled: | ||
raise HTTPException(status_code=400, detail="Inactive user") | ||
return current_user | ||
|
||
@router.post("/token", response_model=schemas.Token) | ||
def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Session = Depends(get_db)): | ||
user = db.query(models.User).filter(models.User.email == form_data.username).first() | ||
if not user: | ||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid credentials") | ||
if not verify_password(form_data.password, user.password): | ||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Incorrect password") | ||
if not user: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Incorrect email or password", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) | ||
access_token_expires = timedelta(minutes=int(ACCESS_TOKEN_EXPIRE_MINUTES)) | ||
access_token = create_access_token( | ||
data={"sub": user.email}, expires_delta=access_token_expires | ||
) | ||
return {"access_token": access_token, "token_type": "bearer"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from sqlalchemy import create_engine | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
import os | ||
|
||
postgres_db_name = os.getenv('POSTGRES_DB') | ||
postgres_user = os.getenv('POSTGRES_USER') | ||
postgres_password = os.getenv('POSTGRES_PASSWORD') | ||
postgres_port = os.getenv('POSTGRES_PORT') | ||
postgres_host = os.getenv('POSTGRES_HOST') | ||
|
||
POSTGRES_URL = f"postgresql://{postgres_user}:{postgres_password}@{postgres_host}:{postgres_port}/{postgres_db_name}" | ||
|
||
dbEngine = create_engine(POSTGRES_URL, echo=True) | ||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=dbEngine) | ||
|
||
Base = declarative_base() | ||
|
||
|
||
def get_db(): | ||
db = SessionLocal() | ||
try: | ||
yield db | ||
finally: | ||
db.close() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from api import user | ||
import auth_guard | ||
from fastapi import FastAPI | ||
from fastapi.middleware.cors import CORSMiddleware | ||
import os | ||
import redis | ||
from fastapi.openapi.utils import get_openapi | ||
import models | ||
from database import dbEngine | ||
|
||
def custom_openapi(): | ||
if app.openapi_schema: | ||
return app.openapi_schema | ||
openapi_schema = get_openapi( | ||
title="Mercury API Docs", | ||
version=os.environ['VERSION'], | ||
description="This is the Swagger documentation of the Mercury API", | ||
routes=app.routes, | ||
) | ||
app.openapi_schema = openapi_schema | ||
return app.openapi_schema | ||
|
||
models.Base.metadata.create_all(bind=dbEngine) | ||
redis_host = os.getenv('REDIS_HOST') | ||
redis_client = redis.Redis(host=redis_host, port=6379) | ||
|
||
app = FastAPI(docs_url="/") | ||
app.openapi = custom_openapi | ||
|
||
if os.getenv('APP_ENV') == 'local': | ||
app.add_middleware( | ||
CORSMiddleware, | ||
allow_origins=["*"], | ||
allow_credentials=True, | ||
allow_methods=["*"], | ||
allow_headers=["*"], | ||
) | ||
|
||
# app.include_router(health.router, tags=['Information'], prefix='/v1') | ||
app.include_router(auth_guard.router, tags=['Access Token'], prefix='/v1') | ||
app.include_router(user.router, tags=['User'], prefix='/v1/user') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | ||
CREATE TABLE TEMPORARY( | ||
TMP_ID SERIAL | ||
); | ||
|
||
DROP TABLE TEMPORARY; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
CREATE TABLE IF NOT EXISTS public.user ( | ||
id uuid DEFAULT uuid_generate_v4() NOT NULL PRIMARY KEY, | ||
username VARCHAR(200), | ||
email VARCHAR(200), | ||
password VARCHAR(200), | ||
is_admin BOOLEAN, | ||
disabled BOOLEAN | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import uuid | ||
from database import Base | ||
from sqlalchemy import Column, Integer, String, Boolean, Numeric | ||
from sqlalchemy.sql import func | ||
from fastapi_utils.guid_type import GUID, GUID_SERVER_DEFAULT_POSTGRESQL | ||
|
||
class User(Base): | ||
__tablename__ = 'user' | ||
id = Column(GUID, primary_key=True, server_default=GUID_SERVER_DEFAULT_POSTGRESQL) | ||
username = Column(String, nullable=False) | ||
email = Column(String, nullable=False) | ||
password = Column(String, nullable=False) | ||
is_admin = Column(Boolean, nullable=True, default=False) | ||
disabled = Column(Boolean, nullable=True, default=False) |
Oops, something went wrong.