feat: one-click upstream auth refresh from custom page viewer
- Add linked_upstream_id to CustomPage model with DB migration
- New POST /api/custom-pages/{pid}/refresh-auth endpoint extracts
credentials from active remote browser and updates linked upstream
- PageViewer toolbar shows key icon button when page has linked upstream
- CustomPages form adds upstream dropdown for remote_browser pages
- Auth capture extracts New-Api-User from localStorage uid/user/self API
- Upstream client sends New-Api-User header in cookie auth mode
- Fix auth capture dialog: transparent background, field persistence,
login URL defaults to base_url/login, focus on click for keyboard input
- Fix upstream test ASCII encoding with non-header characters validation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,19 @@ def _find_token(value: Any) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _clean_auth_header_value(value: Any, field_name: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
if text.startswith("Bearer "):
|
||||
text = text[7:].strip()
|
||||
try:
|
||||
text.encode("latin-1")
|
||||
except UnicodeEncodeError as exc:
|
||||
raise UpstreamError(f"{field_name} contains non-HTTP-header characters; please re-extract and apply the full credential") from exc
|
||||
return text
|
||||
|
||||
|
||||
def _find_user_id(value: Any) -> str:
|
||||
if isinstance(value, dict):
|
||||
for key in ("id", "user_id", "userId"):
|
||||
@@ -232,25 +245,32 @@ class UpstreamClient:
|
||||
if not auth:
|
||||
return headers
|
||||
if self.auth_type == "bearer":
|
||||
token = self.auth_config.get("token", "")
|
||||
token = _clean_auth_header_value(self.auth_config.get("token", ""), "Bearer token")
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif self.auth_type == "api_key":
|
||||
key = self.auth_config.get("key", "")
|
||||
key = _clean_auth_header_value(self.auth_config.get("key", ""), "API key")
|
||||
header = self.auth_config.get("header", "Authorization")
|
||||
if key:
|
||||
headers[header] = key
|
||||
elif self.auth_type == "cookie":
|
||||
cookie_str = self.auth_config.get("cookie_string", "")
|
||||
cookie_str = _clean_auth_header_value(self.auth_config.get("cookie_string", ""), "Cookie")
|
||||
if cookie_str:
|
||||
headers["Cookie"] = cookie_str
|
||||
new_api_user = _clean_auth_header_value(self.auth_config.get("new_api_user", ""), "New-Api-User")
|
||||
if new_api_user:
|
||||
headers["New-Api-User"] = new_api_user
|
||||
elif self.auth_type == "login_password" and self._token:
|
||||
headers["Authorization"] = f"Bearer {self._token}"
|
||||
token = _clean_auth_header_value(self._token, "Login token")
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if self.auth_type == "login_password" and self._new_api_user:
|
||||
headers["New-Api-User"] = self._new_api_user
|
||||
return headers
|
||||
|
||||
def _request(self, method: str, path: str, body: Any = None, auth: bool = True) -> Any:
|
||||
if auth and self.auth_type == "cookie" and "user/self" in path and not self.auth_config.get("new_api_user"):
|
||||
raise UpstreamError("New-API user endpoint requires New-Api-User; re-extract the session cookie after login and save the upstream")
|
||||
url = self._url(path)
|
||||
if body is not None:
|
||||
resp = self._client.request(
|
||||
|
||||
Reference in New Issue
Block a user