From 81e1a2fc58c7bb02b7bc15ea8d6b36f16d977eaf Mon Sep 17 00:00:00 2001 From: Zhi Date: Sat, 21 Feb 2026 12:11:06 +0000 Subject: [PATCH] feat: add JWT auth (login/me), fix bcrypt version, add .gitignore --- .gitignore | 5 ++ app/__pycache__/__init__.cpython-311.pyc | Bin 129 -> 0 bytes app/__pycache__/main.cpython-311.pyc | Bin 12236 -> 0 bytes app/core/__pycache__/__init__.cpython-311.pyc | Bin 134 -> 0 bytes app/core/__pycache__/config.cpython-311.pyc | Bin 1926 -> 0 bytes app/main.py | 81 +++++++++++++++--- .../__pycache__/__init__.cpython-311.pyc | Bin 136 -> 0 bytes app/models/__pycache__/models.cpython-311.pyc | Bin 7744 -> 0 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 137 -> 0 bytes .../__pycache__/schemas.cpython-311.pyc | Bin 9165 -> 0 bytes requirements.txt | 1 + 11 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 .gitignore delete mode 100644 app/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/__pycache__/main.cpython-311.pyc delete mode 100644 app/core/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/core/__pycache__/config.cpython-311.pyc delete mode 100644 app/models/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/models/__pycache__/models.cpython-311.pyc delete mode 100644 app/schemas/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/schemas/__pycache__/schemas.cpython-311.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c383439 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +.env +*.egg-info/ +.venv/ diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index ea7056ad2d64f5bdb35209a5240dbb651d66063f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmZ3^%ge<81oc@nGePuY5CH>>P{wCAAY(d13PUi1CZpd--Y v$7kkcmc+;F6;%G>u*uC&Da}c>D`EvI0vS`x4!^pP1G diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc deleted file mode 100644 index fc0760f1718623967d8833e7af0ebe44c7f39c11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12236 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 A_y7O^ diff --git a/app/core/__pycache__/config.cpython-311.pyc b/app/core/__pycache__/config.cpython-311.pyc deleted file mode 100644 index 76e21c00d3045f4e584be2e2ee05bb9c8cd5704b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1926 zcma)6&2JM&6rcUDy}PmFq#+5xDXj!jwadJpIX(sL)-QJKn1 z^|=T>u-ZdUfltiTOpT;o_8i-@7i`k8J=13L5z_n2R2gM^-|$Vc0!vjWF?@oFvr=^k zDEdrNu?%YX)q4al86MXqJ>q#)*RhQs2<3PR^kw4vRcFNmtjyPZFdJ6o;B3)DJ^;Hl z5VjCOx?mz*ya7?vB_hMw#Yv$N`9&S%@#&@Uvk!RKMqpj#ku8Vd) z#U-`F6cD_pzz$DRN8!oa_2RQLfqWaNDc=rlQVAdumiZOYk}EW}Q=(1+|T*S-4>L;I}!n2S&@shaCrxJC)C zK>^KVC8il`zFTo^yXrHQ%gff9x60(Ds$+H~0I#Vtuy!8!RUU2_pCB};QxBxM-QZ1=)RB@N?DO5%k4H%bSs9%Vin7cx`${k zdHma-QvS18e1-VfT%uzDxWc;(>~ENL^rt$wdHTue`czAlVgq4sUpt3hE80`#v9hT> z(Hha*Ylue`dwY8xkL}O$IrMTUFG{6sKhvP%p&_jjd!0&PWTy@l6XbzEaiq^t9#+A!}^B z<=S_LrXiF((fE3E^2uafd94mLK7RIKXZYN!D}kDCs`(u?Uyq0BOk-nv?N#jenIJva iOwaA4=jzEYF&rd@n~9N~#7JFk#bd%i3w_9<()bscdER*d diff --git a/app/main.py b/app/main.py index 8b60f07..afd2405 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,11 @@ from fastapi import FastAPI, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session from typing import List +from datetime import datetime, timedelta +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel from app.core.config import get_db, settings from app.models import models @@ -22,6 +27,51 @@ app.add_middleware( allow_headers=["*"], ) +# Auth +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + user_id: int = None + +def verify_password(plain_password: str, hashed_password: str) -> bool: + if not hashed_password: + return False + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + password = password[:72] + + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: timedelta = None) -> str: + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=30)) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + +async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + user_id = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = db.query(models.User).filter(models.User.id == user_id).first() + if user is None: + raise credentials_exception + return user # Health check @app.get("/health") @@ -29,6 +79,23 @@ def health_check(): return {"status": "healthy"} +# ============ Auth API ============ + +@app.post("/auth/token", response_model=Token) +async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = db.query(models.User).filter(models.User.username == form_data.username).first() + if not user or not verify_password(form_data.password, user.hashed_password or ""): + raise HTTPException(status_code=401, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}) + if not user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + access_token = create_access_token(data={"sub": str(user.id)}, expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)) + return {"access_token": access_token, "token_type": "bearer"} + +@app.get("/auth/me", response_model=schemas.UserResponse) +async def get_me(current_user: models.User = Depends(get_current_user)): + return current_user + + # ============ Issues API ============ @app.post("/issues", response_model=schemas.IssueResponse, status_code=status.HTTP_201_CREATED) @@ -43,7 +110,7 @@ def create_issue(issue: schemas.IssueCreate, db: Session = Depends(get_db)): @app.get("/issues", response_model=List[schemas.IssueResponse]) def list_issues( project_id: int = None, - status: str = None, + issue_status: str = None, issue_type: str = None, skip: int = 0, limit: int = 100, @@ -53,8 +120,8 @@ def list_issues( if project_id: query = query.filter(models.Issue.project_id == project_id) - if status: - query = query.filter(models.Issue.status == status) + if issue_status: + query = query.filter(models.Issue.status == issue_status) if issue_type: query = query.filter(models.Issue.issue_type == issue_type) @@ -142,19 +209,13 @@ def get_project(project_id: int, db: Session = Depends(get_db)): @app.post("/users", response_model=schemas.UserResponse, status_code=status.HTTP_201_CREATED) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): - # Check if username or email exists existing = db.query(models.User).filter( (models.User.username == user.username) | (models.User.email == user.email) ).first() if existing: raise HTTPException(status_code=400, detail="Username or email already exists") - # Hash password if provided - hashed_password = None - if user.password: - from passlib.context import CryptContext - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - hashed_password = pwd_context.hash(user.password) + hashed_password = get_password_hash(user.password) if user.password else None db_user = models.User( username=user.username, diff --git a/app/models/__pycache__/__init__.cpython-311.pyc b/app/models/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 91d35f50f80e63736e25fe8a56e09680ffd0f591..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 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 D66PE> diff --git a/app/schemas/__pycache__/schemas.cpython-311.pyc b/app/schemas/__pycache__/schemas.cpython-311.pyc deleted file mode 100644 index 35767b066218eedd11a497ba8df9324646820532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9165 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 E0-^p|5&!@I diff --git a/requirements.txt b/requirements.txt index 9623ca0..6689709 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ pydantic==2.5.3 pydantic-settings==2.1.0 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 +bcrypt==4.0.1 python-multipart==0.0.6 alembic==1.13.1 python-dotenv==1.0.0