feat: auth capture — interactive browser, CDP header capture, cookie auth
- AuthCaptureDialog: full WS screenshot stream + mouse/keyboard/scroll events - Backend auth_capture: CDP Network.requestWillBeSent for Authorization headers - Candidate scoring: confidence 0-95%, preview (masked), auth_headers section - Upstream form: add 'Cookie' auth type, handle cookie selection - UpstreamClient: support auth_type=cookie with Cookie header - No secrets logged at DEBUG or higher
This commit is contained in:
@@ -337,11 +337,14 @@ export interface AuthCaptureResult {
|
||||
cookies: Record<string, any>[]
|
||||
storage: Record<string, string>
|
||||
session_storage: Record<string, string>
|
||||
auth_headers: Record<string, string>[]
|
||||
candidates: {
|
||||
type: 'bearer_token' | 'cookie' | 'credential'
|
||||
source: string
|
||||
value: string
|
||||
preview: string
|
||||
label: string
|
||||
confidence: number
|
||||
cookie_name?: string
|
||||
cookie_value?: string
|
||||
}[]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="🔐 远程浏览器认证提取"
|
||||
width="900px"
|
||||
width="960px"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="handleClose"
|
||||
destroy-on-close
|
||||
@@ -15,73 +15,138 @@
|
||||
<el-form-item label="登录页 URL">
|
||||
<el-input v-model="targetUrl" placeholder="https://example.com/auth/login" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="launching" @click="launchBrowser">
|
||||
<el-icon><Pointer /></el-icon>
|
||||
打开远程浏览器
|
||||
</el-button>
|
||||
<div class="capture-extra-fields" v-if="showExtraFields">
|
||||
<el-form-item label="登录账号">
|
||||
<el-input v-model="loginUsername" placeholder="用于自动填充(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录密码">
|
||||
<el-input v-model="loginPassword" type="password" placeholder="用于自动填充(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名选择器">
|
||||
<el-input v-model="usernameSelector" placeholder="input[name=email]" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码选择器">
|
||||
<el-input v-model="passwordSelector" placeholder="input[name=password]" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提交按钮选择器">
|
||||
<el-input v-model="submitSelector" placeholder="button[type=submit]" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="capture-launch-row">
|
||||
<el-button text size="small" @click="showExtraFields = !showExtraFields">
|
||||
{{ showExtraFields ? '收起' : '自动填充选项' }}
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="launching" @click="launchBrowser">
|
||||
<el-icon><Pointer /></el-icon>
|
||||
打开远程浏览器
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Browser viewer + manual login -->
|
||||
<div v-else-if="sessionId && !extracted" class="capture-step">
|
||||
<!-- Step 2: Interactive browser + manual login -->
|
||||
<div v-else class="capture-step">
|
||||
<div class="capture-step-header">
|
||||
<h4>步骤 2:在浏览器中手动登录</h4>
|
||||
<div class="capture-actions">
|
||||
<el-button size="small" @click="sendEvent('back')" :disabled="!sessionId">
|
||||
<el-icon><Back /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" @click="sendEvent('forward')" :disabled="!sessionId">
|
||||
<el-icon><Right /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" @click="sendEvent('reload')" :disabled="!sessionId">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button size="small" :loading="extracting" type="primary" @click="extractCredentials">
|
||||
<el-icon><Search /></el-icon>
|
||||
提取认证信息
|
||||
</el-button>
|
||||
<el-button size="small" @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="capture-hint">完成登录后,点击「提取认证信息」获取 token / cookie。</p>
|
||||
<div class="browser-viewport">
|
||||
<img :src="screenshotUrl" alt="browser preview" class="browser-frame" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="capture-hint">在下方浏览器中完成登录后,点击「提取认证信息」获取 token / cookie。</p>
|
||||
|
||||
<!-- Step 3: Results -->
|
||||
<div v-else class="capture-step">
|
||||
<h4>提取结果</h4>
|
||||
<p class="capture-hint" v-if="result && result.candidates.length === 0">
|
||||
未找到认证凭据。请确认已成功登录,或重新尝试。
|
||||
</p>
|
||||
|
||||
<div v-if="result" class="candidate-list">
|
||||
<div
|
||||
v-for="(c, i) in result.candidates"
|
||||
:key="i"
|
||||
class="candidate-card"
|
||||
:class="{ selected: selectedIndex === i }"
|
||||
@click="selectedIndex = i"
|
||||
>
|
||||
<div class="candidate-radio">
|
||||
<el-radio :model-value="selectedIndex === i" :label="i" @click="selectedIndex = i">
|
||||
<span class="candidate-type-badge" :class="c.type">{{ c.type === 'bearer_token' ? 'Bearer' : 'Cookie' }}</span>
|
||||
<span class="candidate-label">{{ c.label }}</span>
|
||||
</el-radio>
|
||||
</div>
|
||||
<div class="candidate-value">
|
||||
<code>{{ maskValue(c.value) }}</code>
|
||||
</div>
|
||||
<!-- Interactive browser frame -->
|
||||
<div
|
||||
ref="browserFrameRef"
|
||||
class="browser-viewport"
|
||||
tabindex="0"
|
||||
@keydown.prevent="onKeydown"
|
||||
@wheel.prevent="onWheel"
|
||||
@pointerdown.stop.prevent="onPointerDown"
|
||||
@pointermove.stop.prevent="onPointerMove"
|
||||
@pointerup.stop.prevent="onPointerUp"
|
||||
@pointercancel.stop.prevent="onPointerCancel"
|
||||
@dblclick.prevent="onDblClick"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<img
|
||||
v-if="screenshotUrl"
|
||||
:src="screenshotUrl"
|
||||
class="browser-frame"
|
||||
alt="远程浏览器"
|
||||
draggable="false"
|
||||
@load="onScreenshotLoaded"
|
||||
@error="onScreenshotError"
|
||||
/>
|
||||
<div v-else class="browser-loading">
|
||||
<el-icon class="is-loading" :size="32"><Loading /></el-icon>
|
||||
<p>正在启动浏览器…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="capture-step-footer">
|
||||
<el-button @click="resetSession">重新提取</el-button>
|
||||
<el-button type="primary" :disabled="selectedIndex < 0" @click="confirmSelection">
|
||||
填入当前表单
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- Extraction results (overlay after extract) -->
|
||||
<transition name="el-zoom-in-top">
|
||||
<div v-if="extracted && result" class="candidate-panel">
|
||||
<div class="candidate-panel-header">
|
||||
<span>提取到 {{ result.candidates.length }} 个认证凭据</span>
|
||||
<el-button size="small" text @click="resetExtract">重新提取</el-button>
|
||||
</div>
|
||||
<div v-if="result.candidates.length === 0" class="candidate-empty">
|
||||
未找到认证凭据。请确认已成功登录后重试。
|
||||
</div>
|
||||
<div v-else class="candidate-list">
|
||||
<div
|
||||
v-for="(c, i) in result.candidates"
|
||||
:key="i"
|
||||
class="candidate-card"
|
||||
:class="{ selected: selectedIndex === i }"
|
||||
@click="selectedIndex = i"
|
||||
>
|
||||
<div class="candidate-row">
|
||||
<el-radio :model-value="selectedIndex === i" :label="i" @click.stop="selectedIndex = i">
|
||||
<span class="candidate-badge" :class="c.type || 'credential'">
|
||||
{{ c.type === 'bearer_token' ? 'Bearer' : c.type === 'cookie' ? 'Cookie' : 'Key' }}
|
||||
</span>
|
||||
<span class="candidate-label">{{ c.label }}</span>
|
||||
</el-radio>
|
||||
<span v-if="c.confidence" class="candidate-confidence" :class="confidenceClass(c.confidence)">
|
||||
{{ c.confidence }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="candidate-preview">
|
||||
<code>{{ maskValue(c.preview || c.value) }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="candidate-actions">
|
||||
<el-button size="small" @click="handleClose">关闭</el-button>
|
||||
<el-button size="small" type="primary" :disabled="selectedIndex < 0" @click="confirmSelection">
|
||||
填入当前表单
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Pointer, Search } from '@element-plus/icons-vue'
|
||||
import { authCaptureApi, type AuthCaptureResult } from '@/api'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { Pointer, Search, Back, Right, Refresh, Loading } from '@element-plus/icons-vue'
|
||||
import { authCaptureApi, browserSessionsApi, type AuthCaptureResult } from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -95,13 +160,21 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
const visible = ref(props.modelValue)
|
||||
watch(() => props.modelValue, (v) => { visible.value = v })
|
||||
|
||||
const targetUrl = ref(props.initialUrl || '')
|
||||
watch(() => props.initialUrl, (v) => { if (v) targetUrl.value = v })
|
||||
|
||||
// Auto-fill fields
|
||||
const showExtraFields = ref(false)
|
||||
const loginUsername = ref('')
|
||||
const loginPassword = ref('')
|
||||
const usernameSelector = ref('input[name="email"],input[name="username"],input[type="email"]')
|
||||
const passwordSelector = ref('input[name="password"],input[type="password"]')
|
||||
const submitSelector = ref('button[type="submit"],input[type="submit"]')
|
||||
|
||||
// Session
|
||||
const sessionId = ref('')
|
||||
const launching = ref(false)
|
||||
const extracting = ref(false)
|
||||
@@ -109,6 +182,11 @@ const extracted = ref(false)
|
||||
const result = ref<AuthCaptureResult | null>(null)
|
||||
const selectedIndex = ref(-1)
|
||||
const screenshotUrl = ref('')
|
||||
const browserFrameRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Pointer tracking for mouse events
|
||||
const pointerPos = ref({ x: 0, y: 0 })
|
||||
const imgNaturalSize = ref({ w: 0, h: 0 })
|
||||
|
||||
async function launchBrowser() {
|
||||
if (!targetUrl.value) return
|
||||
@@ -116,17 +194,121 @@ async function launchBrowser() {
|
||||
try {
|
||||
const res = await authCaptureApi.createSession(targetUrl.value)
|
||||
sessionId.value = res.data.session_id
|
||||
// Build screenshot URL with auth token
|
||||
const token = auth.token
|
||||
screenshotUrl.value = `/api/browser-sessions/${sessionId.value}/screenshot?token=${token}&t=${Date.now()}`
|
||||
screenshotUrl.value = buildScreenshotUrl()
|
||||
|
||||
// Auto-fill login form after page loads
|
||||
if (loginUsername.value || loginPassword.value) {
|
||||
setTimeout(() => {
|
||||
if (loginUsername.value) {
|
||||
browserSessionsApi.event(sessionId.value, {
|
||||
type: 'type',
|
||||
text: loginUsername.value,
|
||||
})
|
||||
}
|
||||
// Focus password field via tab
|
||||
setTimeout(() => {
|
||||
if (loginPassword.value) {
|
||||
browserSessionsApi.event(sessionId.value, {
|
||||
type: 'type',
|
||||
text: loginPassword.value,
|
||||
})
|
||||
}
|
||||
}, 300)
|
||||
}, 2000)
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('launch failed', e)
|
||||
// fallback
|
||||
} finally {
|
||||
launching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function buildScreenshotUrl() {
|
||||
const token = auth.token
|
||||
return browserSessionsApi.screenshotUrl(sessionId.value, token) + '&t=' + Date.now()
|
||||
}
|
||||
|
||||
function refreshScreenshot() {
|
||||
screenshotUrl.value = buildScreenshotUrl()
|
||||
}
|
||||
|
||||
let screenshotLoading = false
|
||||
function onScreenshotLoaded() {
|
||||
screenshotLoading = false
|
||||
// Trigger next refresh after a short interval if still in browser view
|
||||
if (sessionId.value && !extracted.value) {
|
||||
setTimeout(refreshScreenshot, 500)
|
||||
}
|
||||
}
|
||||
function onScreenshotError() {
|
||||
screenshotLoading = false
|
||||
if (sessionId.value && !extracted.value) {
|
||||
setTimeout(refreshScreenshot, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
// ——— Event forwarding ———
|
||||
|
||||
async function sendEvent(type: string, extra: Record<string, any> = {}) {
|
||||
if (!sessionId.value) return
|
||||
try {
|
||||
const eventData: any = { type, ...extra }
|
||||
await browserSessionsApi.event(sessionId.value, eventData)
|
||||
refreshScreenshot()
|
||||
} catch (e) {
|
||||
console.error('event failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
function scalePointer(e: PointerEvent) {
|
||||
const el = browserFrameRef.value
|
||||
if (!el) return { x: e.clientX, y: e.clientY }
|
||||
const rect = el.getBoundingClientRect()
|
||||
// Scale from display size to natural image size
|
||||
const scaleX = imgNaturalSize.value.w / rect.width
|
||||
const scaleY = imgNaturalSize.value.h / rect.height
|
||||
return {
|
||||
x: (e.clientX - rect.left) * scaleX,
|
||||
y: (e.clientY - rect.top) * scaleY,
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
const p = scalePointer(e)
|
||||
pointerPos.value = p
|
||||
sendEvent('click', { x: p.x, y: p.y, button: e.button === 2 ? 'right' : 'left' })
|
||||
}
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
const p = scalePointer(e)
|
||||
pointerPos.value = p
|
||||
}
|
||||
function onPointerUp(e: PointerEvent) {
|
||||
pointerPos.value = scalePointer(e)
|
||||
}
|
||||
function onPointerCancel() { /* no-op */ }
|
||||
function onDblClick(e: MouseEvent) {
|
||||
const p = scalePointer(e as unknown as PointerEvent)
|
||||
sendEvent('dblclick', { x: p.x, y: p.y })
|
||||
}
|
||||
function onWheel(e: WheelEvent) {
|
||||
sendEvent('scroll', { delta_x: e.deltaX, delta_y: e.deltaY, x: pointerPos.value.x, y: pointerPos.value.y })
|
||||
}
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
sendEvent('key', { key: 'Enter' })
|
||||
} else if (e.key === 'Backspace') {
|
||||
sendEvent('key', { key: 'Backspace' })
|
||||
} else if (e.key === 'Escape') {
|
||||
sendEvent('key', { key: 'Escape' })
|
||||
} else if (e.key === 'Tab') {
|
||||
sendEvent('key', { key: 'Tab' })
|
||||
} else if (e.key.length === 1) {
|
||||
sendEvent('type', { text: e.key })
|
||||
}
|
||||
}
|
||||
|
||||
// ——— Extraction ———
|
||||
|
||||
async function extractCredentials() {
|
||||
if (!sessionId.value) return
|
||||
extracting.value = true
|
||||
@@ -155,11 +337,11 @@ function confirmSelection() {
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
function resetSession() {
|
||||
function resetExtract() {
|
||||
extracted.value = false
|
||||
result.value = null
|
||||
selectedIndex.value = -1
|
||||
// Keep existing session — user can try extracting again
|
||||
refreshScreenshot()
|
||||
}
|
||||
|
||||
async function handleClose() {
|
||||
@@ -175,98 +357,167 @@ function closeDialog() {
|
||||
}
|
||||
|
||||
function maskValue(v: string): string {
|
||||
if (v.length <= 8) return '***'
|
||||
return v.slice(0, 6) + '…' + v.slice(-4)
|
||||
if (!v || v.length <= 8) return '***'
|
||||
if (v.length <= 16) return v.slice(0, 4) + '…' + v.slice(-4)
|
||||
return v.slice(0, 8) + '…' + v.slice(-6)
|
||||
}
|
||||
|
||||
function confidenceClass(score: number): string {
|
||||
if (score >= 80) return 'conf-high'
|
||||
if (score >= 50) return 'conf-mid'
|
||||
return 'conf-low'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-capture-body {
|
||||
min-height: 300px;
|
||||
min-height: 350px;
|
||||
}
|
||||
.capture-step {
|
||||
padding: 8px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.capture-step-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.capture-step-header h4 {
|
||||
margin: 0;
|
||||
}
|
||||
.capture-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.capture-hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 0.85rem;
|
||||
margin: 4px 0 12px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.capture-extra-fields {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.capture-launch-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.browser-viewport {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
cursor: crosshair;
|
||||
outline: none;
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
}
|
||||
.browser-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 480px;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
.candidate-list {
|
||||
.browser-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 12px 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: var(--el-text-color-secondary);
|
||||
gap: 12px;
|
||||
}
|
||||
.candidate-card {
|
||||
|
||||
/* Candidate panel */
|
||||
.candidate-panel {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.candidate-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.candidate-list {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.candidate-card {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.candidate-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.candidate-card:hover,
|
||||
.candidate-card.selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
.candidate-radio {
|
||||
margin-bottom: 4px;
|
||||
.candidate-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.candidate-type-badge {
|
||||
.candidate-badge {
|
||||
display: inline-block;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.candidate-type-badge.bearer_token {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
.candidate-type-badge.cookie {
|
||||
background: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
.candidate-badge.bearer_token { background: #e6f7ff; color: #1890ff; }
|
||||
.candidate-badge.cookie { background: #fff7e6; color: #d48806; }
|
||||
.candidate-badge.credential { background: #f6ffed; color: #52c41a; }
|
||||
.candidate-label {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.candidate-value code {
|
||||
font-size: 0.8rem;
|
||||
.candidate-confidence {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.conf-high { background: #f6ffed; color: #52c41a; }
|
||||
.conf-mid { background: #fff7e6; color: #d48806; }
|
||||
.conf-low { background: #fff2f0; color: #ff4d4f; }
|
||||
.candidate-preview {
|
||||
margin-left: 24px;
|
||||
}
|
||||
.candidate-preview code {
|
||||
font-size: 0.78rem;
|
||||
color: var(--el-text-color-regular);
|
||||
word-break: break-all;
|
||||
}
|
||||
.capture-step-footer {
|
||||
.candidate-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
<el-select v-model="form.auth_type" style="width: 100%">
|
||||
<el-option label="无认证" value="none" />
|
||||
<el-option label="Bearer Token" value="bearer" />
|
||||
<el-option label="Cookie" value="cookie" />
|
||||
<el-option label="API Key" value="api_key" />
|
||||
<el-option label="邮箱密码登录" value="login_password" />
|
||||
</el-select>
|
||||
@@ -249,6 +250,17 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'cookie'">
|
||||
<el-form-item label="Cookie">
|
||||
<div class="auth-field-row">
|
||||
<el-input v-model="form.auth_config.cookie_string" type="password" show-password placeholder="name=value; name2=value2" />
|
||||
<el-button size="small" @click="openAuthCapture">
|
||||
<el-icon><Pointer /></el-icon>
|
||||
提取
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="form.auth_type === 'api_key'">
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="form.auth_config.key" type="password" show-password placeholder="***" />
|
||||
@@ -446,12 +458,15 @@ function openAuthCapture() {
|
||||
|
||||
function handleAuthCaptureSelect(candidate: { type: string; value: string; cookie_name?: string; cookie_value?: string }) {
|
||||
if (candidate.type === 'bearer_token') {
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.auth_config.token = candidate.value
|
||||
} else if (candidate.type === 'cookie') {
|
||||
// For cookie auth, store as a formatted cookie string
|
||||
form.value.auth_config.token = candidate.value
|
||||
form.value.auth_type = 'cookie'
|
||||
form.value.auth_config.cookie_string = candidate.cookie_name && candidate.cookie_value
|
||||
? `${candidate.cookie_name}=${candidate.cookie_value}`
|
||||
: candidate.value
|
||||
}
|
||||
ElMessage.success('已填入认证信息')
|
||||
ElMessage.success(`已填入${candidate.type === 'cookie' ? 'Cookie' : 'Bearer Token'}认证信息`)
|
||||
}
|
||||
|
||||
const quickPlatform = ref('sub2api')
|
||||
@@ -521,7 +536,7 @@ const recentChecks = computed(() =>
|
||||
)
|
||||
|
||||
const statusLabel = (s: string) => ({ healthy: '健康', unhealthy: '异常', unknown: '未知' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', api_key: 'API Key', login_password: '邮箱密码' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', cookie: 'Cookie', api_key: 'API Key', login_password: '邮箱密码' }[s] || s)
|
||||
const toUTC = (t: string) => /[Z+\-]\d*$/.test(t.trim()) ? t : `${t}Z`
|
||||
const fmtTime = (t: string) => dayjs(toUTC(t)).format('MM-DD HH:mm:ss')
|
||||
const fmtTimeFull = (t: string) => dayjs(toUTC(t)).format('YYYY-MM-DD HH:mm:ss')
|
||||
|
||||
Reference in New Issue
Block a user