-
Notifications
You must be signed in to change notification settings - Fork 0
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 #6 from matinone/users
User authentication
- Loading branch information
Showing
24 changed files
with
830 additions
and
46 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
from fastapi import APIRouter | ||
|
||
from app.api.endpoints import question, quiz | ||
from app.api.endpoints import login, question, quiz | ||
|
||
api_router = APIRouter() | ||
api_router.include_router(quiz.router) | ||
api_router.include_router(question.router) | ||
api_router.include_router(login.router) |
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 typing import Annotated | ||
|
||
from fastapi import Depends, HTTPException, status | ||
from fastapi.security import OAuth2PasswordBearer | ||
|
||
import app.models as models | ||
from app.core.security import decode_token | ||
from app.core.settings import Settings, get_settings | ||
from app.models.database import AsyncSessionDep | ||
|
||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/tokens") | ||
|
||
|
||
async def get_current_user( | ||
db: AsyncSessionDep, | ||
token: Annotated[str, Depends(oauth2_scheme)], | ||
settings: Annotated[Settings, Depends(get_settings)], | ||
) -> models.User: | ||
token_data = decode_token(token=token, settings=settings) | ||
user = await models.User.get(db=db, id=int(token_data.sub)) | ||
if not user: | ||
raise HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found" | ||
) | ||
|
||
return 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,69 @@ | ||
from typing import Annotated, Any | ||
|
||
from fastapi import APIRouter, Depends, HTTPException, status | ||
from fastapi.security import OAuth2PasswordRequestForm | ||
|
||
import app.models as models | ||
import app.schemas as schemas | ||
from app.core.security import create_access_token, verify_password | ||
from app.models.database import AsyncSessionDep | ||
|
||
router = APIRouter(prefix="", tags=["login"]) | ||
|
||
|
||
@router.post( | ||
"/tokens", | ||
response_model=schemas.Token, | ||
status_code=status.HTTP_201_CREATED, | ||
summary="Get a new access token", | ||
) | ||
async def get_access_token_from_username( | ||
db: AsyncSessionDep, | ||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()], | ||
) -> Any: | ||
""" | ||
Get an OAuth2 access token from a user logging in with a username and password, | ||
to use in future requests as an authenticated user. | ||
""" | ||
user = await models.User.get_by_username(db=db, username=form_data.username) | ||
if not user: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, | ||
detail="Incorrect username or password", | ||
) | ||
if not verify_password( | ||
plain_password=form_data.password, hashed_password=user.password_hash | ||
): | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, | ||
detail="Incorrect username or password", | ||
) | ||
|
||
response = { | ||
"access_token": create_access_token(subject=user.id), | ||
"token_type": "bearer", | ||
} | ||
return response | ||
|
||
|
||
@router.post( | ||
"/register", | ||
response_model=schemas.UserReturn, | ||
status_code=status.HTTP_201_CREATED, | ||
summary="Register a new user", | ||
) | ||
async def register_user(db: AsyncSessionDep, user: schemas.UserCreate): | ||
new_user = await models.User.get_by_username(db=db, username=user.username) | ||
if new_user: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists" | ||
) | ||
|
||
new_user = await models.User.get_by_email(db=db, email=user.email) | ||
if new_user: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" | ||
) | ||
|
||
new_user = await models.User.create(db=db, user=user) | ||
return new_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
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,60 @@ | ||
from datetime import datetime, timedelta | ||
|
||
from fastapi import HTTPException, status | ||
from jose import ExpiredSignatureError, JWTError, jwt | ||
from passlib.context import CryptContext | ||
from pydantic import ValidationError | ||
|
||
from app.core.settings import Settings, get_settings | ||
from app.schemas import TokenPayload | ||
|
||
settings = get_settings() | ||
|
||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | ||
|
||
|
||
def decode_token(token: str, settings: Settings) -> TokenPayload: | ||
try: | ||
payload = jwt.decode( | ||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] | ||
) | ||
# raises ValidationError if the payload is not valid | ||
token_data = TokenPayload(**payload) | ||
except ExpiredSignatureError as exc: | ||
raise HTTPException( | ||
status_code=status.HTTP_403_FORBIDDEN, detail="Token expired" | ||
) from exc | ||
except (JWTError, ValidationError) as exc: | ||
# any HTTP status code 401 "UNAUTHORIZED" is supposed to also | ||
# return a WWW-Authenticate header | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Could not validate credentials", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) from exc | ||
|
||
return token_data | ||
|
||
|
||
def create_access_token( | ||
subject: str | int, expires_delta: timedelta | None = None | ||
) -> str: | ||
if expires_delta: | ||
expire = datetime.utcnow() + expires_delta | ||
else: | ||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MIN) | ||
|
||
to_encode = {"exp": expire, "sub": str(subject)} | ||
encoded_jwt = jwt.encode( | ||
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM | ||
) | ||
|
||
return encoded_jwt | ||
|
||
|
||
def get_password_hash(password: str) -> str: | ||
return pwd_context.hash(password) | ||
|
||
|
||
def verify_password(plain_password: str, hashed_password: str) -> bool: | ||
return pwd_context.verify(plain_password, hashed_password) |
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
Oops, something went wrong.