Thigazhezhilan J 94f175668a Add automated daily Zerodha token refresh (auto-login)
- New auto_login_service.py: stores encrypted credentials (login ID,
  password, TOTP secret), performs headless Zerodha login via pyotp,
  and refreshes the session daily at 6:05 AM IST via background thread
- New auto_login router: setup, status, remove, and manual trigger endpoints
- Scheduler started at app boot alongside existing daemons
- Added pyotp==2.9.0 dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:47:21 +05:30

88 lines
3.1 KiB
Python

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from app.services.auth_service import get_user_for_session
from app.services.auto_login_service import (
delete_auto_login_credentials,
execute_auto_login,
get_auto_login_status,
save_auto_login_credentials,
)
router = APIRouter(prefix="/api/auto-login")
def _require_user(request: Request):
session_id = request.cookies.get("session_id")
if not session_id:
raise HTTPException(status_code=401, detail="Not authenticated")
user = get_user_for_session(session_id)
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
class AutoLoginSetupRequest(BaseModel):
zerodha_login_id: str
password: str
totp_secret: str
@router.post("/setup")
async def setup_auto_login(payload: AutoLoginSetupRequest, request: Request):
user = _require_user(request)
if not payload.zerodha_login_id.strip():
raise HTTPException(status_code=400, detail="Zerodha login ID is required")
if not payload.password:
raise HTTPException(status_code=400, detail="Password is required")
totp_clean = payload.totp_secret.strip().replace(" ", "")
if len(totp_clean) < 16:
raise HTTPException(status_code=400, detail="TOTP secret must be at least 16 characters")
save_auto_login_credentials(
user_id=user["id"],
zerodha_login_id=payload.zerodha_login_id.strip(),
password=payload.password,
totp_secret=totp_clean,
)
# Immediately test the credentials with a live login attempt
result = execute_auto_login(user_id=user["id"], email=user["username"])
if not result["success"]:
# Roll back — bad credentials shouldn't be saved
delete_auto_login_credentials(user["id"])
raise HTTPException(
status_code=400,
detail=f"Credentials saved but login test failed: {result.get('error', 'Unknown error')}. "
"Please check your Zerodha login ID, password, and TOTP secret.",
)
return {"configured": True, "message": "Auto-login set up and verified successfully"}
@router.get("/status")
async def auto_login_status(request: Request):
user = _require_user(request)
return get_auto_login_status(user["id"])
@router.delete("/setup")
async def remove_auto_login(request: Request):
user = _require_user(request)
delete_auto_login_credentials(user["id"])
return {"configured": False, "message": "Auto-login credentials removed"}
@router.post("/trigger")
async def trigger_auto_login(request: Request):
"""Manually trigger an immediate token refresh."""
user = _require_user(request)
status = get_auto_login_status(user["id"])
if not status.get("configured"):
raise HTTPException(status_code=400, detail="Auto-login is not configured")
result = execute_auto_login(user_id=user["id"], email=user["username"])
if not result["success"]:
raise HTTPException(status_code=502, detail=result.get("error", "Auto-login failed"))
return {"success": True, "message": "Zerodha session refreshed successfully"}