structure auth

This commit is contained in:
Robin COuret
2026-03-05 22:38:21 +01:00
parent 01f9e9f05e
commit a243149bf1
27 changed files with 68 additions and 52 deletions

Binary file not shown.

View File

@@ -1,5 +1,6 @@
fastapi==0.128.6 fastapi==0.128.6
sqlmodel==0.0.32 sqlmodel==0.0.32
python-dotenv==1.2.1
openai==2.21.0 openai==2.21.0
spacy==3.8.11 spacy==3.8.11
PyJWT>=2.11.0 PyJWT>=2.11.0

View File

@@ -3,4 +3,3 @@ LANGUAGE_MODEL_API="http://localhost:8080/v1"
LANGUAGE_MODEL_NAME="SmolLM3-Q4_K_M.gguf" LANGUAGE_MODEL_NAME="SmolLM3-Q4_K_M.gguf"
ORIGIN="http://localhost:5173" ORIGIN="http://localhost:5173"
SECRET_KEY="xxxx" #generate secure random secret key: openssl rand -hex 32 SECRET_KEY="xxxx" #generate secure random secret key: openssl rand -hex 32
ACCESS_TOKEN_EXPIRE_MINUTES=10080

View File

@@ -2,9 +2,9 @@ from fastapi import APIRouter
from .knowledges import router as knowledge_router from .knowledges import router as knowledge_router
from .metrics import router as metric_router from .metrics import router as metric_router
from .users import router as user_router from .auth import router as auth_router
router = APIRouter(prefix="/v1") router = APIRouter(prefix="/v1")
router.include_router(knowledge_router) router.include_router(knowledge_router)
router.include_router(metric_router) router.include_router(metric_router)
router.include_router(user_router) router.include_router(auth_router)

Binary file not shown.

View File

@@ -2,11 +2,13 @@ from typing import Annotated
from fastapi import Depends, APIRouter, HTTPException, status from fastapi import Depends, APIRouter, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from datetime import timedelta
from src.app.models.user import User from src.app.models.user import User
from src.app.data.user import create_user from src.app.data.user import create_user
from src.app.services.auth import get_current_user, authenticate_user, create_access_token, hash_password, Token
from src.app.auth.dependancies import get_current_user, authenticate_user
from src.app.auth.security import hash_password, create_access_token
from src.app.auth.schemas import Token
router = APIRouter(tags=["users"]) router = APIRouter(tags=["users"])

Binary file not shown.

View File

@@ -1,35 +1,21 @@
from src.app.config import settings from src.app.config import settings
from datetime import timedelta, datetime, timezone
from typing import Annotated
from pydantic import BaseModel
from typing import Annotated
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from argon2 import PasswordHasher
from argon2 import PasswordHasher
import jwt import jwt
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from argon2 import PasswordHasher
from argon2.exceptions import (
VerifyMismatchError,
VerificationError,
InvalidHashError,
)
from src.app.models.user import User from src.app.models.user import User
from src.app.data.user import get_user from src.app.data.user import get_user
from .schemas import TokenData
from .security import verify_password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
password_hasher = PasswordHasher() password_hasher = PasswordHasher()
algorithm = "HS256"
access_token_expire_minutes = 10080
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
def authenticate_user(username: str, password: str): def authenticate_user(username: str, password: str):
user: User = get_user(username) user: User = get_user(username)
if not user: if not user:
@@ -40,23 +26,6 @@ def authenticate_user(username: str, password: str):
return False return False
return user return user
def verify_password(plain_password: str, hashed_password: str) -> bool:
isValidated: bool = False
try:
return password_hasher.verify(hashed_password, plain_password)
except (VerifyMismatchError, VerificationError, InvalidHashError):
return False
def create_access_token(data: dict):
expire = datetime.now(timezone.utc) + timedelta(minutes=access_token_expire_minutes)
to_encode = data.copy()
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=algorithm)
return encoded_jwt
async def hash_password(password: str) -> str:
return password_hasher.hash(password)
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User: async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -64,7 +33,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> Use
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithm=algorithm) payload = jwt.decode(token, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
username = payload.get("sub") username = payload.get("sub")
if username is None: if username is None:
raise credentials_exception raise credentials_exception

View File

@@ -0,0 +1,8 @@
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None

View File

@@ -0,0 +1,33 @@
from src.app.config import settings
from datetime import timedelta, datetime, timezone
from argon2 import PasswordHasher
from argon2.exceptions import (
VerifyMismatchError,
VerificationError,
InvalidHashError,
)
import jwt
from jwt.exceptions import InvalidTokenError
password_hasher = PasswordHasher()
def verify_password(plain_password: str, hashed_password: str) -> bool:
try:
return password_hasher.verify(hashed_password, plain_password)
except (VerifyMismatchError, VerificationError, InvalidHashError):
return False
def hash_password(password: str) -> str:
return password_hasher.hash(password)
def create_access_token(data: dict):
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = data.copy()
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
#def create_refresh_token(data: dict) -> str:
#def verify_token(token: str, token_type: str = "access") -> Optional[dict]:

View File

@@ -1,17 +1,19 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings): class Settings(BaseSettings):
# Database # Database
DATABASE_URI: str DATABASE_URI: str = Field("sqlite:///database.db", env='DATABASE_URI')
# Language model # Language model
LANGUAGE_MODEL_API: str = "http://localhost:8080/v1" LANGUAGE_MODEL_API: str = Field("http://localhost:8080/v1", env='LANGUAGE_MODEL_API')
LANGUAGE_MODEL_NAME: str = "SmolLM3-Q4_K_M.gguf" LANGUAGE_MODEL_NAME: str = Field("SmolLM3-Q4_K_M.gguf", env='LANGUAGE_MODEL_NAME')
# Security # Security
ORIGIN: str ORIGIN: str = Field('http://localhost:5173', env='ORIGIN')
SECRET_KEY : str SECRET_KEY : str = Field('random_string', env='SECRET_KEY')
ACCESS_TOKEN_EXPIRE_MINUTES: int ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
ALGORITHM: str = "HS256"
class Config: class Config:
env_file = ".env" env_file = ".env"

View File

View File

@@ -6,6 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
from src.app.database import create_db_and_tables from src.app.database import create_db_and_tables
# Import models in app
# TODO : best practice to manage models import # TODO : best practice to manage models import
from src.app.models.question import Question from src.app.models.question import Question
from src.app.models.knowledge import Knowledge from src.app.models.knowledge import Knowledge
@@ -14,7 +15,7 @@ from src.app.models.user import User
from .api import router from .api import router
#Test #Fake data
from src.app.faker_seed import faker from src.app.faker_seed import faker
#TODO : alternative @app.on_event("startup") ? #TODO : alternative @app.on_event("startup") ?

View File

View File

@@ -4,3 +4,4 @@ class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
username: str username: str
hashed_password: str hashed_password: str
#is_active: bool