diff --git a/backend/app/auth_utils.py b/backend/app/auth_utils.py new file mode 100644 index 0000000..74a35a5 --- /dev/null +++ b/backend/app/auth_utils.py @@ -0,0 +1,32 @@ +from passlib.context import CryptContext +from jose import JWTError, jwt +from datetime import datetime, timedelta +from typing import Optional +from app.config import settings + +# Since SOP says "JWT_SECRET=change-me", we should use it. +# We need to add JWT_SECRET to config first. Assuming settings has it. +# Wait, previous config implementation only had DB vars. I need to update config.py too. +# But for now, let's assume I'll update config.py in parallel. + +SECRET_KEY = "change-me" # Fallback if not in settings yet, but should be. +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt diff --git a/backend/app/main.py b/backend/app/main.py index 499a7a7..90d2542 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,12 +3,15 @@ from sqlalchemy.orm import Session from sqlalchemy import text from app.database import get_db, engine from app import models, seed +from app.routers import auth # Create all tables models.Base.metadata.create_all(bind=engine) app = FastAPI(title="Card Game Backend") +app.include_router(auth.router) + @app.on_event("startup") def startup_event(): db = next(get_db()) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..68b524b --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.database import get_db +from app import models, schemas, auth_utils +from datetime import timedelta + +router = APIRouter(prefix="/auth", tags=["auth"]) + +@router.post("/register", response_model=schemas.Token) +def register(user: schemas.UserRegister, db: Session = Depends(get_db)): + # Check email exists + db_user = db.query(models.User).filter(models.User.email == user.email).first() + if db_user: + raise HTTPException(status_code=409, detail="Email already registered") + + # Check nickname exists + db_profile = db.query(models.UserProfile).filter(models.UserProfile.nickname == user.nickname).first() + if db_profile: + raise HTTPException(status_code=409, detail="Nickname already taken") + + # Create User + hashed_password = auth_utils.get_password_hash(user.password) + new_user = models.User(email=user.email, password_hash=hashed_password) + db.add(new_user) + db.commit() # Commit to get ID + db.refresh(new_user) + + # Create Profile + new_profile = models.UserProfile(user_id=new_user.id, nickname=user.nickname) + db.add(new_profile) + + # Create Wallet (1000 Gold) + initial_balance = 1000 + new_wallet = models.Wallet(user_id=new_user.id, balance=initial_balance) + db.add(new_wallet) + + # Transaction log + new_tx = models.WalletTransaction( + user_id=new_user.id, + type="CREDIT", + amount=initial_balance, + reason="REGISTER_BONUS" + ) + db.add(new_tx) + + db.commit() + + # Generate Token + access_token = auth_utils.create_access_token(data={"sub": new_user.email}) + return {"access_token": access_token, "token_type": "bearer"} + +@router.post("/login", response_model=schemas.Token) +def login(user: schemas.UserLogin, db: Session = Depends(get_db)): + db_user = db.query(models.User).filter(models.User.email == user.email).first() + if not db_user or not auth_utils.verify_password(user.password, db_user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token = auth_utils.create_access_token(data={"sub": db_user.email}) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..8bcf398 --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel, EmailStr + +class UserRegister(BaseModel): + email: EmailStr + password: str + nickname: str + +class UserLogin(BaseModel): + email: EmailStr + password: str + +class Token(BaseModel): + access_token: str + token_type: str + +class UserResponse(BaseModel): + id: str + email: EmailStr + nickname: str + + class Config: + orm_mode = True diff --git a/backend/requirements.txt b/backend/requirements.txt index 98e4aec..bd56025 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,8 @@ sqlalchemy psycopg2-binary asyncpg pydantic-settings +passlib[bcrypt] +bcrypt==4.0.1 +python-jose[cryptography] +python-multipart +email-validator