feat: implement authentication and wallet initialization
This commit is contained in:
32
backend/app/auth_utils.py
Normal file
32
backend/app/auth_utils.py
Normal file
@@ -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
|
||||
@@ -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())
|
||||
|
||||
63
backend/app/routers/auth.py
Normal file
63
backend/app/routers/auth.py
Normal file
@@ -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"}
|
||||
22
backend/app/schemas.py
Normal file
22
backend/app/schemas.py
Normal file
@@ -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
|
||||
@@ -4,3 +4,8 @@ sqlalchemy
|
||||
psycopg2-binary
|
||||
asyncpg
|
||||
pydantic-settings
|
||||
passlib[bcrypt]
|
||||
bcrypt==4.0.1
|
||||
python-jose[cryptography]
|
||||
python-multipart
|
||||
email-validator
|
||||
|
||||
Reference in New Issue
Block a user