refactor(auth): split OAuth + Telegram endpoints to app.domains.auth.oauth (P4.3 cont)
This commit is contained in:
parent
b31b564f36
commit
f5e1e140dc
2 changed files with 414 additions and 520 deletions
|
|
@ -4,7 +4,6 @@ Auth Router - Complete authentication system (email, wallet, OAuth, Telegram)
|
|||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
|
@ -24,13 +23,14 @@ from app.domains.auth.jwt import (
|
|||
_verify_jwt,
|
||||
generate_nonce,
|
||||
)
|
||||
from app.domains.auth.oauth import router as oauth_router
|
||||
from app.domains.auth.passwords import hash_password, verify_password
|
||||
from app.domains.auth.schemas import (
|
||||
EmailLoginRequest,
|
||||
EmailRegisterRequest,
|
||||
GoogleAuthResponse,
|
||||
GoogleAuthResponse as GoogleAuthResponse,
|
||||
NonceResponse,
|
||||
TelegramAuthRequest,
|
||||
TelegramAuthRequest as TelegramAuthRequest,
|
||||
TwoFAEnableRequest,
|
||||
TwoFALoginRequest,
|
||||
TwoFASetupResponse as TwoFASetupResponse,
|
||||
|
|
@ -68,6 +68,9 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
router = APIRouter(tags=["auth"])
|
||||
|
||||
# Include OAuth/Telegram routes from dedicated submodule
|
||||
router.include_router(oauth_router, tags=["auth"])
|
||||
|
||||
# ── Config ──
|
||||
|
||||
|
||||
|
|
@ -271,507 +274,6 @@ async def user_me(request: Request):
|
|||
}
|
||||
|
||||
|
||||
# ── Google OAuth ──
|
||||
@router.get("/google/url", response_model=GoogleAuthResponse)
|
||||
async def google_auth_url():
|
||||
"""Get Google OAuth URL."""
|
||||
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
||||
redirect_uri = os.getenv(
|
||||
"GOOGLE_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/google/callback"
|
||||
)
|
||||
|
||||
if not client_id:
|
||||
return {"url": "/auth/callback?error=google_not_configured"}
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"scope": "openid email profile",
|
||||
"access_type": "offline",
|
||||
"prompt": "consent",
|
||||
}
|
||||
url = f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
|
||||
return {"url": url}
|
||||
|
||||
|
||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "https://rugmunch.io")
|
||||
|
||||
|
||||
@router.get("/google/callback")
|
||||
async def google_callback(request: Request):
|
||||
"""Handle Google OAuth redirect - exchanges code, creates user, redirects to frontend with token."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
code = request.query_params.get("code")
|
||||
error = request.query_params.get("error")
|
||||
|
||||
if error:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error={error}")
|
||||
|
||||
if not code:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=missing_code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv(
|
||||
"GOOGLE_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/google/callback"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=not_configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=token_exchange_failed")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
resp = await client.get(
|
||||
"https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=userinfo_failed")
|
||||
|
||||
google_user = resp.json()
|
||||
email = google_user.get("email")
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": google_user.get("name", email),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
|
||||
# Redirect to frontend with token
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?token={jwt_token}&provider=google")
|
||||
|
||||
|
||||
@router.post("/google/callback", response_model=WalletAuthResponse)
|
||||
async def google_callback_post(request: Request):
|
||||
"""Legacy POST endpoint for manual code exchange."""
|
||||
body = await request.json()
|
||||
code = body.get("code")
|
||||
|
||||
if not code:
|
||||
raise HTTPException(status_code=400, detail="Missing authorization code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv(
|
||||
"GOOGLE_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/google/callback"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=400, detail="Failed to exchange code")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
resp = await client.get(
|
||||
"https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=400, detail="Failed to get user info")
|
||||
|
||||
google_user = resp.json()
|
||||
email = google_user.get("email")
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": google_user.get("name", email),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": jwt_token,
|
||||
"refresh_token": jwt_token,
|
||||
"user": {
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"display_name": user.get("display_name", email),
|
||||
"wallet_address": None,
|
||||
"wallet_chain": None,
|
||||
"tier": user.get("tier", "FREE"),
|
||||
"role": user.get("role", "USER"),
|
||||
"created_at": user.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── Telegram Auth ──
|
||||
@router.post("/telegram", response_model=WalletAuthResponse)
|
||||
async def telegram_auth(req: TelegramAuthRequest):
|
||||
"""Authenticate via Telegram Web App data."""
|
||||
bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
if not bot_token:
|
||||
raise HTTPException(status_code=500, detail="Telegram auth not configured")
|
||||
|
||||
data_check = {
|
||||
"id": str(req.id),
|
||||
"auth_date": str(req.auth_date),
|
||||
}
|
||||
if req.first_name:
|
||||
data_check["first_name"] = req.first_name
|
||||
if req.last_name:
|
||||
data_check["last_name"] = req.last_name
|
||||
if req.username:
|
||||
data_check["username"] = req.username
|
||||
if req.photo_url:
|
||||
data_check["photo_url"] = req.photo_url
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
sorted_data = "\n".join(f"{k}={v}" for k, v in sorted(data_check.items()))
|
||||
secret = hashlib.sha256(bot_token.encode()).digest()
|
||||
expected_hash = hmac.new(secret, sorted_data.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(req.hash, expected_hash):
|
||||
raise HTTPException(status_code=401, detail="Invalid Telegram auth data")
|
||||
|
||||
telegram_user_id = f"tg:{req.id}"
|
||||
user = _get_user(telegram_user_id)
|
||||
|
||||
if not user:
|
||||
display_name = (
|
||||
f"{req.first_name or ''} {req.last_name or ''}".strip()
|
||||
or req.username
|
||||
or f"User{req.id}"
|
||||
)
|
||||
email = f"{req.id}@telegram.rmi"
|
||||
|
||||
user = {
|
||||
"id": telegram_user_id,
|
||||
"email": email,
|
||||
"display_name": display_name,
|
||||
"telegram_id": req.id,
|
||||
"telegram_username": req.username,
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", telegram_user_id, json.dumps(user))
|
||||
r.hset("rmi:users:telegram", str(req.id), telegram_user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": jwt_token,
|
||||
"refresh_token": jwt_token,
|
||||
"user": {
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"display_name": user.get("display_name"),
|
||||
"wallet_address": None,
|
||||
"wallet_chain": None,
|
||||
"tier": user.get("tier", "FREE"),
|
||||
"role": user.get("role", "USER"),
|
||||
"created_at": user.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── GitHub OAuth ──
|
||||
@router.get("/github/url", response_model=GoogleAuthResponse)
|
||||
async def github_auth_url():
|
||||
"""Get GitHub OAuth URL."""
|
||||
client_id = os.getenv("GITHUB_CLIENT_ID")
|
||||
redirect_uri = os.getenv(
|
||||
"GITHUB_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/github/callback"
|
||||
)
|
||||
|
||||
if not client_id:
|
||||
return {"url": "/auth/callback?error=github_not_configured"}
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"scope": "user:email",
|
||||
}
|
||||
url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
||||
return {"url": url}
|
||||
|
||||
|
||||
@router.get("/github/callback")
|
||||
async def github_callback(request: Request):
|
||||
"""Handle GitHub OAuth redirect - exchanges code, creates user, redirects to frontend with token."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
code = request.query_params.get("code")
|
||||
error = request.query_params.get("error")
|
||||
|
||||
if error:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error={error}")
|
||||
|
||||
if not code:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=missing_code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("GITHUB_CLIENT_ID")
|
||||
client_secret = os.getenv("GITHUB_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv(
|
||||
"GITHUB_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/github/callback"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=not_configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Exchange code for access token
|
||||
resp = await client.post(
|
||||
"https://github.com/login/oauth/access_token",
|
||||
data={
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=token_exchange_failed")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
# Get user info
|
||||
resp = await client.get(
|
||||
"https://api.github.com/user",
|
||||
headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=userinfo_failed")
|
||||
|
||||
github_user = resp.json()
|
||||
email = github_user.get("email")
|
||||
|
||||
# If no public email, fetch emails endpoint
|
||||
if not email:
|
||||
resp = await client.get(
|
||||
"https://api.github.com/user/emails",
|
||||
headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
emails = resp.json()
|
||||
primary = next((e for e in emails if e.get("primary")), None)
|
||||
email = (
|
||||
primary.get("email")
|
||||
if primary
|
||||
else (emails[0].get("email") if emails else None)
|
||||
)
|
||||
|
||||
if not email:
|
||||
email = f"{github_user.get('login', 'github_user')}@github.rmi"
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": github_user.get("name", github_user.get("login", email)),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?token={jwt_token}&provider=github")
|
||||
|
||||
|
||||
# ── X/Twitter OAuth ──
|
||||
@router.get("/x/url", response_model=GoogleAuthResponse)
|
||||
async def x_auth_url():
|
||||
"""Get X/Twitter OAuth URL."""
|
||||
client_id = os.getenv("X_CLIENT_ID")
|
||||
redirect_uri = os.getenv("X_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/x/callback")
|
||||
|
||||
if not client_id:
|
||||
return {"url": "/auth/callback?error=x_not_configured"}
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"scope": "tweet.read users.read",
|
||||
}
|
||||
url = f"https://twitter.com/i/oauth2/authorize?{urlencode(params)}"
|
||||
return {"url": url}
|
||||
|
||||
|
||||
@router.get("/x/callback")
|
||||
async def x_callback(request: Request):
|
||||
"""Handle X/Twitter OAuth redirect - exchanges code, creates user, redirects to frontend with token."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
code = request.query_params.get("code")
|
||||
error = request.query_params.get("error")
|
||||
|
||||
if error:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error={error}")
|
||||
|
||||
if not code:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=missing_code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("X_CLIENT_ID")
|
||||
client_secret = os.getenv("X_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv("X_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/x/callback")
|
||||
|
||||
if not client_id or not client_secret:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=not_configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# X uses OAuth 2.0 - exchange code for token
|
||||
resp = await client.post(
|
||||
"https://api.twitter.com/2/oauth2/token",
|
||||
data={
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"code_verifier": "challenge", # X requires PKCE - we need to implement proper PKCE
|
||||
},
|
||||
auth=(client_id, client_secret),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=token_exchange_failed")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
# Get user info from X API
|
||||
resp = await client.get(
|
||||
"https://api.twitter.com/2/users/me",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=userinfo_failed")
|
||||
|
||||
x_user = resp.json().get("data", {})
|
||||
username = x_user.get("username", f"x_user_{x_user.get('id', 'unknown')}")
|
||||
email = f"{username}@x.rmi" # X API v2 doesn't always return email
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": x_user.get("name", username),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?token={jwt_token}&provider=x")
|
||||
|
||||
|
||||
# ── 2FA / TOTP Endpoints ──
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,412 @@
|
|||
"""OAuth flows - re-exports from app.domains.auth.
|
||||
"""OAuth + Telegram auth endpoints.
|
||||
|
||||
Phase 3B of AUDIT-2026-Q3.md.
|
||||
|
||||
Public API:
|
||||
- google_auth_url, google_callback, google_callback_post
|
||||
- github_auth_url, github_callback
|
||||
- x_auth_url, x_callback
|
||||
- telegram_auth
|
||||
"""
|
||||
from app.auth import ( # noqa: F401
|
||||
github_auth_url,
|
||||
github_callback,
|
||||
google_auth_url,
|
||||
google_callback,
|
||||
google_callback_post,
|
||||
telegram_auth,
|
||||
x_auth_url,
|
||||
x_callback,
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from app.core.redis import get_redis
|
||||
from app.domains.auth.jwt import _create_jwt, _derive_user_id
|
||||
from app.domains.auth.schemas import (
|
||||
GoogleAuthResponse,
|
||||
TelegramAuthRequest,
|
||||
WalletAuthResponse,
|
||||
)
|
||||
from app.domains.auth.store import _get_user, _get_user_by_email
|
||||
|
||||
router = APIRouter(tags=["auth"])
|
||||
|
||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "https://rugmunch.io")
|
||||
|
||||
|
||||
@router.get("/google/url", response_model=GoogleAuthResponse)
|
||||
async def google_auth_url():
|
||||
"""Get Google OAuth URL."""
|
||||
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
||||
redirect_uri = os.getenv(
|
||||
"GOOGLE_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/google/callback"
|
||||
)
|
||||
|
||||
if not client_id:
|
||||
return {"url": "/auth/callback?error=google_not_configured"}
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"scope": "openid email profile",
|
||||
"access_type": "offline",
|
||||
"prompt": "consent",
|
||||
}
|
||||
url = f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
|
||||
return {"url": url}
|
||||
|
||||
|
||||
@router.get("/google/callback")
|
||||
async def google_callback(request: Request):
|
||||
"""Handle Google OAuth redirect - exchanges code, creates user, redirects to frontend with token."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
code = request.query_params.get("code")
|
||||
error = request.query_params.get("error")
|
||||
|
||||
if error:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error={error}")
|
||||
|
||||
if not code:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=missing_code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv(
|
||||
"GOOGLE_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/google/callback"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=not_configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=token_exchange_failed")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
resp = await client.get(
|
||||
"https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=userinfo_failed")
|
||||
|
||||
google_user = resp.json()
|
||||
email = google_user.get("email")
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": google_user.get("name", email),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?token={jwt_token}&provider=google")
|
||||
|
||||
|
||||
@router.post("/google/callback", response_model=WalletAuthResponse)
|
||||
async def google_callback_post(request: Request):
|
||||
"""Legacy POST endpoint for manual code exchange."""
|
||||
body = await request.json()
|
||||
code = body.get("code")
|
||||
|
||||
if not code:
|
||||
raise HTTPException(status_code=400, detail="Missing authorization code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv(
|
||||
"GOOGLE_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/google/callback"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise HTTPException(status_code=500, detail="Google OAuth not configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=400, detail="Failed to exchange code")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
resp = await client.get(
|
||||
"https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=400, detail="Failed to get user info")
|
||||
|
||||
google_user = resp.json()
|
||||
email = google_user.get("email")
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": google_user.get("name", email),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": jwt_token,
|
||||
"refresh_token": jwt_token,
|
||||
"user": {
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"display_name": user.get("display_name", email),
|
||||
"wallet_address": None,
|
||||
"wallet_chain": None,
|
||||
"tier": user.get("tier", "FREE"),
|
||||
"role": user.get("role", "USER"),
|
||||
"created_at": user.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.post("/telegram", response_model=WalletAuthResponse)
|
||||
async def telegram_auth(req: TelegramAuthRequest):
|
||||
"""Authenticate via Telegram Web App data."""
|
||||
bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
if not bot_token:
|
||||
raise HTTPException(status_code=500, detail="Telegram auth not configured")
|
||||
|
||||
data_check = {
|
||||
"id": str(req.id),
|
||||
"auth_date": str(req.auth_date),
|
||||
}
|
||||
if req.first_name:
|
||||
data_check["first_name"] = req.first_name
|
||||
if req.last_name:
|
||||
data_check["last_name"] = req.last_name
|
||||
if req.username:
|
||||
data_check["username"] = req.username
|
||||
if req.photo_url:
|
||||
data_check["photo_url"] = req.photo_url
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
sorted_data = "\n".join(f"{k}={v}" for k, v in sorted(data_check.items()))
|
||||
secret = hashlib.sha256(bot_token.encode()).digest()
|
||||
expected_hash = hmac.new(secret, sorted_data.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(req.hash, expected_hash):
|
||||
raise HTTPException(status_code=401, detail="Invalid Telegram auth data")
|
||||
|
||||
telegram_user_id = f"tg:{req.id}"
|
||||
user = _get_user(telegram_user_id)
|
||||
|
||||
if not user:
|
||||
display_name = (
|
||||
f"{req.first_name or ''} {req.last_name or ''}".strip()
|
||||
or req.username
|
||||
or f"User{req.id}"
|
||||
)
|
||||
email = f"{req.id}@telegram.rmi"
|
||||
|
||||
user = {
|
||||
"id": telegram_user_id,
|
||||
"email": email,
|
||||
"display_name": display_name,
|
||||
"telegram_id": req.id,
|
||||
"telegram_username": req.username,
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", telegram_user_id, json.dumps(user))
|
||||
r.hset("rmi:users:telegram", str(req.id), telegram_user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": jwt_token,
|
||||
"refresh_token": jwt_token,
|
||||
"user": {
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"display_name": user.get("display_name"),
|
||||
"wallet_address": None,
|
||||
"wallet_chain": None,
|
||||
"tier": user.get("tier", "FREE"),
|
||||
"role": user.get("role", "USER"),
|
||||
"created_at": user.get("created_at"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/github/url", response_model=GoogleAuthResponse)
|
||||
async def github_auth_url():
|
||||
"""Get GitHub OAuth URL."""
|
||||
client_id = os.getenv("GITHUB_CLIENT_ID")
|
||||
redirect_uri = os.getenv(
|
||||
"GITHUB_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/github/callback"
|
||||
)
|
||||
|
||||
if not client_id:
|
||||
return {"url": "/auth/callback?error=github_not_configured"}
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"scope": "user:email",
|
||||
}
|
||||
url = f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
||||
return {"url": url}
|
||||
|
||||
|
||||
@router.get("/github/callback")
|
||||
async def github_callback(request: Request):
|
||||
"""Handle GitHub OAuth redirect - exchanges code, creates user, redirects to frontend with token."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
code = request.query_params.get("code")
|
||||
error = request.query_params.get("error")
|
||||
|
||||
if error:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error={error}")
|
||||
|
||||
if not code:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=missing_code")
|
||||
|
||||
import httpx
|
||||
|
||||
client_id = os.getenv("GITHUB_CLIENT_ID")
|
||||
client_secret = os.getenv("GITHUB_CLIENT_SECRET")
|
||||
redirect_uri = os.getenv(
|
||||
"GITHUB_REDIRECT_URI", "https://rugmunch.io/api/v1/auth/github/callback"
|
||||
)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=not_configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"https://github.com/login/oauth/access_token",
|
||||
data={
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=token_exchange_failed")
|
||||
|
||||
tokens = resp.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
resp = await client.get(
|
||||
"https://api.github.com/user",
|
||||
headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?error=userinfo_failed")
|
||||
|
||||
github_user = resp.json()
|
||||
email = github_user.get("email")
|
||||
|
||||
if not email:
|
||||
resp = await client.get(
|
||||
"https://api.github.com/user/emails",
|
||||
headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
emails = resp.json()
|
||||
primary = next((e for e in emails if e.get("primary")), None)
|
||||
email = (
|
||||
primary.get("email")
|
||||
if primary
|
||||
else (emails[0].get("email") if emails else None)
|
||||
)
|
||||
|
||||
if not email:
|
||||
email = f"{github_user.get('login', 'github_user')}@github.rmi"
|
||||
|
||||
user = _get_user_by_email(email)
|
||||
if not user:
|
||||
user_id = _derive_user_id(email)
|
||||
user = {
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"display_name": github_user.get("name", github_user.get("login", email)),
|
||||
"tier": "FREE",
|
||||
"role": "USER",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"xp": 0,
|
||||
"level": 1,
|
||||
"badges": [],
|
||||
"scans_remaining": 5,
|
||||
"scans_used": 0,
|
||||
}
|
||||
r = get_redis()
|
||||
r.hset("rmi:users", user_id, json.dumps(user))
|
||||
r.hset("rmi:users:email", email.lower(), user_id)
|
||||
|
||||
jwt_token = _create_jwt(
|
||||
user["id"], user["email"], user.get("tier", "FREE"), user.get("role", "USER")
|
||||
)
|
||||
return RedirectResponse(f"{FRONTEND_URL}/auth/callback?token={jwt_token}&provider=github")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue