"""身份源插件共享逻辑:绑定查找、账号 find-or-create、平台 token 签发。 账号落点: - 已有绑定 → 直接映射到既有 users 账号(老用户换身份源登录也是同一账号体系) - 无绑定 → 新建 users 账号(密码为随机值,本插件身份源不走密码登录)+ 绑定记录 签发: - chongming_auth.issue_token —— claims 契约与网关内置登录完全一致, 网关验签 / user_info 透传 / admin 判超管全部无缝生效 """ import logging import secrets from datetime import datetime from chongming_auth import LoginPostResponse, Users, issue_token from chongming_config import get_config from chongming_database import Database from chongming_password import hash_password from sqlalchemy import select from sqlalchemy.exc import IntegrityError import time from .models import IdentityBinding logger = logging.getLogger("plugins.auth_oauth") # 演示身份源建号使用的占位密码(随机、不可用于密码登录) _UNUSABLE_PASSWORD_BYTES = 32 async def _lookup_user(db: Database, provider: str, provider_uid: str) -> Users | None: """按绑定关系定位既有账号;无绑定或账号已消失返回 None。""" async with db.get_session() as session: binding = ( await session.execute( select(IdentityBinding).where( IdentityBinding.provider == provider, # type: ignore IdentityBinding.provider_uid == provider_uid, # type: ignore ) ) ).scalar_one_or_none() if binding is None: return None user = ( await session.execute(select(Users).where(Users.id == binding.user_id)) # type: ignore ).scalar_one_or_none() return user async def _create_user_with_binding( db: Database, provider: str, provider_uid: str, profile: dict ) -> None: """新建 users 账号 + 绑定记录(同一事务)。 profile: {username, email, full_name?, avatar_url?} """ async with db.get_session() as session: user = Users( username=str(profile.get("username", ""))[:50], email=str(profile["email"]), hashed_password=hash_password(secrets.token_urlsafe(_UNUSABLE_PASSWORD_BYTES)), full_name=profile.get("full_name"), avatar_url=profile.get("avatar_url"), created_at=datetime.now(), updated_at=datetime.now(), is_active=True, ) session.add(user) await session.flush() # 取 user.id 供绑定外键 session.add(IdentityBinding( user_id=user.id, provider=provider, provider_uid=provider_uid, )) # type: ignore await session.commit() async def find_or_create_user( db: Database, provider: str, provider_uid: str, profile: dict ) -> tuple[Users | None, str]: """按身份源 find-or-create 账号,返回 (user, message)。 user 非 None 即成功;唯一约束冲突(并发首登 / username·email 被占)时 回退到重查绑定,仍无则返回失败原因。 """ user = await _lookup_user(db, provider, provider_uid) if user is not None: return user, "" try: await _create_user_with_binding(db, provider, provider_uid, profile) except IntegrityError as e: logger.warning("[%s] 建号冲突(%s),尝试复用既有绑定: %s", provider, provider_uid, e) user = await _lookup_user(db, provider, provider_uid) if user is None: return None, "创建用户失败(用户名或邮箱已被占用),请改用其他方式登录" return user, "" def issue_login_response(user: Users) -> LoginPostResponse: """按平台契约签发 token 并组装与内置登录同 shape 的响应。""" security = get_config()["security"] token = issue_token( user, security["secret_key"], security["algorithm"], ) return LoginPostResponse( result="success", token=token, message="登录成功", timestamp=time.time() ) def fail_response(message: str) -> LoginPostResponse: """业务失败(不抛异常):result=fail + 原文案,保持与内置登录同 shape。""" return LoginPostResponse( result="fail", token="", message=message, timestamp=time.time() )