Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
74e4986
openspec初始化
hhhhsc701 Apr 9, 2026
67124f9
oauth spec开发结果
hhhhsc701 Apr 9, 2026
f75a08a
oauth 单元测试
hhhhsc701 Apr 10, 2026
695ff84
oauth 重定向修复
hhhhsc701 Apr 10, 2026
cdcc8da
oauth 重定向修复
hhhhsc701 Apr 13, 2026
61f54f2
oauth 重定向修复
hhhhsc701 Apr 13, 2026
35cf40b
oauth 抽象实现
hhhhsc701 Apr 13, 2026
77bea00
gde provider
hhhhsc701 Apr 13, 2026
420e8a9
gde provider
hhhhsc701 Apr 13, 2026
b60e0eb
enhance unlink_account logic to check for password authentication bef…
hhhhsc701 Apr 13, 2026
e0a14b7
refactor OAuthAccountsSection to load enabled providers and improve a…
hhhhsc701 Apr 13, 2026
bb3569e
add OAuth linking functionality with state management and error handling
hhhhsc701 Apr 13, 2026
7c2629a
refactor OAuth account deletion logic to use direct deletion and upda…
hhhhsc701 Apr 14, 2026
97d96e9
update GDE OAuth configuration to use environment variables for URLs …
hhhhsc701 Apr 14, 2026
7747865
add SSL verification configuration for OAuth requests and update cont…
hhhhsc701 Apr 14, 2026
93f7ebf
remove hardcoded OAuth credentials from const.py and update .env.example
hhhhsc701 Apr 14, 2026
ea91d9a
remove avatar_url references from user info handling and update email…
hhhhsc701 Apr 14, 2026
0fc6745
refactor user identity handling in OAuth account unlinking logic
hhhhsc701 Apr 14, 2026
3c75afb
update OAuthAccountsSection to simplify display logic for linked acco…
hhhhsc701 Apr 14, 2026
d6e3dfb
refactor OAuth user binding logic to check for existing accounts befo…
hhhhsc701 Apr 14, 2026
1196e6d
删除冗余文件
hhhhsc701 Apr 14, 2026
fd2d187
删除冗余文件
hhhhsc701 Apr 14, 2026
e2d423c
add user OAuth account table and update trigger for third-party logins
hhhhsc701 Apr 14, 2026
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
7 changes: 6 additions & 1 deletion backend/apps/config_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from apps.vectordatabase_app import router as vectordatabase_router
from apps.dify_app import router as dify_router
from apps.idata_app import router as idata_router
from apps.file_management_app import file_management_config_router as file_manager_router
from apps.file_management_app import (
file_management_config_router as file_manager_router,
)
from apps.image_app import router as proxy_router
from apps.knowledge_summary_app import router as summary_router
from apps.mock_user_management_app import router as mock_user_management_router
from apps.model_managment_app import router as model_manager_router
from apps.oauth_app import router as oauth_router
from apps.prompt_app import router as prompt_router
from apps.remote_mcp_app import router as remote_mcp_router
from apps.skill_app import router as skill_router
Expand Down Expand Up @@ -51,6 +54,8 @@
logger.info("Normal mode - using real user management router")
app.include_router(user_management_router)

app.include_router(oauth_router)

app.include_router(summary_router)
app.include_router(prompt_router)
app.include_router(skill_router)
Expand Down
295 changes: 295 additions & 0 deletions backend/apps/oauth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import logging

from fastapi import APIRouter, Header, HTTPException
from fastapi.responses import JSONResponse, RedirectResponse
from http import HTTPStatus
from typing import Optional

from consts.exceptions import OAuthLinkError, OAuthProviderError, UnauthorizedError
from consts.oauth_providers import get_all_provider_definitions
from services.oauth_service import (
create_or_update_oauth_account,
ensure_user_tenant_exists,
exchange_code_for_provider_token,
get_authorize_url,
get_enabled_providers,
get_provider_user_info,
list_linked_accounts,
unlink_account,
)
from utils.auth_utils import (
calculate_expires_at,
generate_session_jwt,
get_current_user_id,
)

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/user/oauth", tags=["oauth"])


@router.get("/providers")
async def get_providers():
providers = get_enabled_providers()
return JSONResponse(
status_code=HTTPStatus.OK,
content={"message": "success", "data": providers},
)


@router.get("/authorize")
async def authorize(provider: str):
try:
url = get_authorize_url(provider)
return RedirectResponse(url=url, status_code=HTTPStatus.FOUND)
except OAuthProviderError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"OAuth authorize failed: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="OAuth authorization failed",
)


@router.get("/link")
async def link(provider: str, authorization: Optional[str] = Header(None)):

Check failure on line 55 in backend/apps/oauth_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2LR-2856xqTh8S_93Z&open=AZ2LR-2856xqTh8S_93Z&pullRequest=2775
if not authorization:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Not logged in")

Check failure on line 57 in backend/apps/oauth_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Not logged in" 6 times.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2LR-2856xqTh8S_93Y&open=AZ2LR-2856xqTh8S_93Y&pullRequest=2775

try:
user_id, _ = get_current_user_id(authorization)
url = get_authorize_url(provider, link_user_id=user_id)
return RedirectResponse(url=url, status_code=HTTPStatus.FOUND)
except UnauthorizedError:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Not logged in")
except OAuthProviderError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"OAuth link failed: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="OAuth link failed",
)


