Skip to content

Commit

Permalink
add refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
FireFading committed Oct 16, 2023
1 parent bac0592 commit 9ab7042
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 42 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ TEST_DATABASE_URL="sqlite+aiosqlite://"
SECRET_KEY="secret"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=600
JWT_REFRESH_SECRET_KEY="secret"
55 changes: 36 additions & 19 deletions app/controllers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@
from typing import Annotated

from app.config import jwt_settings
from app.database import get_session
from app.models.users import User as UserModel
from app.schemas.tokens import TokenData
from app.schemas.users import UserCreate as UserCreateSchema
from app.services.users import UsersService, users_service
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from app.models.users import User as UserModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import with_async_session

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


class UsersController:
def __init__(self, users_service: UsersService) -> None:
Expand All @@ -23,16 +19,16 @@ def __init__(self, users_service: UsersService) -> None:
@with_async_session
async def register(
self,
user_schema: UserModel,
user_schema: UserCreateSchema,
session: AsyncSession | None = None,
) -> None:
) -> UserModel | HTTPException:
email = user_schema.email
if await self.users_service.get_user(session=session, email=email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this email already exists",
)
user = UserModel(**user_schema.dict())
user = UserModel(**user_schema.model_dump())
return await self.users_service.create(user=user, session=session)

@with_async_session
Expand All @@ -42,7 +38,9 @@ async def authenticate_user(
password: str,
session: AsyncSession | None,
) -> UserModel | HTTPException:
user = await self.users_service.authenticate_user(username=username, password=password, session=session)
user = await self.users_service.authenticate_user(
username=username, password=password, session=session
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -54,7 +52,7 @@ async def authenticate_user(
@with_async_session
async def get_current_user(
self,
token: Annotated[str, Depends(oauth2_scheme)],
token: str,
session: AsyncSession | None = None,
) -> UserModel | HTTPException:
credentials_exception = HTTPException(
Expand All @@ -63,31 +61,50 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, jwt_settings.secret_key, algorithms=[jwt_settings.algorithm])
payload = jwt.decode(
token, jwt_settings.secret_key, algorithms=[jwt_settings.algorithm]
)
username = payload.get("sub")
if not username:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = await self.users_service.get_user(username=token_data.username, session=session)
user = await self.users_service.get_user(
username=token_data.username, session=session
)
if not user:
raise credentials_exception
return user

async def get_current_active_user(
self,
current_user: Annotated[UserModel, Depends(get_current_user)],
):
) -> UserModel | HTTPException:
if current_user.disabled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"
)
return current_user

def create_access_token(self, data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=jwt_settings.access_token_expire_minutes)
to_encode["exp"] = expire
return jwt.encode(to_encode, jwt_settings.secret_key, algorithm=jwt_settings.algorithm)
def create_access_token(self, subject: str) -> str:
expires_delta = datetime.now(timezone.utc) + timedelta(
minutes=jwt_settings.access_token_expire_minutes
)
to_encode = {"exp": expires_delta, "sub": subject}
return jwt.encode(
to_encode, jwt_settings.secret_key, algorithm=jwt_settings.algorithm
)

def create_refresh_token(self, subject: str) -> str:
expires_delta = datetime.now(timezone.utc) + timedelta(
minutes=jwt_settings.refresh_token_expire_minutes
)

to_encode = {"exp": expires_delta, "sub": subject}
return jwt.encode(
to_encode, jwt_settings.jwt_refresh_secret_key, jwt_settings.algorithm
)


users_controller = UsersController(users_service=users_service)
50 changes: 30 additions & 20 deletions app/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,58 @@

from app.controllers.users import users_controller
from app.models.users import User as UserModel
from app.schemas.users import User as UserSchema, UserCreate as UserCreateSchema
from app.schemas.users import (
User as UserSchema,
UserCreate as UserCreateSchema,
UserShow as UserShowSchema,
)
from app.schemas.tokens import Token
from fastapi import APIRouter, Depends, status, Response
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.security import OAuth2PasswordBearer


router = APIRouter(
prefix="/users", tags=["users"], responses={404: {"description": "Not found"}}
)

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/login/", scheme_name="JWT")


@router.post(
"/register/",
status_code=status.HTTP_201_CREATED,
summary="Registration",
response_model=UserShowSchema,
)
async def register(user_schema: UserCreateSchema):
await users_controller.register(user_schema=user_schema)
return {"email": user_schema.email, "detail": "User created"}
return await users_controller.register(user_schema=user_schema)


@router.post(
"/token", response_model=Token, status_code=status.HTTP_200_OK, summary="Login"
"/login/", response_model=Token, status_code=status.HTTP_200_OK, summary="Login"
)
async def login(
response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
form_data: OAuth2PasswordRequestForm = Depends(),
):
user = await users_controller.authenticate_user(
username=form_data.username, password=form_data.password
)
access_token = users_controller.create_access_token(data={"sub": user.username})
response.set_cookie(
key="access_token", value=f"Bearer {access_token}", httponly=True
)
return {"access_token": access_token, "token_type": "bearer"}


# @router.get("/users/me/", response_model=None, status_code=status.HTTP_200_OK, summary="Get current user")
# async def current_user(current_user: Annotated[UserModel, Depends(users_controller.get_current_active_user)]):
# return current_user


# @router.get("/users/me/items/")
# async def about_user(current_user: Annotated[UserModel, Depends(users_controller.get_current_active_user)]):
# return [{"item_id": "Foo", "owner": current_user.username}]
access_token = users_controller.create_access_token(subject=user.username)
refresh_token = users_controller.create_refresh_token(subject=user.username)
response.set_cookie(key="access_token", value=f"{access_token}", httponly=True)
response.set_cookie(key="refresh_token", value=f"{refresh_token}", httponly=True)
return {
"access_token": access_token,
}


@router.get(
"/users/me/",
response_model=UserShowSchema,
status_code=status.HTTP_200_OK,
summary="Get user info",
)
async def current_user(token: str = Depends(oauth2_scheme)):
return await users_controller.get_current_user(token=token)
1 change: 0 additions & 1 deletion app/schemas/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

class Token(BaseModel):
access_token: str
token_type: str


class TokenData(BaseModel):
Expand Down
5 changes: 5 additions & 0 deletions app/schemas/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ class UserCreate(BaseModel):
password: str | None = None


class UserShow(BaseModel):
username: str
email: str | None = None
full_name: str | None = None

class UserInDB(User):
hashed_password: str
2 changes: 2 additions & 0 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ class JWTSettings(BaseSettings):
secret_key: str
algorithm: str
access_token_expire_minutes: int
refresh_token_expire_minutes: int
jwt_refresh_secret_key: str

4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ services:
postgres:
container_name: postgres
image: postgres:15-bullseye
ports:
- "5432:5432"
# ports:
# - "5432:5432"
env_file:
- .env.example
healthcheck:
Expand Down

0 comments on commit 9ab7042

Please sign in to comment.