Skip to content

Refactor api #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
app/__pycache__
app/.idea
app/.venv
app/orm-user.db
app/app/__pycache__
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do **/__pycache__/ instead.

app/app/api/__pycache__
app/app/auth/__pycache__
app/app/core/__pycache__
app/app/db/__pycache__
app/app/models/__pycache__
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add linting to the project, have a look at: https://github.com/astral-sh/ruff

Empty file added app/app/__init__.py
Empty file.
Empty file added app/app/api/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions app/app/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .user import router as user_router
from .login import router as login_router

__all__ = ["user_router", "login_router"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18 changes: 18 additions & 0 deletions app/app/api/routes/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session
from app.auth.auth import authenticate_user
from app.auth.security import create_access_token
from app.db.session import get_session
from datetime import timedelta
from app.core.config import ACCESS_TOKEN_EXPIRE_MINUTES

router = APIRouter()

@router.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session)):
user = authenticate_user(form_data.username, session, form_data.password)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
access_token = create_access_token(data={"sub": user.username}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
return {"access_token": access_token, "token_type": "bearer"}
9 changes: 9 additions & 0 deletions app/app/api/routes/profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from fastapi import APIRouter, Depends
from app.models.user import User
from app.auth.auth import get_current_active_user

router = APIRouter()

@router.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
31 changes: 31 additions & 0 deletions app/app/api/routes/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from app.models.user import User
from app.db.session import get_session
from app.auth.security import get_hash
import random

router = APIRouter()

@router.post("/signup")
async def add_user(session: Session = Depends(get_session), name: str = "", password: str = "", email: str = ""):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend using Pydantic models (e.g., BaseModel classes) for the request body instead of passing parameters like name, email, and password directly in the function signature.

statement = select(User).where(User.email == email)
if session.exec(statement).first():
raise HTTPException(status_code=400, detail="Email already exists")

while True:
username = name + str(random.randint(1, 100))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use slugify instead.

if not session.get(User, username):
break

new_user = User(
name=name,
username=username,
email=email,
hashed_password=get_hash(password),
disabled=False
)
session.add(new_user)
session.commit()
session.refresh(new_user)
return {"name": name, "username": username}
Empty file added app/app/auth/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions app/app/auth/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from fastapi import HTTPException, status, Depends
from jose import jwt, JWTError
from sqlmodel import Session
from fastapi.security import OAuth2PasswordBearer
from app.models.user import User
from app.db.session import get_session
from app.auth.security import verify_password
from app.core.config import SECRET_KEY, ALGORITHM

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

def authenticate_user(username: str, session: Session, password: str):
user = session.get(User, username)
if not user or not verify_password(user.hashed_password, password):
return False
return user

async def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)):
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, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception

user = session.get(User, username)
if user is None:
raise credentials_exception
return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
18 changes: 18 additions & 0 deletions app/app/auth/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from passlib.context import CryptContext
from datetime import datetime, timedelta, timezone
from jose import jwt
from app.core.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def get_hash(password: str) -> str:
return pwd_context.hash(password)

def verify_password(hashed_password, password):
return pwd_context.verify(password, hashed_password)

def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
Empty file added app/app/core/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions app/app/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SECRET_KEY = "your_secret_key_here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
Empty file added app/app/db/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions app/app/db/init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sqlmodel import SQLModel
from .session import engine

def create_db_and_table():
SQLModel.metadata.create_all(engine)
8 changes: 8 additions & 0 deletions app/app/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from sqlmodel import create_engine, Session

connect_args = {"check_same_thread": False}
engine = create_engine("sqlite:///orm-user.db", connect_args=connect_args)

def get_session():
with Session(engine) as session:
yield session
2 changes: 2 additions & 0 deletions app/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .user import User
__all__ = ["User"]
8 changes: 8 additions & 0 deletions app/app/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from sqlmodel import SQLModel, Field

class User(SQLModel, table=True):
name: str
username: str | None = Field(default=None, primary_key=True)
email: str | None = Field(default=None)
hashed_password: str | None = Field(default=None)
disabled: bool | None = Field(default=None)
12 changes: 12 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastapi import FastAPI
from app.api.routes import user, login, profile
from app.db.init_db import create_db_and_table

app = FastAPI()
app.include_router(profile.router, tags=["Auth Profile"])
app.include_router(user.router, tags=["Users"])
app.include_router(login.router, tags=["Auth"])

@app.on_event("startup")
def on_startup():
create_db_and_table()
35 changes: 35 additions & 0 deletions app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
annotated-types==0.7.0
anyio==4.9.0
bcrypt==3.2.2
certifi==2025.6.15
cffi==1.17.1
click==8.2.1
ecdsa==0.19.1
fastapi==0.115.12
greenlet==3.2.2
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.10
iniconfig==2.1.0
packaging==25.0
passlib==1.7.4
pluggy==1.6.0
pyasn1==0.4.8
pycparser==2.22
pydantic==2.11.5
pydantic_core==2.33.2
Pygments==2.19.1
pytest==8.4.0
pytest-asyncio==1.0.0
python-jose==3.4.0
python-multipart==0.0.20
rsa==4.9.1
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.41
sqlmodel==0.0.24
starlette==0.46.2
typing-inspection==0.4.1
typing_extensions==4.13.2
uvicorn==0.34.2
146 changes: 0 additions & 146 deletions main.py

This file was deleted.