EZOFIS-IRP / backend /app /otp_service.py
Seth
update
ced5eff
"""
OTP (One-Time Password) service for email-based authentication.
"""
import random
import string
from datetime import datetime, timedelta
from typing import Dict, Optional
from sqlalchemy.orm import Session
from fastapi import HTTPException
from .models import User
from .brevo_service import send_otp_email
# Store OTPs in memory (in production, use Redis or database)
otp_store: Dict[str, dict] = {}
def generate_otp(length: int = 6) -> str:
"""
Generate a random OTP code.
Args:
length: Length of OTP (default: 6)
Returns:
Random OTP string
"""
return ''.join(random.choices(string.digits, k=length))
async def request_otp(email: str, db: Session) -> dict:
"""
Generate and send OTP to email using Brevo.
Args:
email: Email address to send OTP to
db: Database session
Returns:
Dictionary with success message
"""
# Generate OTP
otp = generate_otp()
expires_at = datetime.utcnow() + timedelta(minutes=10)
# Store OTP (in production, use Redis or database with TTL)
otp_store[email.lower()] = {
'otp': otp,
'expires_at': expires_at,
'attempts': 0,
'max_attempts': 5
}
# Send OTP via Brevo
try:
await send_otp_email(email, otp)
print(f"[INFO] OTP generated and sent to {email}")
except Exception as e:
# Remove OTP from store if email sending failed
if email.lower() in otp_store:
del otp_store[email.lower()]
raise HTTPException(
status_code=500,
detail=f"Failed to send OTP email: {str(e)}"
)
return {
"message": "OTP sent to your email address",
"expires_in_minutes": 10
}
async def verify_otp(email: str, otp: str, db: Session) -> User:
"""
Verify OTP and return/create user.
Args:
email: Email address
otp: OTP code to verify
db: Database session
Returns:
User object
Raises:
HTTPException: If OTP is invalid, expired, or max attempts exceeded
"""
email_lower = email.lower()
stored = otp_store.get(email_lower)
if not stored:
raise HTTPException(
status_code=400,
detail="OTP not found. Please request a new OTP."
)
# Check if expired
if datetime.utcnow() > stored['expires_at']:
del otp_store[email_lower]
raise HTTPException(
status_code=400,
detail="OTP has expired. Please request a new OTP."
)
# Check max attempts
if stored['attempts'] >= stored['max_attempts']:
del otp_store[email_lower]
raise HTTPException(
status_code=400,
detail="Maximum verification attempts exceeded. Please request a new OTP."
)
# Verify OTP
if stored['otp'] != otp:
stored['attempts'] += 1
remaining_attempts = stored['max_attempts'] - stored['attempts']
raise HTTPException(
status_code=400,
detail=f"Invalid OTP. {remaining_attempts} attempt(s) remaining."
)
# OTP verified successfully
# Get or create user
user = db.query(User).filter(User.email == email_lower).first()
if not user:
user = User(
email=email_lower,
auth_method='otp',
email_verified=True
)
db.add(user)
db.commit()
db.refresh(user)
print(f"[INFO] New user created via OTP: {email_lower}")
# Enrich contact data from Apollo.io and update Brevo + Monday.com
try:
from .apollo_service import enrich_contact_by_email
from .brevo_service import create_brevo_contact, BREVO_TRIAL_LIST_ID
from .monday_service import create_monday_lead
# Enrich contact data from Apollo.io
enriched_data = await enrich_contact_by_email(email_lower)
# Use enriched data if available
first_name = enriched_data.get("first_name") if enriched_data else None
last_name = enriched_data.get("last_name") if enriched_data else None
org_name = enriched_data.get("organization_name") if enriched_data else None
# Fallback to email domain if Apollo didn't provide organization
if not org_name:
org_domain = email_lower.split('@')[1] if '@' in email_lower else None
org_name = org_domain.split('.')[0].capitalize() if org_domain else None
# Update Brevo contact with enriched data
await create_brevo_contact(
email=email_lower,
first_name=first_name,
last_name=last_name,
organization_name=org_name or (enriched_data.get("organization_name") if enriched_data else None),
phone_number=enriched_data.get("phone_number") if enriched_data else None,
linkedin_url=enriched_data.get("linkedin_url") if enriched_data else None,
title=enriched_data.get("title") if enriched_data else None,
headline=enriched_data.get("headline") if enriched_data else None,
organization_website=enriched_data.get("organization_website") if enriched_data else None,
organization_address=enriched_data.get("organization_address") if enriched_data else None,
list_id=BREVO_TRIAL_LIST_ID
)
# Create lead in Monday.com
await create_monday_lead(
email=email_lower,
first_name=first_name,
last_name=last_name,
phone_number=enriched_data.get("phone_number") if enriched_data else None,
linkedin_url=enriched_data.get("linkedin_url") if enriched_data else None,
title=enriched_data.get("title") if enriched_data else None,
headline=enriched_data.get("headline") if enriched_data else None,
organization_name=org_name or (enriched_data.get("organization_name") if enriched_data else None),
organization_website=enriched_data.get("organization_website") if enriched_data else None,
organization_address=enriched_data.get("organization_address") if enriched_data else None,
)
except Exception as e:
# Don't fail user creation if integrations fail
print(f"[WARNING] Failed to enrich/update contact for {email_lower}: {str(e)}")
else:
user.email_verified = True
if user.auth_method != 'otp':
user.auth_method = 'otp'
db.commit()
print(f"[INFO] User verified via OTP: {email_lower}")
# Remove OTP from store after successful verification
del otp_store[email_lower]
return user