Initial commit - Migrated to Dev environment
This commit is contained in:
240
backend/app/auth/router.py
Executable file
240
backend/app/auth/router.py
Executable file
@@ -0,0 +1,240 @@
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Header
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
import redis.asyncio as redis
|
||||
|
||||
# --- KONFIGURÁCIÓ ---
|
||||
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/service_finder_db"
|
||||
REDIS_URL = "redis://localhost:6379"
|
||||
SECRET_KEY = "szuper_titkos_jwt_kulcs_amit_env_bol_kellene_olvasni"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = 7
|
||||
|
||||
# --- ADATBÁZIS SETUP (SQLAlchemy 2.0) ---
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
|
||||
# --- REDIS SETUP ---
|
||||
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
|
||||
# --- SECURITY UTILS ---
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v2/auth/login")
|
||||
|
||||
class ClientType(str, Enum):
|
||||
WEB = "web"
|
||||
MOBILE = "mobile"
|
||||
|
||||
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_token(data: dict, expires_delta: timedelta):
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
# --- PYDANTIC SCHEMAS ---
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
email: EmailStr
|
||||
is_active: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str # OAuth2 form compatibility miatt username, de emailt várunk
|
||||
password: str
|
||||
client_type: ClientType # 'web' vagy 'mobile'
|
||||
|
||||
# --- ÜZLETI LOGIKA & ROUTER ---
|
||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
# 1. Email ellenőrzése
|
||||
stmt = select(User).where(User.email == user.email)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalars().first():
|
||||
raise HTTPException(status_code=400, detail="Ez az email cím már regisztrálva van.")
|
||||
|
||||
# 2. User létrehozása (inaktív)
|
||||
hashed_pwd = get_password_hash(user.password)
|
||||
new_user = User(email=user.email, password_hash=hashed_pwd, is_active=False)
|
||||
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
|
||||
# Itt kellene elküldeni az emailt a verify linkkel (most szimuláljuk)
|
||||
return new_user
|
||||
|
||||
@router.get("/verify/{token}")
|
||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
||||
# Megjegyzés: A valóságban a token-t dekódolni kellene, hogy kinyerjük a user ID-t.
|
||||
# Most szimuláljuk, hogy a token valójában a user email-címe base64-ben vagy hasonló.
|
||||
# Egyszerűsítés a példa kedvéért: feltételezzük, hogy a token = user_id
|
||||
|
||||
try:
|
||||
user_id = int(token) # DEMO ONLY
|
||||
stmt = select(User).where(User.id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Felhasználó nem található")
|
||||
|
||||
user.is_active = True
|
||||
await db.commit()
|
||||
return {"message": "Fiók sikeresen aktiválva!"}
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen token")
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
client_type: ClientType = ClientType.WEB, # Query param vagy form field
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Kritikus Redis Session Limitáció implementációja.
|
||||
"""
|
||||
# 1. User keresése
|
||||
stmt = select(User).where(User.email == form_data.username)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user or not verify_password(form_data.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Hibás email vagy jelszó")
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=403, detail="A fiók még nincs aktiválva.")
|
||||
|
||||
# 2. Token generálás
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
refresh_token_expires = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
|
||||
# A tokenbe beleégetjük a client_type-ot is, hogy validálásnál ellenőrizhessük
|
||||
token_data = {"sub": str(user.id), "client_type": client_type.value}
|
||||
|
||||
access_token = create_token(token_data, access_token_expires)
|
||||
refresh_token = create_token({"sub": str(user.id), "type": "refresh"}, refresh_token_expires)
|
||||
|
||||
# 3. REDIS SESSION KEZELÉS (A feladat kritikus része)
|
||||
# Kulcs formátum: session:{user_id}:{client_type} -> access_token
|
||||
session_key = f"session:{user.id}:{client_type.value}"
|
||||
|
||||
# A Redis 'SET' parancsa felülírja a kulcsot, ha az már létezik.
|
||||
# Ez megvalósítja a "Logout other devices" logikát az AZONOS típusú eszközökre.
|
||||
# Ezzel egy időben, mivel a kulcs tartalmazza a típust (web/mobile),
|
||||
# garantáljuk, hogy max 1 web és 1 mobile lehet (külön kulcsok).
|
||||
|
||||
await redis_client.set(
|
||||
name=session_key,
|
||||
value=access_token,
|
||||
ex=ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
|
||||
# --- MIDDLEWARE / DEPENDENCY TOKEN ELLENŐRZÉSHEZ ---
|
||||
async def get_current_user(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Nem sikerült hitelesíteni a felhasználót",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
client_type: str = payload.get("client_type")
|
||||
|
||||
if user_id is None or client_type is None:
|
||||
raise credentials_exception
|
||||
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
# KRITIKUS: Token validálása Redis ellenében (Stateful JWT)
|
||||
# Ha a Redisben lévő token nem egyezik a küldött tokennel,
|
||||
# akkor a felhasználót kijelentkeztették egy másik eszközről.
|
||||
session_key = f"session:{user_id}:{client_type}"
|
||||
stored_token = await redis_client.get(session_key)
|
||||
|
||||
if stored_token != token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="A munkamenet lejárt vagy egy másik eszközről beléptek."
|
||||
)
|
||||
|
||||
stmt = select(User).where(User.id == int(user_id))
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalars().first()
|
||||
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
# --- MAIN APP ---
|
||||
app = FastAPI(title="Service Finder API")
|
||||
app.include_router(router)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Service Finder API fut"}
|
||||
|
||||
@app.get("/protected-route")
|
||||
async def protected(user: User = Depends(get_current_user)):
|
||||
|
||||
return {"message": f"Szia {user.email}, érvényes a munkameneted!"}
|
||||
|
||||
Reference in New Issue
Block a user