refactor(backend): update API routes and service layer

- Update agent router/service/repository with new endpoints
- Update auth routes with phone-based authentication
- Update users service with new phone lookup
- Update schedule_items with new schemas
- Update message schemas with visibility support
- Update settings with new automation scheduler config
- Update CLI with new commands
- Update tests to match new API contracts
This commit is contained in:
qzl
2026-03-19 18:42:59 +08:00
parent 641d847008
commit f0af44d840
36 changed files with 1083 additions and 1853 deletions
+4 -4
View File
@@ -76,11 +76,11 @@ async def _verify_user_with_supabase(token: str) -> CurrentUser | None:
parsed_id = UUID(user_id)
except ValueError:
return None
email = getattr(user, "email", None)
phone = getattr(user, "phone", None)
role = getattr(user, "role", None)
return CurrentUser(
id=parsed_id,
email=email if isinstance(email, str) else None,
phone=phone if isinstance(phone, str) else None,
role=role if isinstance(role, str) else None,
)
@@ -125,9 +125,9 @@ async def get_current_user(
raise HTTPException(status_code=401, detail="Unauthorized")
logger.debug("JWT validation successful", user_id=str(user_id))
email = payload.get("email") if isinstance(payload.get("email"), str) else None
phone = payload.get("phone") if isinstance(payload.get("phone"), str) else None
role = payload.get("role") if isinstance(payload.get("role"), str) else None
return CurrentUser(id=user_id, email=email, role=role)
return CurrentUser(id=user_id, phone=phone, role=role)
async def get_user_repository(
+1 -1
View File
@@ -38,7 +38,7 @@ class UserRepository(Protocol):
...
async def search_users(self, query: str, limit: int = 20) -> list[Profile]:
"""Search users by username (ilike) or email (exact match)."""
"""Search users by username (ilike) or phone (exact match)."""
...
+63 -31
View File
@@ -21,19 +21,22 @@ if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from schemas.user.context import UserContext
from v1.auth.schemas import UserByEmailResponse
logger = get_logger("v1.users.service")
_EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
_PHONE_QUERY_PATTERN = re.compile(r"^[+()\-\s\d]{4,32}$")
class AuthLookupGateway(Protocol):
async def get_user_id_by_email(self, email: str) -> str | None: ...
async def search_user_ids_by_phone(
self, query: str, limit: int = 20
) -> list[str]: ...
class AuthByEmailGateway(Protocol):
async def get_user_by_email(self, email: str) -> "UserByEmailResponse": ...
class AuthByPhoneGateway(Protocol):
async def search_user_ids_by_phone(
self, query: str, limit: int = 20
) -> list[str]: ...
class UserContextInvalidator(Protocol):
@@ -41,15 +44,14 @@ class UserContextInvalidator(Protocol):
class AuthLookupAdapter:
def __init__(self, gateway: AuthByEmailGateway) -> None:
def __init__(self, gateway: AuthByPhoneGateway) -> None:
self._gateway = gateway
async def get_user_id_by_email(self, email: str) -> str | None:
async def search_user_ids_by_phone(self, query: str, limit: int = 20) -> list[str]:
try:
response = await self._gateway.get_user_by_email(email)
return response.id
return await self._gateway.search_user_ids_by_phone(query, limit=limit)
except HTTPException:
return None
return []
class UserService(BaseService):
@@ -92,11 +94,11 @@ class UserService(BaseService):
if user is None:
raise HTTPException(status_code=404, detail="User not found")
email = self._current_user.email if self._current_user else None
phone = self._current_user.phone if self._current_user else None
return UserContext(
id=str(user.id),
username=user.username,
email=email,
phone=phone,
avatar_url=user.avatar_url,
bio=user.bio,
settings=parse_profile_settings(user.settings),
@@ -152,11 +154,11 @@ class UserService(BaseService):
error=str(exc),
)
email = self._current_user.email if self._current_user else None
phone = self._current_user.phone if self._current_user else None
return UserContext(
id=str(user.id),
username=user.username,
email=email,
phone=phone,
avatar_url=user.avatar_url,
bio=user.bio,
settings=parse_profile_settings(user.settings),
@@ -181,36 +183,59 @@ class UserService(BaseService):
async def search_users(self, request: UserSearchRequest) -> list[UserContext]:
query = request.query.strip()
if _EMAIL_PATTERN.match(query):
return await self._search_by_email(query)
if _looks_like_phone_query(query):
phone_results = await self._search_by_phone(query)
if not query.isdigit():
return phone_results
username_results = await self._search_by_username(query)
if not phone_results:
return username_results
merged_by_id = {result.id: result for result in phone_results}
for result in username_results:
merged_by_id.setdefault(result.id, result)
return list(merged_by_id.values())
return await self._search_by_username(query)
async def _search_by_email(self, email: str) -> list[UserContext]:
async def _search_by_phone(self, phone: str) -> list[UserContext]:
if self._auth_gateway is None:
raise HTTPException(status_code=503, detail="Auth lookup unavailable")
user_id_str = await self._auth_gateway.get_user_id_by_email(email)
if user_id_str is None:
user_id_values = await self._auth_gateway.search_user_ids_by_phone(
phone, limit=20
)
if not user_id_values:
return []
user_ids: list[UUID] = []
for raw_id in user_id_values:
try:
user_ids.append(UUID(raw_id))
except ValueError:
continue
if not user_ids:
return []
try:
user = await self._repository.get_by_user_id(UUID(user_id_str))
users_by_id = await self._repository.get_by_user_ids(user_ids)
except SQLAlchemyError:
raise HTTPException(status_code=503, detail="User store unavailable")
if user is None:
return []
return [
UserContext(
id=str(user.id),
username=user.username,
avatar_url=user.avatar_url,
bio=user.bio,
settings=parse_profile_settings(user.settings),
results: list[UserContext] = []
for user_id in user_ids:
user = users_by_id.get(user_id)
if user is None:
continue
results.append(
UserContext(
id=str(user.id),
username=user.username,
avatar_url=user.avatar_url,
bio=user.bio,
settings=parse_profile_settings(user.settings),
)
)
]
return results
async def _search_by_username(self, query: str) -> list[UserContext]:
try:
@@ -228,3 +253,10 @@ class UserService(BaseService):
)
for user in users
]
def _looks_like_phone_query(query: str) -> bool:
if not _PHONE_QUERY_PATTERN.fullmatch(query):
return False
digits_count = sum(char.isdigit() for char in query)
return digits_count >= 4