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:
SmartUp Developer
2026-05-19 09:27:14 +08:00
parent 7cb0ff1608
commit 4c71148ff9
13 changed files with 462 additions and 53 deletions
+14 -3
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
import logging
from typing import Optional
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
@@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/auth-capture", tags=["auth-capture"])
SENSITIVE_CANDIDATE_FIELDS = frozenset({"value", "cookie_value"})
class CaptureSessionCreate(BaseModel):
url: str = Field(..., description="Target login page URL to open in browser")
@@ -41,6 +43,14 @@ class CaptureExtractResponse(BaseModel):
candidates: list[dict] = []
def _sanitize_candidate(candidate: dict[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in candidate.items()
if key not in SENSITIVE_CANDIDATE_FIELDS
}
def _browser_error(exc: Exception) -> HTTPException:
if isinstance(exc, BrowserDependencyError):
return HTTPException(503, str(exc))
@@ -108,8 +118,9 @@ async def extract_credentials(
raise _browser_error(exc)
if not include_raw:
# Strip raw data — only keep curated candidates
return CaptureExtractResponse(candidates=result.get("candidates", []))
# Strip raw data — only keep curated candidates with masked previews
candidates = [_sanitize_candidate(candidate) for candidate in result.get("candidates", [])]
return CaptureExtractResponse(candidates=candidates)
return CaptureExtractResponse(**result)