-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #100 from grillazz/99-jwt-auth-sample
99 jwt auth sample
- Loading branch information
Showing
25 changed files
with
979 additions
and
416 deletions.
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
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
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,2 @@ | ||
POSTGRES_PASSWORD=secret | ||
SECRET_KEY=key | ||
FERNET_KEY=Ms1HSn513x0_4WWFBQ3hYPDGAHpKH_pIseC5WwqyO7M= |
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
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
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,40 @@ | ||
"""user auth | ||
Revision ID: 2dcc708f88f8 | ||
Revises: 0d1ee3949d21 | ||
Create Date: 2023-07-22 12:19:28.780926 | ||
""" | ||
from alembic import op | ||
import sqlalchemy as sa | ||
|
||
|
||
# revision identifiers, used by Alembic. | ||
revision = '2dcc708f88f8' | ||
down_revision = '0d1ee3949d21' | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade(): | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.create_table('user', | ||
sa.Column('uuid', sa.UUID(), nullable=False), | ||
sa.Column('email', sa.String(), nullable=False), | ||
sa.Column('first_name', sa.String(), nullable=False), | ||
sa.Column('last_name', sa.String(), nullable=False), | ||
sa.Column('password', sa.LargeBinary(), nullable=False), | ||
sa.PrimaryKeyConstraint('uuid'), | ||
sa.UniqueConstraint('uuid') | ||
) | ||
op.create_unique_constraint(None, 'nonsense', ['name'], schema='happy_hog') | ||
op.create_unique_constraint(None, 'stuff', ['name'], schema='happy_hog') | ||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade(): | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.drop_constraint(None, 'stuff', schema='happy_hog', type_='unique') | ||
op.drop_constraint(None, 'nonsense', schema='happy_hog', type_='unique') | ||
op.drop_table('user') | ||
# ### end Alembic commands ### |
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,16 @@ | ||
import logging | ||
|
||
from fastapi import APIRouter, status, Request | ||
|
||
router = APIRouter() | ||
|
||
|
||
@router.get("/redis", status_code=status.HTTP_200_OK) | ||
async def redis_check(request: Request): | ||
_redis = await request.app.state.redis | ||
_info = None | ||
try: | ||
_info = await _redis.info() | ||
except Exception as e: | ||
logging.error(f"Redis error: {e}") | ||
return _info |
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,34 @@ | ||
from fastapi import APIRouter, Depends, status, Request, HTTPException | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
from app.database import get_db | ||
from app.models.user import User | ||
from app.schemas.user import UserSchema, UserResponse, UserLogin, TokenResponse | ||
from app.services.auth import create_access_token | ||
|
||
router = APIRouter(prefix="/v1/user") | ||
|
||
|
||
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse) | ||
async def create_user(payload: UserSchema, request: Request, db_session: AsyncSession = Depends(get_db)): | ||
_user: User = User(**payload.model_dump()) | ||
await _user.save(db_session) | ||
|
||
# TODO: add refresh token | ||
_user.access_token = await create_access_token(_user, request) | ||
return _user | ||
|
||
|
||
@router.post("/token", status_code=status.HTTP_201_CREATED, response_model=TokenResponse) | ||
async def get_token_for_user(user: UserLogin, request: Request, db_session: AsyncSession = Depends(get_db)): | ||
_user: User = await User.find(db_session, [User.email == user.email]) | ||
|
||
# TODO: out exception handling to external module | ||
if not _user: | ||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") | ||
if not _user.check_password(user.password): | ||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Password is incorrect") | ||
|
||
# TODO: add refresh token | ||
_token = await create_access_token(_user, request) | ||
return {"access_token": _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
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,24 +1,35 @@ | ||
from fastapi import FastAPI | ||
from contextlib import asynccontextmanager | ||
|
||
from fastapi import FastAPI, Depends | ||
|
||
from app.api.nonsense import router as nonsense_router | ||
from app.api.shakespeare import router as shakespeare_router | ||
from app.api.stuff import router as stuff_router | ||
from app.utils.logging import AppLogger | ||
from app.api.user import router as user_router | ||
from app.api.health import router as health_router | ||
from app.redis import get_redis | ||
from app.services.auth import AuthBearer | ||
|
||
logger = AppLogger.__call__().get_logger() | ||
|
||
app = FastAPI(title="Stuff And Nonsense API", version="0.5") | ||
|
||
@asynccontextmanager | ||
async def lifespan(app: FastAPI): | ||
# Load the redis connection | ||
app.state.redis = await get_redis() | ||
yield | ||
# close redis connection and release the resources | ||
app.state.redis.close() | ||
|
||
|
||
app = FastAPI(title="Stuff And Nonsense API", version="0.6", lifespan=lifespan) | ||
|
||
app.include_router(stuff_router) | ||
app.include_router(nonsense_router) | ||
app.include_router(shakespeare_router) | ||
app.include_router(user_router) | ||
|
||
|
||
@app.on_event("startup") | ||
async def startup_event(): | ||
logger.info("Starting up...") | ||
|
||
|
||
@app.on_event("shutdown") | ||
async def shutdown_event(): | ||
logger.info("Shutting down...") | ||
app.include_router(health_router, prefix="/v1/public/health", tags=["Health, Public"]) | ||
app.include_router(health_router, prefix="/v1/health", tags=["Health, Bearer"], dependencies=[Depends(AuthBearer())]) |
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
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,50 @@ | ||
import uuid | ||
from typing import Any | ||
|
||
from cryptography.fernet import Fernet | ||
from sqlalchemy import Column, String, LargeBinary, select | ||
from sqlalchemy.dialects.postgresql import UUID | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
from app import config | ||
from app.models.base import Base | ||
|
||
global_settings = config.get_settings() | ||
|
||
cipher_suite = Fernet(global_settings.secret_key) | ||
|
||
|
||
class User(Base): # type: ignore | ||
uuid = Column( | ||
UUID(as_uuid=True), | ||
unique=True, | ||
default=uuid.uuid4, | ||
primary_key=True, | ||
) | ||
email = Column(String, nullable=False) | ||
first_name = Column(String, nullable=False) | ||
last_name = Column(String, nullable=False) | ||
_password = Column("password", LargeBinary, nullable=False) | ||
|
||
def __init__(self, email: str, first_name: str, last_name: str, password: str = None): | ||
self.email = email | ||
self.first_name = first_name | ||
self.last_name = last_name | ||
self.password = password | ||
|
||
@property | ||
def password(self): | ||
return cipher_suite.decrypt(self._password).decode() | ||
|
||
@password.setter | ||
def password(self, password: str): | ||
self._password = cipher_suite.encrypt(password.encode()) | ||
|
||
def check_password(self, password: str): | ||
return self.password == password | ||
|
||
@classmethod | ||
async def find(cls, database_session: AsyncSession, where_conditions: list[Any]): | ||
_stmt = select(cls).where(*where_conditions) | ||
_result = await database_session.execute(_stmt) | ||
return _result.scalars().first() |
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 redis.asyncio as redis | ||
|
||
from app import config | ||
|
||
|
||
global_settings = config.get_settings() | ||
|
||
|
||
async def get_redis(): | ||
return await redis.from_url( | ||
global_settings.redis_url.unicode_string(), | ||
encoding="utf-8", | ||
decode_responses=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
Oops, something went wrong.