@router.get("/callback")
async def callback(

Check failure on line 76 in backend/apps/oauth_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 49 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2LR-2856xqTh8S_93a&open=AZ2LR-2856xqTh8S_93a&pullRequest=2775
provider: str,
code: str = "",
state: str = "",
error: Optional[str] = None,
error_description: Optional[str] = None,
):
if error:
return JSONResponse(
status_code=HTTPStatus.BAD_REQUEST,
content={
"message": "OAuth provider returned an error",
"data": {
"oauth_error": error,
"oauth_error_description": error_description or "Unknown error",
},
},
)

if not code:
return JSONResponse(
status_code=HTTPStatus.BAD_REQUEST,
content={
"message": "No authorization code received",
"data": {
"oauth_error": "no_code",
"oauth_error_description": "No authorization code received",
},
},
)

if provider not in get_all_provider_definitions():
return JSONResponse(
status_code=HTTPStatus.BAD_REQUEST,
content={
"message": "Unsupported OAuth provider",
"data": {
"oauth_error": "unsupported_provider",
"oauth_error_description": f"Provider '{provider}' is not supported",
},
},
)

from services.oauth_service import parse_state

state_info = parse_state(state)
link_user_id = state_info.get("link_user_id", "")

try:
token_data = exchange_code_for_provider_token(provider, code)
provider_access_token = token_data["access_token"]

user_info = get_provider_user_info(
provider,
provider_access_token,
openid=token_data.get("openid", ""),
)

provider_user_id = user_info["id"]
email = user_info["email"]
username = user_info["username"]

from utils.auth_utils import get_supabase_admin_client
from services.oauth_service import get_oauth_account_by_provider

if link_user_id:
supabase_user_id = link_user_id
else:
# First check if this OAuth account is already bound to a user
existing_binding = get_oauth_account_by_provider(provider, provider_user_id)
if existing_binding:
supabase_user_id = existing_binding["user_id"]
else:
# No binding found, search/create user by email in Supabase
admin_client = get_supabase_admin_client()
if not admin_client:
raise RuntimeError("Supabase admin client not available")

supabase_user_id = None
page = 1
while True:
users_resp = admin_client.auth.admin.list_users(
page=page, per_page=100
)
users = users_resp if len(users_resp) > 0 else []
if not users:
break
for u in users:
if u.email and u.email.lower() == email.lower():
supabase_user_id = u.id
break
if supabase_user_id:
break
if len(users) < 100:
break
page += 1

if not supabase_user_id:
if not email:
email = f"{provider}_{provider_user_id}@oauth.nexent"
create_resp = admin_client.auth.admin.create_user(
{
"email": email,
"email_confirm": True,
"user_metadata": {
"full_name": username,
"provider": provider,
},
}
)
supabase_user_id = create_resp.user.id

ensure_user_tenant_exists(user_id=supabase_user_id, email=email)

create_or_update_oauth_account(
user_id=supabase_user_id,
provider=provider,
provider_user_id=provider_user_id,
email=email,
username=username,
)

expiry_seconds = 3600
jwt_token = generate_session_jwt(supabase_user_id, expires_in=expiry_seconds)
expires_at = calculate_expires_at(jwt_token)

return JSONResponse(
status_code=HTTPStatus.OK,
content={
"message": "OAuth login successful",
"data": {
"user": {
"id": str(supabase_user_id),
"email": email,
},
"session": {
"access_token": jwt_token,
"refresh_token": "",
"expires_at": expires_at,
"expires_in_seconds": expiry_seconds,
},
},
},
)

except Exception as e:
logger.error(f"OAuth callback failed for provider={provider}: {e}")

Check warning on line 222 in backend/apps/oauth_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not log user-controlled data.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2LR-2856xqTh8S_93d&open=AZ2LR-2856xqTh8S_93d&pullRequest=2775
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
content={
"message": "OAuth login failed",
"data": {
"oauth_error": "callback_failed",
"oauth_error_description": "OAuth login failed",
},
},
)


@router.get("/accounts")
async def get_accounts(authorization: Optional[str] = Header(None)):

Check failure on line 236 in backend/apps/oauth_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2LR-2856xqTh8S_93b&open=AZ2LR-2856xqTh8S_93b&pullRequest=2775
if not authorization:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Not logged in")

try:
user_id, _ = get_current_user_id(authorization)
accounts = list_linked_accounts(user_id)
return JSONResponse(
status_code=HTTPStatus.OK,
content={"message": "success", "data": accounts},
)
except UnauthorizedError:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Not logged in")
except Exception as e:
logger.error(f"Failed to get OAuth accounts: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to get OAuth accounts",
)


@router.delete("/accounts/{provider}")
async def delete_account(provider: str, authorization: Optional[str] = Header(None)):

Check failure on line 258 in backend/apps/oauth_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ2LR-2856xqTh8S_93c&open=AZ2LR-2856xqTh8S_93c&pullRequest=2775
if not authorization:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Not logged in")

try:
user_id, _ = get_current_user_id(authorization)

has_password_auth = False
from utils.auth_utils import get_supabase_admin_client

admin_client = get_supabase_admin_client()
if admin_client:
try:
user_resp = admin_client.auth.admin.get_user_by_id(user_id)
user_metadata = getattr(user_resp.user, "user_metadata", {}) or {}
signup_provider = user_metadata.get("provider", "email")
has_password_auth = signup_provider == "email"
except Exception as e:
logger.warning(f"Failed to check user identities for {user_id}: {e}")

unlink_account(user_id, provider, has_password_auth=has_password_auth)
return JSONResponse(
status_code=HTTPStatus.OK,
content={
"message": "success",
"data": {"provider": provider, "unlinked": True},
},
)
except OAuthLinkError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except UnauthorizedError:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Not logged in")
except Exception as e:
logger.error(f"Failed to unlink OAuth account: {e}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to unlink OAuth account",
)
Loading
Loading