@router.post("/accounts/{account_id}/test") async def test_account(account_id: int, user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db)): """Real connection test: verify OAuth creds and Gmail API profile.""" account = await db.get(GmailAccount, account_id) if not account or account.user_id != user.id: raise HTTPException(404, "Account not found") token = account.oauth_token if not token: return {"ok": False, "detail": "No OAuth token stored for this account"} from app.db.session import SyncSession with SyncSession() as s: acc_sync = s.get(GmailAccount, account.id) tok_sync = s.get(OAuthToken, token.id) try: client = gmail_service.build_gmail_client_from_account(acc_sync, tok_sync) result = client.verify_connection() except Exception as e: result = {"ok": False, "detail": str(e)[:400]} if result.get("ok"): account.status = "authorized" await db.commit() else: account.status = "expired" if "auth" in (result.get("detail") or "").lower() else account.status account.last_error = (result.get("detail") or "")[:500] await db.commit() await log_audit(db, user.id, "gmail.test", "gmail_account", account.id, {"ok": result.get("ok")}, None) return result @router.post("/accounts/{account_id}/reauthorize") async def reauthorize(account_id: int, user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db)): account = await db.get(GmailAccount, account_id) if not account or account.user_id != user.id: raise HTTPException(404, "Account not found") try: client = gmail_service.resolve_client_config_for_account(account) except gmail_service.GmailOauthError as e: raise HTTPException(400, str(e)) state = gmail_service.generate_state() import json as _json import redis as _redis from app.core.config import settings try: r = _redis.from_url(settings.REDIS_URL, socket_timeout=2) r.setex(f"oauthstate:{state}", 600, _json.dumps({"user_id": user.id})) except Exception: raise HTTPException(503, "State store unavailable") url = gmail_service.build_authorization_url(client, state) return {"authorization_url": url} @router.post("/accounts/{account_id}/revoke") async def disconnect_account(account_id: int, user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """Disconnect: revoke token at Google and delete stored credentials.""" account = await db.get(GmailAccount, account_id) if not account or account.user_id != user.id: raise HTTPException(404, "Account not found") token = account.oauth_token revoked_at_google = False if token: from app.services.gmail_service import revoke_token revoked_at_google = revoke_token(token.refresh_token or token.access_token) account.status = "revoked" account.is_active = False account.revoked_by_user = True account.revoked_at = datetime.now(timezone.utc) if token: await db.delete(token) await db.commit() await log_audit(db, user.id, "gmail.disconnect", "gmail_account", account.id, {"email": account.email, "revoked_at_google": revoked_at_google}, request.client.host if request else None) return {"ok": True, "revoked_at_google": revoked_at_google} @router.delete("/accounts/{account_id}") async def delete_account(account_id: int, user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): return await disconnect_account(account_id, user, db, request) @router.post("/accounts/clear-all") async def clear_all_accounts(user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """Clear All: revoke & remove all locally saved Gmail connections (audit preserved).""" rows = (await db.execute(select(GmailAccount).where( GmailAccount.user_id == user.id))).scalars().all() revoked = 0 for account in rows: token = account.oauth_token if token: if gmail_service.revoke_token(token.refresh_token or token.access_token): revoked += 1 account.status = "revoked" account.is_active = False account.revoked_by_user = True account.revoked_at = datetime.now(timezone.utc) if token: await db.delete(token) await db.commit() await log_audit(db, user.id, "gmail.clear_all", "gmail_account", None, {"count": len(rows), "revoked_at_google": revoked}, request.client.host if request else None) return {"ok": True, "removed": len(rows), "revoked_at_google": revoked} @router.post("/accounts/import-config") async def import_client_config(file: UploadFile = File(...), user: User = Depends(get_current_user_csrf), db: AsyncSession = Depends(get_db), request: Request = None): """Import & validate a Google OAuth client_secret JSON (never echoed back).""" from app.core.config import settings content = await file.read() if len(content) > 1024 * 512: raise HTTPException(413, "File too large (max 512 KB)") if not file.filename.lower().endswith(".json"): raise HTTPException(400, "Only .json configuration files are accepted") try: parsed = gmail_service.parse_client_json(content) except gmail_service.GmailOauthError as e: raise HTTPException(400, str(e)) row = OAuthClientConfig( name=parsed["project_id"] or file.filename[:100], user_id=user.id, client_id=parsed["client_id"], client_secret=parsed["client_secret"], # encrypted via property setter project_id=parsed["project_id"], redirect_uri=f"{settings.BASE_URL}/auth/google/callback", ) db.add(row) await db.commit()