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:
co-authored by
Claude Opus 4.7
parent
7cb0ff1608
commit
4c71148ff9
@@ -97,6 +97,12 @@
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.access_mode === 'remote_browser'" label="关联上游">
|
||||
<el-select v-model="form.linked_upstream_id" clearable placeholder="选择要一键刷新凭证的上游" style="width:100%">
|
||||
<el-option v-for="u in upstreamList" :key="u.id" :label="`${u.name} (${u.base_url})`" :value="u.id" />
|
||||
</el-select>
|
||||
<div class="form-hint">关联后可在页面查看器中一键刷新该上游的认证凭证</div>
|
||||
</el-form-item>
|
||||
<div class="login-section">
|
||||
<div class="login-section-head">
|
||||
<span>登录自动填充</span>
|
||||
@@ -158,7 +164,7 @@ import {
|
||||
SetUp, Reading, Cpu, DataLine, Grid, Connection,
|
||||
Ticket, Wallet, Key, Tools, Star, House,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { customPagesApi, type CustomPageAccessMode, type CustomPageData } from '@/api'
|
||||
import { customPagesApi, upstreamsApi, type CustomPageAccessMode, type CustomPageData, type UpstreamData } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -182,6 +188,7 @@ const iconMap: Record<string, any> = {
|
||||
|
||||
// ---- state ----
|
||||
const list = ref<CustomPageData[]>([])
|
||||
const upstreamList = ref<UpstreamData[]>([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
@@ -206,6 +213,7 @@ type PageFormState = {
|
||||
login_autofill_enabled: boolean
|
||||
login_password_configured: boolean
|
||||
login_password_clear: boolean
|
||||
linked_upstream_id: number | null
|
||||
}
|
||||
|
||||
const defaultForm = (): PageFormState => ({
|
||||
@@ -225,6 +233,7 @@ const defaultForm = (): PageFormState => ({
|
||||
login_autofill_enabled: false,
|
||||
login_password_configured: false,
|
||||
login_password_clear: false,
|
||||
linked_upstream_id: null,
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
const rules = {
|
||||
@@ -235,8 +244,9 @@ const rules = {
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await customPagesApi.list()
|
||||
list.value = res.data
|
||||
const [pagesRes, upstreamsRes] = await Promise.all([customPagesApi.list(), upstreamsApi.list()])
|
||||
list.value = pagesRes.data
|
||||
upstreamList.value = upstreamsRes.data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -269,6 +279,7 @@ function openEdit(page: CustomPageData) {
|
||||
login_autofill_enabled: page.login_autofill_enabled,
|
||||
login_password_configured: page.login_password_configured,
|
||||
login_password_clear: false,
|
||||
linked_upstream_id: page.linked_upstream_id ?? null,
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<el-icon><Right /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canRefreshAuth" content="一键刷新上游凭证">
|
||||
<el-button size="small" text type="warning" :loading="refreshingAuth" @click="refreshAuth">
|
||||
<el-icon><Key /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="在新标签页打开">
|
||||
<el-button size="small" text @click="openExternal">
|
||||
<el-icon><TopRight /></el-icon>
|
||||
@@ -209,6 +214,8 @@ type RemoteBrowserErrorState = {
|
||||
|
||||
const pageIcon = computed(() => iconMap[page.value?.icon || 'Link'] || LinkIcon)
|
||||
const isRemoteBrowser = computed(() => page.value?.access_mode === 'remote_browser')
|
||||
const canRefreshAuth = computed(() => isRemoteBrowser.value && page.value?.linked_upstream_id && remoteSession.value)
|
||||
const refreshingAuth = ref(false)
|
||||
const effectivePageId = computed(() => props.pageId ?? Number(route.params.id))
|
||||
const embedded = computed(() => props.embedded)
|
||||
const showRemoteError = computed(() => Boolean(remoteErrorState.value) && !isStartingRemoteBrowser.value)
|
||||
@@ -430,6 +437,23 @@ async function copyRemoteSelection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAuth() {
|
||||
if (!page.value) return
|
||||
refreshingAuth.value = true
|
||||
try {
|
||||
const res = await customPagesApi.refreshAuth(page.value.id)
|
||||
if (res.data.success) {
|
||||
ElMessage.success(res.data.message || '凭证已刷新')
|
||||
} else {
|
||||
ElMessage.warning(res.data.message || '刷新失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '刷新凭证失败')
|
||||
} finally {
|
||||
refreshingAuth.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function remoteViewport() {
|
||||
const rect = remoteFrameRef.value?.getBoundingClientRect()
|
||||
return {
|
||||
|
||||
@@ -270,8 +270,8 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'login_password'">
|
||||
<el-form-item label="登录邮箱">
|
||||
<el-input v-model="form.auth_config.email" placeholder="admin@example.com" />
|
||||
<el-form-item :label="form.auth_config.username_field === 'username' ? '登录账号' : '登录邮箱'">
|
||||
<el-input v-model="form.auth_config.email" :placeholder="form.auth_config.username_field === 'username' ? 'admin' : 'admin@example.com'" />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录密码">
|
||||
<el-input v-model="form.auth_config.password" type="password" show-password placeholder="***" />
|
||||
@@ -409,7 +409,7 @@
|
||||
|
||||
<AuthCaptureDialog
|
||||
v-model="authCaptureVisible"
|
||||
:initial-url="form.base_url"
|
||||
:initial-url="authCaptureInitialUrl"
|
||||
@select="handleAuthCaptureSelect"
|
||||
/>
|
||||
</div>
|
||||
@@ -452,11 +452,17 @@ const rules = {
|
||||
|
||||
const authCaptureVisible = ref(false)
|
||||
|
||||
const authCaptureInitialUrl = computed(() => {
|
||||
const base = (form.value.base_url || '').replace(/\/+$/, '')
|
||||
if (!base) return ''
|
||||
return base + '/login'
|
||||
})
|
||||
|
||||
function openAuthCapture() {
|
||||
authCaptureVisible.value = true
|
||||
}
|
||||
|
||||
function handleAuthCaptureSelect(candidate: { type: string; value: string; cookie_name?: string; cookie_value?: string }) {
|
||||
function handleAuthCaptureSelect(candidate: { type: string; value: string; cookie_name?: string; cookie_value?: string; new_api_user?: string }) {
|
||||
if (candidate.type === 'bearer_token') {
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
@@ -466,6 +472,17 @@ function handleAuthCaptureSelect(candidate: { type: string; value: string; cooki
|
||||
form.value.auth_config.cookie_string = candidate.cookie_name && candidate.cookie_value
|
||||
? `${candidate.cookie_name}=${candidate.cookie_value}`
|
||||
: candidate.value
|
||||
if (candidate.new_api_user) {
|
||||
form.value.auth_config.new_api_user = candidate.new_api_user
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
} else if (quickPlatform.value === 'new-api-user') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
ElMessage.warning('已填入 Cookie,但未提取到 New-Api-User,请重新登录后再提取')
|
||||
}
|
||||
ElMessage.success('已填入 Cookie')
|
||||
} else if (candidate.type === 'api_key') {
|
||||
form.value.auth_type = 'api_key'
|
||||
@@ -495,6 +512,7 @@ function handlePlatformChange(val: string) {
|
||||
form.value.rate_endpoint = '/groups/rates'
|
||||
form.value.auth_type = 'login_password'
|
||||
form.value.auth_config.login_path = '/auth/login'
|
||||
form.value.auth_config.username_field = 'email'
|
||||
} else if (val === 'new-api') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/group/'
|
||||
@@ -506,6 +524,7 @@ function handlePlatformChange(val: string) {
|
||||
form.value.rate_endpoint = '/api/user/self/groups'
|
||||
form.value.auth_type = 'login_password'
|
||||
form.value.auth_config.login_path = '/api/user/login'
|
||||
form.value.auth_config.username_field = 'username'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user