diff --git a/server/.env.example b/server/.env.example index 925fbad..373fcc5 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,4 +1,6 @@ DATABASE_URI="sqlite:///database.db" LANGUAGE_MODEL_API="http://localhost:8080/v1" MODEL_NAME="SmolLM3-Q4_K_M.gguf" -ORIGIN="http://localhost:5173" \ No newline at end of file +ORIGIN="http://localhost:5173" +SECRET_SIGN="xxxx" #generate secure random secret key: openssl rand -hex 32 +ACCESS_TOKEN_EXPIRE_MINUTES=10080 \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt index b151ac7..b750282 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -3,5 +3,7 @@ sqlmodel==0.0.32 python-dotenv==1.2.1 openai==2.21.0 spacy==3.8.11 +PyJWT>=2.11.0 +argon2-cffi>=25.1.0 # python -m spacy download en_core_web_sm # python -m spacy download fr_core_news_sm \ No newline at end of file diff --git a/server/src/app/__pycache__/database.cpython-311.pyc b/server/src/app/__pycache__/database.cpython-311.pyc index b0470e3..abff500 100644 Binary files a/server/src/app/__pycache__/database.cpython-311.pyc and b/server/src/app/__pycache__/database.cpython-311.pyc differ diff --git a/server/src/app/__pycache__/faker_seed.cpython-311.pyc b/server/src/app/__pycache__/faker_seed.cpython-311.pyc index 0acdbaf..491ccd6 100644 Binary files a/server/src/app/__pycache__/faker_seed.cpython-311.pyc and b/server/src/app/__pycache__/faker_seed.cpython-311.pyc differ diff --git a/server/src/app/__pycache__/main.cpython-311.pyc b/server/src/app/__pycache__/main.cpython-311.pyc index a7d461f..cfdbf97 100644 Binary files a/server/src/app/__pycache__/main.cpython-311.pyc and b/server/src/app/__pycache__/main.cpython-311.pyc differ diff --git a/server/src/app/api/v1/__init__.py b/server/src/app/api/v1/__init__.py index 8204fd4..d01ee60 100644 --- a/server/src/app/api/v1/__init__.py +++ b/server/src/app/api/v1/__init__.py @@ -2,7 +2,9 @@ from fastapi import APIRouter from .knowledges import router as knowledge_router from .metrics import router as metric_router +from .users import router as user_router router = APIRouter(prefix="/v1") router.include_router(knowledge_router) -router.include_router(metric_router) \ No newline at end of file +router.include_router(metric_router) +router.include_router(user_router) \ No newline at end of file diff --git a/server/src/app/api/v1/__pycache__/__init__.cpython-311.pyc b/server/src/app/api/v1/__pycache__/__init__.cpython-311.pyc index 8c3a1b4..b0c6a39 100644 Binary files a/server/src/app/api/v1/__pycache__/__init__.cpython-311.pyc and b/server/src/app/api/v1/__pycache__/__init__.cpython-311.pyc differ diff --git a/server/src/app/api/v1/__pycache__/knowledges.cpython-311.pyc b/server/src/app/api/v1/__pycache__/knowledges.cpython-311.pyc index e6bf093..f1ca81d 100644 Binary files a/server/src/app/api/v1/__pycache__/knowledges.cpython-311.pyc and b/server/src/app/api/v1/__pycache__/knowledges.cpython-311.pyc differ diff --git a/server/src/app/api/v1/__pycache__/users.cpython-311.pyc b/server/src/app/api/v1/__pycache__/users.cpython-311.pyc new file mode 100644 index 0000000..7584b8b Binary files /dev/null and b/server/src/app/api/v1/__pycache__/users.cpython-311.pyc differ diff --git a/server/src/app/api/v1/knowledges.py b/server/src/app/api/v1/knowledges.py index d0b0056..eb7d4d5 100644 --- a/server/src/app/api/v1/knowledges.py +++ b/server/src/app/api/v1/knowledges.py @@ -1,3 +1,4 @@ +from typing import Annotated from fastapi import APIRouter from src.app.models.knowledge import Knowledge diff --git a/server/src/app/api/v1/users.py b/server/src/app/api/v1/users.py new file mode 100644 index 0000000..d5e6252 --- /dev/null +++ b/server/src/app/api/v1/users.py @@ -0,0 +1,36 @@ +from typing import Annotated + +from fastapi import Depends, APIRouter, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer +from datetime import timedelta + +from src.app.models.user import User +from src.app.crud.crud_user import create_user +from src.app.services.auth import get_current_user, authenticate_user, create_access_token, hash_password, Token + +router = APIRouter(tags=["users"]) + +@router.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: + user = authenticate_user(form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token = create_access_token(data={"sub": user.username}) + return Token(access_token=access_token, token_type="bearer") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/token") + +@router.get("/user") +async def user(current_user: Annotated[str, Depends(get_current_user)]): + return current_user + +@router.post("/user") +async def create(username, password): + hashed_password = await hash_password(password) + user = User(username = username, hashed_password = hashed_password) + created_user = create_user(user) + return created_user \ No newline at end of file diff --git a/server/src/app/crud/__pycache__/crud_knowledges.cpython-311.pyc b/server/src/app/crud/__pycache__/crud_knowledges.cpython-311.pyc index 0fe5fe6..955b230 100644 Binary files a/server/src/app/crud/__pycache__/crud_knowledges.cpython-311.pyc and b/server/src/app/crud/__pycache__/crud_knowledges.cpython-311.pyc differ diff --git a/server/src/app/crud/__pycache__/crud_metrics.cpython-311.pyc b/server/src/app/crud/__pycache__/crud_metrics.cpython-311.pyc index ba6d79b..ec39e23 100644 Binary files a/server/src/app/crud/__pycache__/crud_metrics.cpython-311.pyc and b/server/src/app/crud/__pycache__/crud_metrics.cpython-311.pyc differ diff --git a/server/src/app/crud/__pycache__/crud_user.cpython-311.pyc b/server/src/app/crud/__pycache__/crud_user.cpython-311.pyc new file mode 100644 index 0000000..4e3d813 Binary files /dev/null and b/server/src/app/crud/__pycache__/crud_user.cpython-311.pyc differ diff --git a/server/src/app/crud/crud_knowledges.py b/server/src/app/crud/crud_knowledges.py index 1b64de5..e6182a9 100644 --- a/server/src/app/crud/crud_knowledges.py +++ b/server/src/app/crud/crud_knowledges.py @@ -20,9 +20,6 @@ def read_knowledges(): def read_knowledge(knowledge_id: int): with Session(engine) as session: - #statement = select(Knowledge).where(Knowledge.id == knowledge_id) - #results = session.exec(statement) - #knowledge = results.first() knowledge = session.get(Knowledge, knowledge_id) return knowledge @@ -36,6 +33,7 @@ def update_knowledge(knowledge_id: int, content: str, uri: str): session.add(knowledge) session.commit() session.refresh(knowledge) + return knowledge def delete_knowledge(knowledge_id: int): with Session(engine) as session: diff --git a/server/src/app/crud/crud_user.py b/server/src/app/crud/crud_user.py new file mode 100644 index 0000000..028f6ce --- /dev/null +++ b/server/src/app/crud/crud_user.py @@ -0,0 +1,38 @@ +from sqlmodel import Session, select + +from src.app.models.user import User +from src.app.database import engine + +def create_user(user: User): + with Session(engine) as session: + session.add(user) + session.commit() + session.refresh(user) + return user + +def get_user(username: str): + with Session(engine) as session: + statement = select(User).where(User.username == username) + results = session.exec(statement) + result = results.first() + return result + +def read_users(): + with Session(engine) as session: + statement = select(User) + results = session.exec(statement) + users = results.all() + return users + +def update_user(user_id: int, username: str, hash: str): + with Session(engine) as session: + user = session.get(User, user_id) + user.username = username if username else user.username + user.hash = hash if hash else user.hash + return user + +def delete_user(user_id: int): + with Session(engine) as session: + user = session.get(User, user_id) + session.delete(user) + session.commit() \ No newline at end of file diff --git a/server/src/app/database.py b/server/src/app/database.py index 086df93..31406b5 100644 --- a/server/src/app/database.py +++ b/server/src/app/database.py @@ -7,7 +7,7 @@ load_dotenv() database_uri=os.environ.get("DATABASE_URI") connect_args = {"check_same_thread": False} -engine = create_engine(database_uri, echo=True, connect_args=connect_args) +engine = create_engine(database_uri, echo=False, connect_args=connect_args) def create_db_and_tables(): SQLModel.metadata.create_all(engine) diff --git a/server/src/app/faker_seed.py b/server/src/app/faker_seed.py index 5a31d65..bd6ae02 100644 --- a/server/src/app/faker_seed.py +++ b/server/src/app/faker_seed.py @@ -1,7 +1,7 @@ from src.app.models.knowledge import Knowledge -from src.app.crud.crud_knowledges import create_knowledge, read_knowledges, read_knowledge, update_knowledge, delete_knowledge +from src.app.crud.crud_knowledges import create_knowledge from src.app.models.question import Question -from src.app.crud.crud_questions import create_question, read_questions, read_question +from src.app.crud.crud_questions import create_question from src.app.models.metric import Metric from src.app.crud.crud_metrics import create_metric @@ -37,3 +37,4 @@ def faker(): create_metric(metric3) create_metric(metric4) create_metric(metric5) + diff --git a/server/src/app/main.py b/server/src/app/main.py index 9b8cf94..cb0d9f9 100644 --- a/server/src/app/main.py +++ b/server/src/app/main.py @@ -11,6 +11,7 @@ from src.app.database import create_db_and_tables from src.app.models.question import Question from src.app.models.knowledge import Knowledge from src.app.models.metric import Metric +from src.app.models.user import User from .api import router diff --git a/server/src/app/models/__pycache__/knowledge.cpython-311.pyc b/server/src/app/models/__pycache__/knowledge.cpython-311.pyc index 728ab7f..5de219b 100644 Binary files a/server/src/app/models/__pycache__/knowledge.cpython-311.pyc and b/server/src/app/models/__pycache__/knowledge.cpython-311.pyc differ diff --git a/server/src/app/models/__pycache__/metric.cpython-311.pyc b/server/src/app/models/__pycache__/metric.cpython-311.pyc index 2b00308..4557d95 100644 Binary files a/server/src/app/models/__pycache__/metric.cpython-311.pyc and b/server/src/app/models/__pycache__/metric.cpython-311.pyc differ diff --git a/server/src/app/models/__pycache__/question.cpython-311.pyc b/server/src/app/models/__pycache__/question.cpython-311.pyc index f83ea23..b8b9649 100644 Binary files a/server/src/app/models/__pycache__/question.cpython-311.pyc and b/server/src/app/models/__pycache__/question.cpython-311.pyc differ diff --git a/server/src/app/models/__pycache__/user.cpython-311.pyc b/server/src/app/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..4e0f82c Binary files /dev/null and b/server/src/app/models/__pycache__/user.cpython-311.pyc differ diff --git a/server/src/app/models/knowledge.py b/server/src/app/models/knowledge.py index d98c182..6e28b2c 100644 --- a/server/src/app/models/knowledge.py +++ b/server/src/app/models/knowledge.py @@ -1,5 +1,4 @@ from sqlmodel import Field, SQLModel, Relationship -#TODO : add pydantic validation class Knowledge(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) diff --git a/server/src/app/models/metric.py b/server/src/app/models/metric.py index 75a72ad..6671ffe 100644 --- a/server/src/app/models/metric.py +++ b/server/src/app/models/metric.py @@ -1,8 +1,6 @@ from sqlmodel import Field, SQLModel, Relationship from src.app.models.question import Question -#TODO : add pydantic validation - class Metric(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) diff --git a/server/src/app/models/question.py b/server/src/app/models/question.py index 4da7d02..86b5aeb 100644 --- a/server/src/app/models/question.py +++ b/server/src/app/models/question.py @@ -1,6 +1,5 @@ from sqlmodel import Field, SQLModel, Relationship from src.app.models.knowledge import Knowledge -#TODO : add pydantic validation class Question(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) diff --git a/server/src/app/models/user.py b/server/src/app/models/user.py new file mode 100644 index 0000000..355d511 --- /dev/null +++ b/server/src/app/models/user.py @@ -0,0 +1,6 @@ +from sqlmodel import Field, SQLModel, Relationship + +class User(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + username: str + hashed_password: str \ No newline at end of file diff --git a/server/src/app/services/__pycache__/auth.cpython-311.pyc b/server/src/app/services/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..63c9bee Binary files /dev/null and b/server/src/app/services/__pycache__/auth.cpython-311.pyc differ diff --git a/server/src/app/services/__pycache__/user.cpython-311.pyc b/server/src/app/services/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..33ff495 Binary files /dev/null and b/server/src/app/services/__pycache__/user.cpython-311.pyc differ diff --git a/server/src/app/services/auth.py b/server/src/app/services/auth.py new file mode 100644 index 0000000..0466b79 --- /dev/null +++ b/server/src/app/services/auth.py @@ -0,0 +1,80 @@ +import os +from dotenv import load_dotenv + +from datetime import timedelta, datetime, timezone +from typing import Annotated +from pydantic import BaseModel + + +import jwt +from jwt.exceptions import InvalidTokenError +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer + +from argon2 import PasswordHasher + +from src.app.models.user import User +from src.app.crud.crud_user import get_user + + +load_dotenv() +secret_key = os.environ.get("SECRET") +algorithm = "HS256" +access_token_expire_minutes = 10080 +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token") +password_hasher = PasswordHasher() + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + username: str | None = None + +def authenticate_user(username: str, password: str): + user: User = get_user(username) + if not user: + verify_password(password, user.hashed_password) + return False + if not verify_password(password, user.hashed_password): + return False + return user + +def verify_password(plain_password: str, hashed_password: str) -> bool: + isValidated: bool = False + try: + isValidated = password_hasher.verify(hashed_password, plain_password) + except: + isValidated = False + return isValidated + +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, 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: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, secret_key, algorithm) + username = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except InvalidTokenError: + raise credentials_exception + user = get_user(username=token_data.username) + if user is None: + raise credentials_exception + return user + +