Automated Action d63fc9b68d Implement complete Enviodeck Authentication API with FastAPI
- Phone number authentication with OTP verification
- Email/password authentication with secure bcrypt hashing
- Third-party OAuth login support for Google and Apple
- JWT token-based authentication system
- Rate limiting for OTP requests (5/minute)
- SQLite database with SQLAlchemy ORM
- Comprehensive user model with multiple auth providers
- Alembic database migrations setup
- API documentation with Swagger/OpenAPI
- Health check and system endpoints
- Environment configuration with security best practices
- Code quality with Ruff linting and formatting

Features:
- POST /auth/request-otp - Request OTP for phone authentication
- POST /auth/verify-otp - Verify OTP and get access token
- POST /auth/signup-email - Email signup with password
- POST /auth/login-email - Email login authentication
- POST /auth/login-google - Google OAuth integration
- POST /auth/login-apple - Apple OAuth integration
- GET /user/me - Get current authenticated user info
- GET / - API information and documentation links
- GET /health - Application health check
2025-06-21 08:59:35 +00:00

177 lines
5.5 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from slowapi import Limiter
from slowapi.util import get_remote_address
from fastapi import Request
from app.db.session import get_db
from app.schemas.auth import (
OTPRequest,
OTPVerify,
EmailSignup,
EmailLogin,
GoogleLogin,
AppleLogin,
Token,
)
from app.schemas.user import UserCreate
from app.models.user import AuthProvider
from app.services.user import (
get_user_by_phone,
get_user_by_email,
create_user,
authenticate_user,
verify_user_phone,
)
from app.utils.otp import generate_otp, store_otp, verify_otp, send_otp
from app.utils.auth import create_access_token
router = APIRouter()
limiter = Limiter(key_func=get_remote_address)
@router.post("/request-otp", response_model=dict)
@limiter.limit("5/minute")
async def request_otp(
request: Request, otp_request: OTPRequest, db: Session = Depends(get_db)
):
"""Request OTP for phone number authentication"""
# Generate and store OTP
otp = generate_otp()
if not store_otp(otp_request.phone_number, otp):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to generate OTP",
)
# Send OTP (mock implementation)
if not send_otp(otp_request.phone_number, otp):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send OTP",
)
return {"message": "OTP sent successfully"}
@router.post("/verify-otp", response_model=Token)
async def verify_otp_endpoint(otp_verify: OTPVerify, db: Session = Depends(get_db)):
"""Verify OTP and return access token"""
# Verify OTP
if not verify_otp(otp_verify.phone_number, otp_verify.otp):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired OTP"
)
# Get or create user
user = get_user_by_phone(db, otp_verify.phone_number)
if not user:
user_create = UserCreate(
phone_number=otp_verify.phone_number, auth_provider=AuthProvider.PHONE
)
user = create_user(db, user_create)
else:
user = verify_user_phone(db, otp_verify.phone_number)
# Create access token
access_token = create_access_token(data={"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/signup-email", response_model=Token)
async def signup_email(signup_data: EmailSignup, db: Session = Depends(get_db)):
"""Sign up with email and password"""
# Check if user already exists
if get_user_by_email(db, signup_data.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
)
# Create user
user_create = UserCreate(
email=signup_data.email,
name=signup_data.name,
password=signup_data.password,
auth_provider=AuthProvider.EMAIL,
)
user = create_user(db, user_create)
# Create access token
access_token = create_access_token(data={"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login-email", response_model=Token)
async def login_email(login_data: EmailLogin, db: Session = Depends(get_db)):
"""Login with email and password"""
user = authenticate_user(db, login_data.email, login_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"
)
# Create access token
access_token = create_access_token(data={"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login-google", response_model=Token)
async def login_google(google_data: GoogleLogin, db: Session = Depends(get_db)):
"""Login with Google OAuth token"""
# Mock Google token verification
# In production, verify the token with Google's API
mock_user_data = {"email": "user@gmail.com", "name": "Google User"}
# Get or create user
user = get_user_by_email(db, mock_user_data["email"])
if not user:
user_create = UserCreate(
email=mock_user_data["email"],
name=mock_user_data["name"],
auth_provider=AuthProvider.GOOGLE,
)
user = create_user(db, user_create)
# Create access token
access_token = create_access_token(data={"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login-apple", response_model=Token)
async def login_apple(apple_data: AppleLogin, db: Session = Depends(get_db)):
"""Login with Apple OAuth token"""
# Mock Apple token verification
# In production, verify the token with Apple's API
mock_user_data = {"email": "user@icloud.com", "name": "Apple User"}
# Get or create user
user = get_user_by_email(db, mock_user_data["email"])
if not user:
user_create = UserCreate(
email=mock_user_data["email"],
name=mock_user_data["name"],
auth_provider=AuthProvider.APPLE,
)
user = create_user(db, user_create)
# Create access token
access_token = create_access_token(data={"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}