2025-12-19 02:07:25 +07:00
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
import secrets
|
|
|
|
|
|
2025-12-18 17:15:21 +07:00
|
|
|
from fastapi import APIRouter, HTTPException, status, Request
|
2025-12-14 02:38:35 +07:00
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
from app.api.deps import DbSession, CurrentUser
|
|
|
|
|
from app.core.security import verify_password, get_password_hash, create_access_token
|
2025-12-18 17:15:21 +07:00
|
|
|
from app.core.rate_limit import limiter
|
2025-12-19 02:07:25 +07:00
|
|
|
from app.models import User, UserRole, Admin2FASession
|
|
|
|
|
from app.schemas import UserRegister, UserLogin, TokenResponse, UserPrivate, LoginResponse
|
|
|
|
|
from app.services.telegram_notifier import telegram_notifier
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/register", response_model=TokenResponse)
|
2025-12-18 17:15:21 +07:00
|
|
|
@limiter.limit("5/minute")
|
|
|
|
|
async def register(request: Request, data: UserRegister, db: DbSession):
|
2025-12-14 02:38:35 +07:00
|
|
|
# Check if login already exists
|
|
|
|
|
result = await db.execute(select(User).where(User.login == data.login.lower()))
|
|
|
|
|
if result.scalar_one_or_none():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Login already registered",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create user
|
|
|
|
|
user = User(
|
|
|
|
|
login=data.login.lower(),
|
|
|
|
|
password_hash=get_password_hash(data.password),
|
|
|
|
|
nickname=data.nickname,
|
|
|
|
|
)
|
|
|
|
|
db.add(user)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(user)
|
|
|
|
|
|
|
|
|
|
# Generate token
|
|
|
|
|
access_token = create_access_token(subject=user.id)
|
|
|
|
|
|
|
|
|
|
return TokenResponse(
|
|
|
|
|
access_token=access_token,
|
2025-12-18 17:15:21 +07:00
|
|
|
user=UserPrivate.model_validate(user),
|
2025-12-14 02:38:35 +07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-12-19 02:07:25 +07:00
|
|
|
@router.post("/login", response_model=LoginResponse)
|
2025-12-18 17:15:21 +07:00
|
|
|
@limiter.limit("10/minute")
|
|
|
|
|
async def login(request: Request, data: UserLogin, db: DbSession):
|
2025-12-14 02:38:35 +07:00
|
|
|
# Find user
|
|
|
|
|
result = await db.execute(select(User).where(User.login == data.login.lower()))
|
|
|
|
|
user = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
if not user or not verify_password(data.password, user.password_hash):
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
|
detail="Incorrect login or password",
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-19 02:07:25 +07:00
|
|
|
# Check if user is banned
|
|
|
|
|
if user.is_banned:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail="Your account has been banned",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# If admin with Telegram linked, require 2FA
|
|
|
|
|
if user.role == UserRole.ADMIN.value and user.telegram_id:
|
|
|
|
|
# Generate 6-digit code
|
|
|
|
|
code = "".join([str(secrets.randbelow(10)) for _ in range(6)])
|
|
|
|
|
|
|
|
|
|
# Create 2FA session (expires in 5 minutes)
|
|
|
|
|
session = Admin2FASession(
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
code=code,
|
|
|
|
|
expires_at=datetime.utcnow() + timedelta(minutes=5),
|
|
|
|
|
)
|
|
|
|
|
db.add(session)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(session)
|
|
|
|
|
|
|
|
|
|
# Send code to Telegram
|
|
|
|
|
message = f"🔐 <b>Код подтверждения для входа в админку</b>\n\nВаш код: <code>{code}</code>\n\nКод действителен 5 минут."
|
|
|
|
|
sent = await telegram_notifier.send_message(user.telegram_id, message)
|
|
|
|
|
|
|
|
|
|
if sent:
|
|
|
|
|
session.telegram_sent = True
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
|
|
|
|
return LoginResponse(
|
|
|
|
|
requires_2fa=True,
|
|
|
|
|
two_factor_session_id=session.id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Regular user or admin without Telegram - generate token immediately
|
|
|
|
|
# Admin without Telegram can login but admin panel will check for Telegram
|
|
|
|
|
access_token = create_access_token(subject=user.id)
|
|
|
|
|
|
|
|
|
|
return LoginResponse(
|
|
|
|
|
access_token=access_token,
|
|
|
|
|
user=UserPrivate.model_validate(user),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/2fa/verify", response_model=TokenResponse)
|
|
|
|
|
@limiter.limit("5/minute")
|
|
|
|
|
async def verify_2fa(request: Request, session_id: int, code: str, db: DbSession):
|
|
|
|
|
"""Verify 2FA code and return JWT token."""
|
|
|
|
|
# Find session
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Admin2FASession).where(Admin2FASession.id == session_id)
|
|
|
|
|
)
|
|
|
|
|
session = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
if not session:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Invalid session",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if session.is_verified:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Session already verified",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if datetime.utcnow() > session.expires_at:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Code expired",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if session.code != code:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="Invalid code",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Mark as verified
|
|
|
|
|
session.is_verified = True
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
|
|
|
|
# Get user
|
|
|
|
|
result = await db.execute(select(User).where(User.id == session.user_id))
|
|
|
|
|
user = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
if not user:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail="User not found",
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-14 02:38:35 +07:00
|
|
|
# Generate token
|
|
|
|
|
access_token = create_access_token(subject=user.id)
|
|
|
|
|
|
|
|
|
|
return TokenResponse(
|
|
|
|
|
access_token=access_token,
|
2025-12-18 17:15:21 +07:00
|
|
|
user=UserPrivate.model_validate(user),
|
2025-12-14 02:38:35 +07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-12-18 17:15:21 +07:00
|
|
|
@router.get("/me", response_model=UserPrivate)
|
2025-12-14 02:38:35 +07:00
|
|
|
async def get_me(current_user: CurrentUser):
|
2025-12-18 17:15:21 +07:00
|
|
|
"""Get current user's full profile (including private data)"""
|
|
|
|
|
return UserPrivate.model_validate(current_user)
|