diff --git a/README.md b/README.md index 429247c..367e8ba 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # Initial Commit :D +Run 'uv export --format requirements-txt --no-dev --no-emit-project --output-file requirements.txt' for major release versions (Maybe I make this a git actions thing?) +Run 'uv run fastapi dev' for dev environment + +Recommended vscode extensions used for formatting: Ruff, Pylance. Use strict type checking. + # Run 'uv export --format requirements-txt --no-dev --no-emit-project --output-file requirements.txt' for major release versions (Maybe I make this a git actions thing?) diff --git a/app/logic/user_logic.py b/app/logic/user_logic.py index ecd3e06..9e051d7 100644 --- a/app/logic/user_logic.py +++ b/app/logic/user_logic.py @@ -6,12 +6,17 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from pwdlib import PasswordHash -from app.config import Settings -from app.database import get_from_database_where +from app.config import settings +from app.database import ( + add_to_database, + get_from_database, + get_from_database_where, + get_from_database_where_id, +) from app.models.token import TokenData -from app.models.user import User, UserInDB +from app.models.user import User, UserIn, UserInDB -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/auth") password_hash = PasswordHash.recommended() @@ -26,15 +31,37 @@ def get_password_hash(password: str): return password_hash.hash(password) +def get_users(): + return get_from_database(UserInDB) + + def get_user(username: str): user = get_from_database_where( - User, - User.username, # type: ignore (Type correct on runtime) + UserInDB, + UserInDB.username, # type: ignore (Type correct on runtime) username, - True, + limit=1, ) if user is not None: - return UserInDB.model_validate(user[0]) + return user[0] + + +def get_user_by_id(id: int): + user = get_from_database_where_id(UserInDB, id) + if user is not None: + return user + + +def save_user(user: UserIn): + print("Got save user call") + user_in_db = UserInDB( + username=user.username, + authority=user.authority, + hashed_password=get_password_hash(user.password), + ) + add_to_database(user_in_db) + print("Saved user") + return User.model_validate(get_user(user.username)) def authenticate_user(username: str, password: str): @@ -59,8 +86,8 @@ def create_access_token( to_encode.update({"exp": expire}) encoded_jwt = jwt.encode( # pyright: ignore[reportUnknownMemberType] (Third party skill issue) to_encode, - Settings.secret_key, - algorithm=Settings.algorithm, + settings.secret_key, + algorithm=settings.algorithm, ) return encoded_jwt @@ -74,8 +101,8 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): try: payload = jwt.decode( # pyright: ignore[reportUnknownMemberType] (Third party skill issue) token, - Settings.secret_key, - algorithms=[Settings.algorithm], + settings.secret_key, + algorithms=[settings.algorithm], ) username = payload.get("sub") if username is None: @@ -86,7 +113,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): user = get_user(token_data.username) # type: ignore Look just above here idiot! if user is None: raise credentials_exception - return user + return User.model_validate(user) async def get_current_active_user( diff --git a/app/main.py b/app/main.py index 186eed0..3c6e7ae 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ from fastapi import FastAPI from app.config import settings from app.database import create_database -from app.routers import project_route, service_route +from app.routers import project_route, service_route, user_route @asynccontextmanager @@ -24,3 +24,4 @@ app = FastAPI( app.include_router(project_route.router) app.include_router(service_route.router) +app.include_router(user_route.router) diff --git a/app/models/user.py b/app/models/user.py index c953516..26f83ef 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,7 +1,7 @@ from sqlmodel import Field, SQLModel -class User(SQLModel, table=True): +class User(SQLModel): id: int | None = Field(default=None, primary_key=True) username: str @@ -9,5 +9,9 @@ class User(SQLModel, table=True): authority: int -class UserInDB(User): +class UserIn(User): + password: str + + +class UserInDB(User, table=True): hashed_password: str diff --git a/app/routers/user_route.py b/app/routers/user_route.py index 371ff0f..00cee2d 100644 --- a/app/routers/user_route.py +++ b/app/routers/user_route.py @@ -4,29 +4,56 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from app.config import Settings +from app.config import settings from app.logic.user_logic import ( authenticate_user, create_access_token, get_current_user, + get_user_by_id, + get_users, + save_user, ) from app.models.token import Token -from app.models.user import User +from app.models.user import User, UserIn router = APIRouter(prefix="/users", tags=["users"]) -@router.get("/") -def get_default(): - return {"message": "Root for Users API"} +@router.get("") +def fetch_users(): + users = get_users() + if not users: + return None + return [User.model_validate(user) for user in users] -@router.get("/me") -async def read_me(current_user: Annotated[User, Depends(get_current_user)]): +@router.post("") +async def create_user(user_in: UserIn): + user = save_user(user_in) + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) + access_token = create_access_token( + data={"sub": user.username}, + expires_delta=access_token_expires, + ) + return Token(access_token=access_token, token_type="bearer") + + +@router.get("/user") +async def fetch_current_user(current_user: Annotated[User, Depends(get_current_user)]): return current_user -@router.post("/token") +@router.get("/user/id/{user_id}") +async def fetch_user_by_id(user_id: int): + return User.model_validate(get_user_by_id(user_id)) + + +@router.get("/user/name/{user_name}") +async def fetch_user_by_name(user_id: int): + return User.model_validate(get_user_by_id(user_id)) + + +@router.post("/auth") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], ) -> Token: @@ -37,7 +64,7 @@ async def login_for_access_token( detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) - access_token_expires = timedelta(minutes=Settings.access_token_expire_minutes) + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires, diff --git a/data/.path b/data/.path new file mode 100644 index 0000000..e69de29