Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Text2SQL API package."""
77 changes: 77 additions & 0 deletions api/app_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Application factory for the text2sql Flask app."""

import logging
import os

from dotenv import load_dotenv
from flask import Flask, redirect, url_for
from flask_dance.contrib.google import make_google_blueprint
from flask_dance.contrib.github import make_github_blueprint
from flask_dance.consumer.storage.session import SessionStorage

from api.auth.oauth_handlers import setup_oauth_handlers
from api.routes.auth import auth_bp
from api.routes.graphs import graphs_bp
from api.routes.database import database_bp

# Load environment variables from .env file
load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")


def create_app():
"""Create and configure the Flask application."""
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY")
if not app.secret_key:
import secrets
app.secret_key = secrets.token_hex(32)
logging.warning("FLASK_SECRET_KEY not set, using generated key. Set this in production!")

# Google OAuth setup
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
google_bp = make_google_blueprint(
client_id=GOOGLE_CLIENT_ID,
client_secret=GOOGLE_CLIENT_SECRET,
scope=[
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"openid"
]
)
app.register_blueprint(google_bp, url_prefix="/login")

# GitHub OAuth setup
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
github_bp = make_github_blueprint(
client_id=GITHUB_CLIENT_ID,
client_secret=GITHUB_CLIENT_SECRET,
scope="user:email",
storage=SessionStorage()
)
app.register_blueprint(github_bp, url_prefix="/login")

# Set up OAuth signal handlers
setup_oauth_handlers(google_bp, github_bp)

# Register blueprints
app.register_blueprint(auth_bp)
app.register_blueprint(graphs_bp)
app.register_blueprint(database_bp)

@app.errorhandler(Exception)
def handle_oauth_error(error):
"""Handle OAuth-related errors gracefully"""
# Check if it's an OAuth-related error
if "token" in str(error).lower() or "oauth" in str(error).lower():
logging.warning("OAuth error occurred: %s", error)
from flask import session
session.clear()
return redirect(url_for("auth.home"))

# For other errors, let them bubble up
raise error

return app
17 changes: 17 additions & 0 deletions api/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Authentication module for text2sql API

from .user_management import (
ensure_user_in_organizations,
update_identity_last_login,
validate_and_cache_user,
token_required
)
from .oauth_handlers import setup_oauth_handlers

__all__ = [
"ensure_user_in_organizations",
"update_identity_last_login",
"validate_and_cache_user",
"token_required",
"setup_oauth_handlers"
]
122 changes: 122 additions & 0 deletions api/auth/oauth_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""OAuth signal handlers for Google and GitHub authentication."""

import logging
import time

import requests
from flask import session
from flask_dance.consumer import oauth_authorized
from flask_dance.contrib.google import google
from flask_dance.contrib.github import github

from .user_management import ensure_user_in_organizations


def setup_oauth_handlers(google_bp, github_bp):
"""Set up OAuth signal handlers for both Google and GitHub blueprints."""

@oauth_authorized.connect_via(google_bp)
def google_logged_in(blueprint, token): # pylint: disable=unused-argument
"""Handle Google OAuth authorization callback."""
if not token:
return False

try:
# Get user profile
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
google_user = resp.json()
user_id = google_user.get("id")
email = google_user.get("email")
name = google_user.get("name")

# Validate required fields
if not user_id or not email:
logging.error("Missing required fields from Google OAuth response")
return False

if user_id and email:
# Check if identity exists in Organizations graph, create if new
_, _ = ensure_user_in_organizations(
user_id, email, name, "google", google_user.get("picture")
)

# If existing identity, just update last login
# (already done in ensure_user_in_organizations)

# Normalize user info structure for session
user_info_session = {
"id": str(user_id), # Ensure string type
"name": name or "",
"email": email,
"picture": google_user.get("picture", ""),
"provider": "google"
}
session["user_info"] = user_info_session
session["token_validated_at"] = time.time()
return False # Don't create default flask-dance entry in session

except (requests.RequestException, KeyError, ValueError, AttributeError) as e:
logging.error("Google OAuth signal error: %s", e)

return False
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@oauth_authorized.connect_via(github_bp)
def github_logged_in(blueprint, token): # pylint: disable=unused-argument
"""Handle GitHub OAuth authorization callback."""
if not token:
return False

try:
# Get user profile
resp = github.get("/user")
if resp.ok:
github_user = resp.json()

# Get user email (GitHub may require separate call for email)
email_resp = github.get("/user/emails")
email = None
if email_resp.ok:
emails = email_resp.json()
# Find primary email
for email_obj in emails:
if email_obj.get("primary", False):
email = email_obj.get("email")
break
# If no primary email found, use the first one
if not email and emails:
email = emails[0].get("email")

user_id = str(github_user.get("id"))
name = github_user.get("name") or github_user.get("login")

# Validate required fields
if not user_id or not email:
logging.error("Missing required fields from GitHub OAuth response")
return False

if user_id and email:
# Check if identity exists in Organizations graph, create if new
_, _ = ensure_user_in_organizations(
user_id, email, name, "github", github_user.get("avatar_url")
)

# If existing identity, just update last login
# (already done in ensure_user_in_organizations)

# Normalize user info structure for session
user_info_session = {
"id": user_id,
"name": name or "",
"email": email,
"picture": github_user.get("avatar_url", ""),
"provider": "github"
}
session["user_info"] = user_info_session
session["token_validated_at"] = time.time()
return False # Don't create default flask-dance entry in session

except (requests.RequestException, KeyError, ValueError, AttributeError) as e:
logging.error("GitHub OAuth signal error: %s", e)

return False
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading