feat: add admin social login bindings
This commit is contained in:
20
admin-frontend/package-lock.json
generated
20
admin-frontend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"element-plus": "^2.11.3",
|
"element-plus": "^2.11.3",
|
||||||
|
"qrcode.vue": "^3.6.0",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
@@ -900,7 +901,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
@@ -1512,15 +1512,13 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash-unified": {
|
"node_modules/lodash-unified": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@@ -1614,7 +1612,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -1656,6 +1653,15 @@
|
|||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.vue": {
|
||||||
|
"version": "3.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.9.1.tgz",
|
||||||
|
"integrity": "sha512-CpHVRz5iveqwRFh+nzzSYV9hPWU6q+YSOKyq5ZievjQIBv4bIIDzajGgtNz/yYSlczjAkYM3GNAQJHwwCukMEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.53.3",
|
"version": "4.53.3",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||||
@@ -1730,7 +1736,6 @@
|
|||||||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -1805,7 +1810,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.25",
|
"@vue/compiler-dom": "3.5.25",
|
||||||
"@vue/compiler-sfc": "3.5.25",
|
"@vue/compiler-sfc": "3.5.25",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"element-plus": "^2.11.3",
|
"element-plus": "^2.11.3",
|
||||||
|
"qrcode.vue": "^3.6.0",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,3 +44,28 @@ export async function reportAdminPasskeyClientError(payload = {}) {
|
|||||||
const { data } = await api.post('/admin/passkeys/client-error', payload)
|
const { data } = await api.post('/admin/passkeys/client-error', payload)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAdminSocialBindings() {
|
||||||
|
const { data } = await api.get('/admin/social-bindings')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAdminSocialLoginUrl(payload = {}) {
|
||||||
|
const { data } = await api.post('/admin/social-login-url', payload)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pollAdminSocialLogin(payload = {}) {
|
||||||
|
const { data } = await api.post('/admin/social-poll', payload)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bindAdminSocialCallback(provider, payload = {}) {
|
||||||
|
const { data } = await api.post(`/admin/social-bindings/${encodeURIComponent(provider)}/callback`, payload)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unbindAdminSocial(provider) {
|
||||||
|
const { data } = await api.delete(`/admin/social-bindings/${encodeURIComponent(provider)}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|||||||
73
admin-frontend/src/pages/AdminSocialBindCallbackPage.vue
Normal file
73
admin-frontend/src/pages/AdminSocialBindCallbackPage.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { bindAdminSocialCallback } from '../api/admin'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const statusText = ref('正在完成绑定')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const hashQuery = String(window.location.hash || '').split('?')[1] || ''
|
||||||
|
const params = new URLSearchParams(window.location.search || hashQuery)
|
||||||
|
const provider = String(params.get('provider') || params.get('type') || '').trim().toLowerCase()
|
||||||
|
const code = String(params.get('code') || '').trim()
|
||||||
|
const routeProvider = String(route.query?.provider || route.query?.type || '').trim().toLowerCase()
|
||||||
|
const routeCode = String(route.query?.code || '').trim()
|
||||||
|
const finalProvider = provider || routeProvider
|
||||||
|
const finalCode = code || routeCode
|
||||||
|
|
||||||
|
if (!finalProvider || !finalCode) {
|
||||||
|
ElMessage.error('快捷登录回调参数不完整')
|
||||||
|
window.location.replace('/yuyx/admin#/settings')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bindAdminSocialCallback(finalProvider, { provider: finalProvider, code: finalCode })
|
||||||
|
ElMessage.success('管理员快捷登录已绑定')
|
||||||
|
window.location.replace('/yuyx/admin#/settings')
|
||||||
|
} catch (error) {
|
||||||
|
const payload = error?.response?.data
|
||||||
|
statusText.value = payload?.error || '快捷登录绑定失败'
|
||||||
|
ElMessage.error(statusText.value)
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.location.replace('/yuyx/admin#/settings')
|
||||||
|
}, 1200)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="callback-wrap">
|
||||||
|
<el-card shadow="never" class="callback-card">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
<div class="callback-text">{{ statusText }}</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.callback-wrap {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callback-card {
|
||||||
|
width: min(420px, 94vw);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callback-text {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import QrcodeVue from 'qrcode.vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createAdminSocialLoginUrl,
|
||||||
createAdminPasskeyOptions,
|
createAdminPasskeyOptions,
|
||||||
createAdminPasskeyVerify,
|
createAdminPasskeyVerify,
|
||||||
|
fetchAdminSocialBindings,
|
||||||
deleteAdminPasskey,
|
deleteAdminPasskey,
|
||||||
fetchAdminPasskeys,
|
fetchAdminPasskeys,
|
||||||
logout,
|
logout,
|
||||||
|
pollAdminSocialLogin,
|
||||||
reportAdminPasskeyClientError,
|
reportAdminPasskeyClientError,
|
||||||
|
unbindAdminSocial,
|
||||||
updateAdminPassword,
|
updateAdminPassword,
|
||||||
updateAdminUsername,
|
updateAdminUsername,
|
||||||
} from '../api/admin'
|
} from '../api/admin'
|
||||||
@@ -26,6 +31,23 @@ const passkeyItems = ref([])
|
|||||||
const passkeyRegisterOptions = ref(null)
|
const passkeyRegisterOptions = ref(null)
|
||||||
const passkeyRegisterOptionsAt = ref(0)
|
const passkeyRegisterOptionsAt = ref(0)
|
||||||
const PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS = 240000
|
const PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS = 240000
|
||||||
|
const socialBindingsLoading = ref(false)
|
||||||
|
const socialBindLoadingProvider = ref('')
|
||||||
|
const socialBindings = ref([])
|
||||||
|
const socialEnabled = ref(false)
|
||||||
|
const qrOpen = ref(false)
|
||||||
|
const qrValue = ref('')
|
||||||
|
const qrProvider = ref('wx')
|
||||||
|
let socialPollTimer = null
|
||||||
|
let socialPollStartedAt = 0
|
||||||
|
|
||||||
|
const providerLabels = {
|
||||||
|
qq: 'QQ',
|
||||||
|
wx: '微信',
|
||||||
|
alipay: '支付宝',
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleSocialBindings = computed(() => socialBindings.value.filter((item) => providerLabels[item.provider]))
|
||||||
|
|
||||||
function validateStrongPassword(value) {
|
function validateStrongPassword(value) {
|
||||||
const text = String(value || '')
|
const text = String(value || '')
|
||||||
@@ -233,8 +255,137 @@ async function removePasskey(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function socialBindRedirectUri() {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.pathname = '/yuyx/admin-social-bind-callback'
|
||||||
|
url.search = ''
|
||||||
|
url.hash = ''
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function socialProviderIcon(provider) {
|
||||||
|
if (provider === 'wx') return '微'
|
||||||
|
if (provider === 'qq') return 'Q'
|
||||||
|
return '支'
|
||||||
|
}
|
||||||
|
|
||||||
|
function socialQrPrompt(provider) {
|
||||||
|
if (provider === 'wx') return '请使用微信扫描二维码完成绑定'
|
||||||
|
if (provider === 'qq') return '请使用 QQ 扫描二维码完成绑定'
|
||||||
|
return '请使用支付宝扫描二维码完成绑定'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSocialPolling() {
|
||||||
|
if (socialPollTimer) {
|
||||||
|
window.clearTimeout(socialPollTimer)
|
||||||
|
socialPollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSocialQr() {
|
||||||
|
stopSocialPolling()
|
||||||
|
qrOpen.value = false
|
||||||
|
qrValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSocialPoll(provider, state, intervalSeconds) {
|
||||||
|
stopSocialPolling()
|
||||||
|
socialPollStartedAt = Date.now()
|
||||||
|
const tick = async () => {
|
||||||
|
if (Date.now() - socialPollStartedAt > 5 * 60 * 1000) {
|
||||||
|
closeSocialQr()
|
||||||
|
ElMessage.warning('二维码已过期,请重新获取')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await pollAdminSocialLogin({ provider, state })
|
||||||
|
if (result?.status === 'authorized' && result?.url) {
|
||||||
|
closeSocialQr()
|
||||||
|
window.location.assign(result.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socialPollTimer = window.setTimeout(tick, Math.max(Number(intervalSeconds || 2), 2) * 1000)
|
||||||
|
} catch (error) {
|
||||||
|
closeSocialQr()
|
||||||
|
const data = error?.response?.data
|
||||||
|
ElMessage.error(data?.error || '扫码状态获取失败,请重新尝试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socialPollTimer = window.setTimeout(tick, Math.max(Number(intervalSeconds || 2), 2) * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSocialBindings() {
|
||||||
|
socialBindingsLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await fetchAdminSocialBindings()
|
||||||
|
socialEnabled.value = Boolean(data?.enabled)
|
||||||
|
socialBindings.value = Array.isArray(data?.items) ? data.items : []
|
||||||
|
} catch {
|
||||||
|
socialEnabled.value = false
|
||||||
|
socialBindings.value = []
|
||||||
|
} finally {
|
||||||
|
socialBindingsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bindSocial(item) {
|
||||||
|
const provider = String(item?.provider || '').trim()
|
||||||
|
if (!provider || socialBindLoadingProvider.value) return
|
||||||
|
socialBindLoadingProvider.value = provider
|
||||||
|
try {
|
||||||
|
const data = await createAdminSocialLoginUrl({
|
||||||
|
provider,
|
||||||
|
redirect_uri: socialBindRedirectUri(),
|
||||||
|
})
|
||||||
|
if (provider !== 'wx') {
|
||||||
|
window.location.assign(data.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const value = data.scan_url || data.qrcode || data.url
|
||||||
|
if (!value || !data.scan_state) {
|
||||||
|
ElMessage.error('微信二维码获取失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qrProvider.value = provider
|
||||||
|
qrValue.value = value
|
||||||
|
qrOpen.value = true
|
||||||
|
scheduleSocialPoll(provider, data.scan_state, data.scan_poll_interval || 2)
|
||||||
|
} catch (error) {
|
||||||
|
const data = error?.response?.data
|
||||||
|
ElMessage.error(data?.error || '获取聚合登录地址失败')
|
||||||
|
} finally {
|
||||||
|
socialBindLoadingProvider.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unbindSocial(item) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定解绑${item?.provider_label || '快捷登录'}吗?`, '解绑快捷登录', {
|
||||||
|
confirmButtonText: '解绑',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await unbindAdminSocial(item.provider)
|
||||||
|
ElMessage.success('已解绑')
|
||||||
|
await loadSocialBindings()
|
||||||
|
} catch (error) {
|
||||||
|
const data = error?.response?.data
|
||||||
|
ElMessage.error(data?.error || '解绑失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPasskeys()
|
loadPasskeys()
|
||||||
|
loadSocialBindings()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopSocialPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -331,6 +482,66 @@ onMounted(() => {
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
|
||||||
|
<div class="section-head">
|
||||||
|
<h3 class="section-title">快捷登录绑定</h3>
|
||||||
|
<el-tag v-if="socialEnabled" size="small" type="success">已启用</el-tag>
|
||||||
|
<el-tag v-else size="small" type="info">未启用</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="!socialEnabled"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
title="聚合登录未启用,请先在系统配置中填写并启用 Space 聚合登录。"
|
||||||
|
show-icon
|
||||||
|
class="help-alert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-loading="socialBindingsLoading">
|
||||||
|
<el-empty v-if="visibleSocialBindings.length === 0" description="暂无可绑定的快捷登录方式" />
|
||||||
|
<div v-else class="social-list">
|
||||||
|
<div v-for="item in visibleSocialBindings" :key="item.provider" class="social-row">
|
||||||
|
<div class="social-provider">
|
||||||
|
<span class="social-icon" :class="`provider-${item.provider}`">{{ socialProviderIcon(item.provider) }}</span>
|
||||||
|
<div class="social-info">
|
||||||
|
<strong>{{ item.provider_label }}</strong>
|
||||||
|
<span v-if="item.bound">{{ item.nickname || '已绑定' }}</span>
|
||||||
|
<span v-else>未绑定</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="social-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="item.bound"
|
||||||
|
type="danger"
|
||||||
|
text
|
||||||
|
@click="unbindSocial(item)"
|
||||||
|
>
|
||||||
|
解绑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:disabled="!socialEnabled"
|
||||||
|
:loading="socialBindLoadingProvider === item.provider"
|
||||||
|
@click="bindSocial(item)"
|
||||||
|
>
|
||||||
|
绑定
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="qrOpen" :title="`${providerLabels[qrProvider]}绑定`" width="min(340px, 92vw)" @close="closeSocialQr">
|
||||||
|
<div class="social-qr-box">
|
||||||
|
<QrcodeVue v-if="qrValue" :value="qrValue" :size="220" level="M" />
|
||||||
|
<div class="social-qr-prompt">{{ socialQrPrompt(qrProvider) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -356,6 +567,18 @@ onMounted(() => {
|
|||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head .section-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.help {
|
.help {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -365,4 +588,104 @@ onMounted(() => {
|
|||||||
.help-alert {
|
.help-alert {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.social-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-row {
|
||||||
|
min-height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(248, 250, 252, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-provider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-wx {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-qq {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-alipay {
|
||||||
|
background: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-info {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-info strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-info span {
|
||||||
|
max-width: min(52vw, 360px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-qr-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-qr-prompt {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #374151;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.social-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ const EmailPage = () => import('../pages/EmailPage.vue')
|
|||||||
const SecurityPage = () => import('../pages/SecurityPage.vue')
|
const SecurityPage = () => import('../pages/SecurityPage.vue')
|
||||||
const SystemPage = () => import('../pages/SystemPage.vue')
|
const SystemPage = () => import('../pages/SystemPage.vue')
|
||||||
const SettingsPage = () => import('../pages/SettingsPage.vue')
|
const SettingsPage = () => import('../pages/SettingsPage.vue')
|
||||||
|
const AdminSocialBindCallbackPage = () => import('../pages/AdminSocialBindCallbackPage.vue')
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{ path: '/social-bind-callback', name: 'admin_social_bind_callback', component: AdminSocialBindCallbackPage },
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: AdminLayout,
|
component: AdminLayout,
|
||||||
|
|||||||
@@ -26,15 +26,21 @@ from db.migrations import migrate_database as _migrate_database
|
|||||||
from db.admin import (
|
from db.admin import (
|
||||||
admin_reset_user_password,
|
admin_reset_user_password,
|
||||||
clean_old_operation_logs,
|
clean_old_operation_logs,
|
||||||
|
delete_admin_social_login_binding,
|
||||||
|
find_admin_social_login_binding,
|
||||||
|
find_admin_social_login_binding_by_identity,
|
||||||
get_admin_by_id,
|
get_admin_by_id,
|
||||||
ensure_default_admin,
|
ensure_default_admin,
|
||||||
get_admin_by_username,
|
get_admin_by_username,
|
||||||
get_hourly_registration_count,
|
get_hourly_registration_count,
|
||||||
get_system_config_raw as _get_system_config_raw,
|
get_system_config_raw as _get_system_config_raw,
|
||||||
get_system_stats,
|
get_system_stats,
|
||||||
|
list_admin_social_login_bindings,
|
||||||
update_admin_password,
|
update_admin_password,
|
||||||
update_admin_username,
|
update_admin_username,
|
||||||
update_system_config as _update_system_config,
|
update_system_config as _update_system_config,
|
||||||
|
update_admin_social_login_binding_profile,
|
||||||
|
upsert_admin_social_login_binding,
|
||||||
verify_admin,
|
verify_admin,
|
||||||
)
|
)
|
||||||
from db.accounts import (
|
from db.accounts import (
|
||||||
@@ -144,7 +150,7 @@ logger = get_logger(__name__)
|
|||||||
DB_FILE = config.DB_FILE
|
DB_FILE = config.DB_FILE
|
||||||
|
|
||||||
# 数据库版本 (用于迁移管理)
|
# 数据库版本 (用于迁移管理)
|
||||||
DB_VERSION = 22
|
DB_VERSION = 23
|
||||||
|
|
||||||
|
|
||||||
# ==================== 系统配置缓存(P1 / O-03) ====================
|
# ==================== 系统配置缓存(P1 / O-03) ====================
|
||||||
|
|||||||
101
db/admin.py
101
db/admin.py
@@ -245,6 +245,107 @@ def update_admin_username(old_username: str, new_username: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 管理员聚合登录绑定 ====================
|
||||||
|
|
||||||
|
|
||||||
|
def find_admin_social_login_binding_by_identity(provider: str, social_uid: str):
|
||||||
|
with db_pool.get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT * FROM admin_social_login_bindings WHERE provider = ? AND social_uid = ?",
|
||||||
|
(provider, social_uid),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def find_admin_social_login_binding(admin_id: int, provider: str):
|
||||||
|
with db_pool.get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT * FROM admin_social_login_bindings WHERE admin_id = ? AND provider = ?",
|
||||||
|
(int(admin_id), provider),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_admin_social_login_binding(*, admin_id: int, provider: str, social_uid: str, nickname: str = "", avatar_url: str = ""):
|
||||||
|
with db_pool.get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
now = get_cst_now_str()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO admin_social_login_bindings (
|
||||||
|
admin_id, provider, social_uid, nickname, avatar_url, created_at, updated_at, last_login_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(admin_id, provider) DO UPDATE SET
|
||||||
|
social_uid = excluded.social_uid,
|
||||||
|
nickname = excluded.nickname,
|
||||||
|
avatar_url = excluded.avatar_url,
|
||||||
|
updated_at = excluded.updated_at,
|
||||||
|
last_login_at = excluded.last_login_at
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
int(admin_id),
|
||||||
|
provider,
|
||||||
|
social_uid,
|
||||||
|
str(nickname or "")[:128],
|
||||||
|
str(avatar_url or "")[:512],
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
conn.rollback()
|
||||||
|
return None
|
||||||
|
return find_admin_social_login_binding(admin_id, provider)
|
||||||
|
|
||||||
|
|
||||||
|
def update_admin_social_login_binding_profile(binding_id: int, *, nickname: str = "", avatar_url: str = "") -> bool:
|
||||||
|
with db_pool.get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE admin_social_login_bindings
|
||||||
|
SET nickname = ?, avatar_url = ?, updated_at = ?, last_login_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(str(nickname or "")[:128], str(avatar_url or "")[:512], get_cst_now_str(), get_cst_now_str(), int(binding_id)),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def list_admin_social_login_bindings(admin_id: int) -> list[dict]:
|
||||||
|
with db_pool.get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM admin_social_login_bindings
|
||||||
|
WHERE admin_id = ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
""",
|
||||||
|
(int(admin_id),),
|
||||||
|
)
|
||||||
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_admin_social_login_binding(admin_id: int, provider: str) -> bool:
|
||||||
|
with db_pool.get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM admin_social_login_bindings WHERE admin_id = ? AND provider = ?",
|
||||||
|
(int(admin_id), provider),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
def get_system_stats() -> dict:
|
def get_system_stats() -> dict:
|
||||||
"""获取系统统计信息"""
|
"""获取系统统计信息"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ def _get_migration_steps():
|
|||||||
(20, _migrate_to_v20),
|
(20, _migrate_to_v20),
|
||||||
(21, _migrate_to_v21),
|
(21, _migrate_to_v21),
|
||||||
(22, _migrate_to_v22),
|
(22, _migrate_to_v22),
|
||||||
|
(23, _migrate_to_v23),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -1002,3 +1003,33 @@ def _migrate_to_v22(conn):
|
|||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_to_v23(conn):
|
||||||
|
"""迁移到版本23 - 管理员聚合登录绑定。"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_social_login_bindings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
admin_id INTEGER NOT NULL,
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
social_uid TEXT NOT NULL,
|
||||||
|
nickname TEXT DEFAULT '',
|
||||||
|
avatar_url TEXT DEFAULT '',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login_at TIMESTAMP,
|
||||||
|
UNIQUE (provider, social_uid),
|
||||||
|
UNIQUE (admin_id, provider),
|
||||||
|
FOREIGN KEY (admin_id) REFERENCES admins (id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_admin ON admin_social_login_bindings(admin_id)")
|
||||||
|
cursor.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_provider_uid ON admin_social_login_bindings(provider, social_uid)"
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|||||||
24
db/schema.py
24
db/schema.py
@@ -276,6 +276,26 @@ def ensure_schema(conn) -> None:
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 管理员聚合登录绑定表
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_social_login_bindings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
admin_id INTEGER NOT NULL,
|
||||||
|
provider TEXT NOT NULL,
|
||||||
|
social_uid TEXT NOT NULL,
|
||||||
|
nickname TEXT DEFAULT '',
|
||||||
|
avatar_url TEXT DEFAULT '',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login_at TIMESTAMP,
|
||||||
|
UNIQUE (provider, social_uid),
|
||||||
|
UNIQUE (admin_id, provider),
|
||||||
|
FOREIGN KEY (admin_id) REFERENCES admins (id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# 聚合登录短期待绑定凭证表
|
# 聚合登录短期待绑定凭证表
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
@@ -432,6 +452,10 @@ def ensure_schema(conn) -> None:
|
|||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_user ON social_login_bindings(user_id)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_user ON social_login_bindings(user_id)")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_admin ON admin_social_login_bindings(admin_id)")
|
||||||
|
cursor.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_provider_uid ON admin_social_login_bindings(provider, social_uid)"
|
||||||
|
)
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)")
|
||||||
|
|||||||
@@ -283,7 +283,8 @@ def register():
|
|||||||
social_uid = str(pending.get("social_uid") or "").strip()
|
social_uid = str(pending.get("social_uid") or "").strip()
|
||||||
enabled_providers = parse_providers((database.get_system_config() or {}).get("social_login_providers"))
|
enabled_providers = parse_providers((database.get_system_config() or {}).get("social_login_providers"))
|
||||||
existing_identity = database.find_social_login_binding(provider, social_uid)
|
existing_identity = database.find_social_login_binding(provider, social_uid)
|
||||||
if provider in enabled_providers and social_uid and not existing_identity:
|
existing_admin_identity = database.find_admin_social_login_binding_by_identity(provider, social_uid)
|
||||||
|
if provider in enabled_providers and social_uid and not existing_identity and not existing_admin_identity:
|
||||||
binding = database.upsert_social_login_binding(
|
binding = database.upsert_social_login_binding(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from datetime import timedelta
|
|||||||
import database
|
import database
|
||||||
from app_logger import get_logger
|
from app_logger import get_logger
|
||||||
from db.utils import get_cst_now, get_cst_now_str
|
from db.utils import get_cst_now, get_cst_now_str
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request, session
|
||||||
from flask_login import current_user, login_required, login_user
|
from flask_login import current_user, login_required, login_user
|
||||||
from services.accounts_service import load_user_accounts
|
from services.accounts_service import load_user_accounts
|
||||||
from services.models import User
|
from services.models import User
|
||||||
@@ -82,6 +82,18 @@ def _binding_row(provider: str, binding: dict | None) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_binding_row(provider: str, binding: dict | None) -> dict:
|
||||||
|
return {
|
||||||
|
"provider": provider,
|
||||||
|
"provider_label": provider_label(provider),
|
||||||
|
"bound": bool(binding),
|
||||||
|
"nickname": (binding or {}).get("nickname") or "",
|
||||||
|
"avatar_url": (binding or {}).get("avatar_url") or "",
|
||||||
|
"last_login_at": (binding or {}).get("last_login_at"),
|
||||||
|
"created_at": (binding or {}).get("created_at"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@api_social_bp.route("/api/auth/social/config", methods=["GET"])
|
@api_social_bp.route("/api/auth/social/config", methods=["GET"])
|
||||||
def social_public_config():
|
def social_public_config():
|
||||||
return jsonify(public_social_config(database.get_system_config()))
|
return jsonify(public_social_config(database.get_system_config()))
|
||||||
@@ -135,6 +147,10 @@ def social_callback():
|
|||||||
return _social_error(error)
|
return _social_error(error)
|
||||||
|
|
||||||
binding = database.find_social_login_binding(profile.provider, profile.social_uid)
|
binding = database.find_social_login_binding(profile.provider, profile.social_uid)
|
||||||
|
admin_binding = database.find_admin_social_login_binding_by_identity(profile.provider, profile.social_uid)
|
||||||
|
if admin_binding:
|
||||||
|
return jsonify({"error": "该第三方账号已绑定管理员账号"}), 409
|
||||||
|
|
||||||
if binding:
|
if binding:
|
||||||
if mode == "bind":
|
if mode == "bind":
|
||||||
current_id = int(getattr(current_user, "id", 0) or 0)
|
current_id = int(getattr(current_user, "id", 0) or 0)
|
||||||
@@ -204,6 +220,9 @@ def bind_social_account():
|
|||||||
existing_identity = database.find_social_login_binding(provider, social_uid)
|
existing_identity = database.find_social_login_binding(provider, social_uid)
|
||||||
if existing_identity and int(existing_identity.get("user_id") or 0) != int(current_user.id):
|
if existing_identity and int(existing_identity.get("user_id") or 0) != int(current_user.id):
|
||||||
return jsonify({"error": "该第三方账号已绑定其他用户"}), 409
|
return jsonify({"error": "该第三方账号已绑定其他用户"}), 409
|
||||||
|
existing_admin_identity = database.find_admin_social_login_binding_by_identity(provider, social_uid)
|
||||||
|
if existing_admin_identity:
|
||||||
|
return jsonify({"error": "该第三方账号已绑定管理员账号"}), 409
|
||||||
|
|
||||||
existing_provider = database.find_user_social_login_binding(int(current_user.id), provider)
|
existing_provider = database.find_user_social_login_binding(int(current_user.id), provider)
|
||||||
if existing_provider and str(existing_provider.get("social_uid") or "") != social_uid:
|
if existing_provider and str(existing_provider.get("social_uid") or "") != social_uid:
|
||||||
@@ -242,6 +261,138 @@ def admin_social_config():
|
|||||||
return protected()
|
return protected()
|
||||||
|
|
||||||
|
|
||||||
|
@api_social_bp.route("/yuyx/api/admin/social-bindings", methods=["GET"])
|
||||||
|
def list_admin_social_bindings():
|
||||||
|
from routes.decorators import admin_required
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
def _inner():
|
||||||
|
cfg = database.get_system_config()
|
||||||
|
providers = parse_providers(cfg.get("social_login_providers")) or list(PROVIDER_LABELS.keys())
|
||||||
|
admin_id = int(session.get("admin_id") or 0)
|
||||||
|
existing = {
|
||||||
|
item["provider"]: item
|
||||||
|
for item in database.list_admin_social_login_bindings(admin_id)
|
||||||
|
}
|
||||||
|
public_cfg = public_social_config(cfg)
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"enabled": bool(public_cfg.get("enabled")),
|
||||||
|
"providers": providers,
|
||||||
|
"items": [_admin_binding_row(provider, existing.get(provider)) for provider in providers],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return _inner()
|
||||||
|
|
||||||
|
|
||||||
|
@api_social_bp.route("/yuyx/api/admin/social-login-url", methods=["POST"])
|
||||||
|
def admin_social_login_url():
|
||||||
|
from routes.decorators import admin_required
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
def _inner():
|
||||||
|
data = _get_json_payload()
|
||||||
|
provider = str(data.get("provider") or "").strip().lower()
|
||||||
|
redirect_uri = str(data.get("redirect_uri") or "").strip()
|
||||||
|
try:
|
||||||
|
result = fetch_social_login_url(
|
||||||
|
database.get_system_config(),
|
||||||
|
provider=provider,
|
||||||
|
mode="bind",
|
||||||
|
redirect_uri=redirect_uri,
|
||||||
|
allowed_hosts=_allowed_redirect_hosts(),
|
||||||
|
)
|
||||||
|
except SocialLoginError as error:
|
||||||
|
logger.warning(f"[admin/social/login-url] provider={provider or '-'} failed: {error.message}")
|
||||||
|
return _social_error(error)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
return _inner()
|
||||||
|
|
||||||
|
|
||||||
|
@api_social_bp.route("/yuyx/api/admin/social-poll", methods=["POST"])
|
||||||
|
def admin_social_poll():
|
||||||
|
from routes.decorators import admin_required
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
def _inner():
|
||||||
|
data = _get_json_payload()
|
||||||
|
provider = str(data.get("provider") or "").strip().lower()
|
||||||
|
state = str(data.get("state") or "").strip()
|
||||||
|
try:
|
||||||
|
result = poll_social_scan(database.get_system_config(), provider=provider, state=state)
|
||||||
|
except SocialLoginError as error:
|
||||||
|
logger.warning(f"[admin/social/poll] provider={provider or '-'} failed: {error.message}")
|
||||||
|
return _social_error(error)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
return _inner()
|
||||||
|
|
||||||
|
|
||||||
|
@api_social_bp.route("/yuyx/api/admin/social-bindings/<provider>/callback", methods=["POST"])
|
||||||
|
def bind_admin_social_callback(provider):
|
||||||
|
from routes.decorators import admin_required
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
def _inner():
|
||||||
|
data = _get_json_payload()
|
||||||
|
provider_value = str(data.get("provider") or provider or data.get("type") or "").strip().lower()
|
||||||
|
code = str(data.get("code") or "").strip()
|
||||||
|
admin_id = int(session.get("admin_id") or 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
profile = fetch_space_profile(database.get_system_config(), provider=provider_value, code=code)
|
||||||
|
except SocialLoginError as error:
|
||||||
|
logger.warning(f"[admin/social/callback] provider={provider_value or '-'} failed: {error.message}")
|
||||||
|
return _social_error(error)
|
||||||
|
|
||||||
|
user_identity = database.find_social_login_binding(profile.provider, profile.social_uid)
|
||||||
|
if user_identity:
|
||||||
|
return jsonify({"error": "该第三方账号已绑定普通用户"}), 409
|
||||||
|
|
||||||
|
existing_identity = database.find_admin_social_login_binding_by_identity(profile.provider, profile.social_uid)
|
||||||
|
if existing_identity and int(existing_identity.get("admin_id") or 0) != admin_id:
|
||||||
|
return jsonify({"error": "该第三方账号已绑定其他管理员"}), 409
|
||||||
|
|
||||||
|
existing_provider = database.find_admin_social_login_binding(admin_id, profile.provider)
|
||||||
|
if existing_provider and str(existing_provider.get("social_uid") or "") != profile.social_uid:
|
||||||
|
return jsonify({"error": f"当前管理员已绑定{provider_label(profile.provider)}"}), 409
|
||||||
|
|
||||||
|
binding = database.upsert_admin_social_login_binding(
|
||||||
|
admin_id=admin_id,
|
||||||
|
provider=profile.provider,
|
||||||
|
social_uid=profile.social_uid,
|
||||||
|
nickname=profile.nickname,
|
||||||
|
avatar_url=profile.avatar_url,
|
||||||
|
)
|
||||||
|
if not binding:
|
||||||
|
return jsonify({"error": "该第三方账号已绑定其他管理员"}), 409
|
||||||
|
|
||||||
|
logger.info(f"[admin/social/bind] admin_id={admin_id} provider={profile.provider}")
|
||||||
|
return jsonify({"success": True, "item": _admin_binding_row(profile.provider, binding)})
|
||||||
|
|
||||||
|
return _inner()
|
||||||
|
|
||||||
|
|
||||||
|
@api_social_bp.route("/yuyx/api/admin/social-bindings/<provider>", methods=["DELETE"])
|
||||||
|
def unbind_admin_social_account(provider):
|
||||||
|
from routes.decorators import admin_required
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
def _inner():
|
||||||
|
provider_value = str(provider or "").strip().lower()
|
||||||
|
if provider_value not in PROVIDER_LABELS:
|
||||||
|
return jsonify({"error": "不支持的登录方式"}), 400
|
||||||
|
admin_id = int(session.get("admin_id") or 0)
|
||||||
|
if not database.delete_admin_social_login_binding(admin_id, provider_value):
|
||||||
|
return jsonify({"error": "绑定记录不存在"}), 404
|
||||||
|
logger.info(f"[admin/social/unbind] admin_id={admin_id} provider={provider_value}")
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
return _inner()
|
||||||
|
|
||||||
|
|
||||||
@api_social_bp.route("/yuyx/api/social-login/test", methods=["POST"])
|
@api_social_bp.route("/yuyx/api/social-login/test", methods=["POST"])
|
||||||
def test_admin_social_config():
|
def test_admin_social_config():
|
||||||
from routes.decorators import admin_required
|
from routes.decorators import admin_required
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Blueprint, current_app, redirect, render_template, session, url_for
|
from flask import Blueprint, current_app, redirect, render_template, request, session, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from routes.decorators import admin_required
|
from routes.decorators import admin_required
|
||||||
@@ -192,3 +192,14 @@ def admin_page():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[admin_spa] 加载manifest失败: {e}")
|
logger.error(f"[admin_spa] 加载manifest失败: {e}")
|
||||||
return "后台页面加载失败,请稍后重试", 500
|
return "后台页面加载失败,请稍后重试", 500
|
||||||
|
|
||||||
|
|
||||||
|
@pages_bp.route("/yuyx/admin-social-bind-callback")
|
||||||
|
@admin_required
|
||||||
|
def admin_social_bind_callback_page():
|
||||||
|
"""管理员快捷登录绑定回调页面(由后台 SPA 继续处理授权参数)。"""
|
||||||
|
query = request.query_string.decode("utf-8", "ignore")
|
||||||
|
target = "/yuyx/admin#/social-bind-callback"
|
||||||
|
if query:
|
||||||
|
target = f"{target}?{query}"
|
||||||
|
return redirect(target)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"file": "assets/MetricGrid-BR486o_b.css",
|
"file": "assets/MetricGrid-BR486o_b.css",
|
||||||
"src": "_MetricGrid-BR486o_b.css"
|
"src": "_MetricGrid-BR486o_b.css"
|
||||||
},
|
},
|
||||||
"_MetricGrid-C3Xjc9mZ.js": {
|
"_MetricGrid-kv-nSROj.js": {
|
||||||
"file": "assets/MetricGrid-C3Xjc9mZ.js",
|
"file": "assets/MetricGrid-kv-nSROj.js",
|
||||||
"name": "MetricGrid",
|
"name": "MetricGrid",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
@@ -14,29 +14,36 @@
|
|||||||
"assets/MetricGrid-BR486o_b.css"
|
"assets/MetricGrid-BR486o_b.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_email-Mh1SHQbX.js": {
|
"_admin-VsbfHbbH.js": {
|
||||||
"file": "assets/email-Mh1SHQbX.js",
|
"file": "assets/admin-VsbfHbbH.js",
|
||||||
|
"name": "admin",
|
||||||
|
"imports": [
|
||||||
|
"index.html"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_email-CgUCpCe3.js": {
|
||||||
|
"file": "assets/email-CgUCpCe3.js",
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_system-CYbWdReq.js": {
|
"_system-CeiBEEoE.js": {
|
||||||
"file": "assets/system-CYbWdReq.js",
|
"file": "assets/system-CeiBEEoE.js",
|
||||||
"name": "system",
|
"name": "system",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_tasks-B7oNpIBD.js": {
|
"_tasks-C6JkguA6.js": {
|
||||||
"file": "assets/tasks-B7oNpIBD.js",
|
"file": "assets/tasks-C6JkguA6.js",
|
||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_users-DzDcz9C_.js": {
|
"_users-D9XvGIoE.js": {
|
||||||
"file": "assets/users-DzDcz9C_.js",
|
"file": "assets/users-D9XvGIoE.js",
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
@@ -46,23 +53,23 @@
|
|||||||
"file": "assets/vendor-axios-B9ygI19o.js",
|
"file": "assets/vendor-axios-B9ygI19o.js",
|
||||||
"name": "vendor-axios"
|
"name": "vendor-axios"
|
||||||
},
|
},
|
||||||
"_vendor-element-B5S5pUKo.js": {
|
"_vendor-element-C68yOrAy.css": {
|
||||||
"file": "assets/vendor-element-B5S5pUKo.js",
|
"file": "assets/vendor-element-C68yOrAy.css",
|
||||||
|
"src": "_vendor-element-C68yOrAy.css"
|
||||||
|
},
|
||||||
|
"_vendor-element-CIudPaVX.js": {
|
||||||
|
"file": "assets/vendor-element-CIudPaVX.js",
|
||||||
"name": "vendor-element",
|
"name": "vendor-element",
|
||||||
"imports": [
|
"imports": [
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/vendor-element-C68yOrAy.css"
|
"assets/vendor-element-C68yOrAy.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_vendor-element-C68yOrAy.css": {
|
"_vendor-misc-DszMq72k.js": {
|
||||||
"file": "assets/vendor-element-C68yOrAy.css",
|
"file": "assets/vendor-misc-DszMq72k.js",
|
||||||
"src": "_vendor-element-C68yOrAy.css"
|
|
||||||
},
|
|
||||||
"_vendor-misc-BeoNyvBp.js": {
|
|
||||||
"file": "assets/vendor-misc-BeoNyvBp.js",
|
|
||||||
"name": "vendor-misc",
|
"name": "vendor-misc",
|
||||||
"imports": [
|
"imports": [
|
||||||
"_vendor-vue-CVxSw_oJ.js"
|
"_vendor-vue-CVxSw_oJ.js"
|
||||||
@@ -73,15 +80,15 @@
|
|||||||
"name": "vendor-vue"
|
"name": "vendor-vue"
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-DOvMEmc8.js",
|
"file": "assets/index-6ynv0Z9Y.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"dynamicImports": [
|
"dynamicImports": [
|
||||||
"src/pages/ReportPage.vue",
|
"src/pages/ReportPage.vue",
|
||||||
@@ -92,22 +99,40 @@
|
|||||||
"src/pages/EmailPage.vue",
|
"src/pages/EmailPage.vue",
|
||||||
"src/pages/SecurityPage.vue",
|
"src/pages/SecurityPage.vue",
|
||||||
"src/pages/SystemPage.vue",
|
"src/pages/SystemPage.vue",
|
||||||
"src/pages/SettingsPage.vue"
|
"src/pages/SettingsPage.vue",
|
||||||
|
"src/pages/AdminSocialBindCallbackPage.vue"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/index-CPs_XZ2s.css"
|
"assets/index-CPs_XZ2s.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"src/pages/AdminSocialBindCallbackPage.vue": {
|
||||||
|
"file": "assets/AdminSocialBindCallbackPage-BsLZg3f-.js",
|
||||||
|
"name": "AdminSocialBindCallbackPage",
|
||||||
|
"src": "src/pages/AdminSocialBindCallbackPage.vue",
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"imports": [
|
||||||
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
|
"_admin-VsbfHbbH.js",
|
||||||
|
"index.html",
|
||||||
|
"_vendor-element-CIudPaVX.js",
|
||||||
|
"_vendor-axios-B9ygI19o.js",
|
||||||
|
"_vendor-misc-DszMq72k.js"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"assets/AdminSocialBindCallbackPage-CXV1zZmY.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
"src/pages/AnnouncementsPage.vue": {
|
"src/pages/AnnouncementsPage.vue": {
|
||||||
"file": "assets/AnnouncementsPage-Dagm5PzE.js",
|
"file": "assets/AnnouncementsPage-BcIVG51R.js",
|
||||||
"name": "AnnouncementsPage",
|
"name": "AnnouncementsPage",
|
||||||
"src": "src/pages/AnnouncementsPage.vue",
|
"src": "src/pages/AnnouncementsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-misc-BeoNyvBp.js",
|
"_vendor-misc-DszMq72k.js",
|
||||||
"_vendor-axios-B9ygI19o.js"
|
"_vendor-axios-B9ygI19o.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -115,72 +140,72 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/EmailPage.vue": {
|
"src/pages/EmailPage.vue": {
|
||||||
"file": "assets/EmailPage-DiZA9Kx_.js",
|
"file": "assets/EmailPage-B1uMhyWi.js",
|
||||||
"name": "EmailPage",
|
"name": "EmailPage",
|
||||||
"src": "src/pages/EmailPage.vue",
|
"src": "src/pages/EmailPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_email-Mh1SHQbX.js",
|
"_email-CgUCpCe3.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_MetricGrid-C3Xjc9mZ.js",
|
"_MetricGrid-kv-nSROj.js",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/EmailPage-CTHxGzDv.css"
|
"assets/EmailPage-CTHxGzDv.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/FeedbacksPage.vue": {
|
"src/pages/FeedbacksPage.vue": {
|
||||||
"file": "assets/FeedbacksPage-DrMVqBKf.js",
|
"file": "assets/FeedbacksPage-CG9FZytm.js",
|
||||||
"name": "FeedbacksPage",
|
"name": "FeedbacksPage",
|
||||||
"src": "src/pages/FeedbacksPage.vue",
|
"src": "src/pages/FeedbacksPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_MetricGrid-C3Xjc9mZ.js",
|
"_MetricGrid-kv-nSROj.js",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/FeedbacksPage-CPmSqIaj.css"
|
"assets/FeedbacksPage-CPmSqIaj.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LogsPage.vue": {
|
"src/pages/LogsPage.vue": {
|
||||||
"file": "assets/LogsPage-Cy6Q0ave.js",
|
"file": "assets/LogsPage-Ct-BSxV6.js",
|
||||||
"name": "LogsPage",
|
"name": "LogsPage",
|
||||||
"src": "src/pages/LogsPage.vue",
|
"src": "src/pages/LogsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-DzDcz9C_.js",
|
"_users-D9XvGIoE.js",
|
||||||
"_tasks-B7oNpIBD.js",
|
"_tasks-C6JkguA6.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/LogsPage-BUgx3sZr.css"
|
"assets/LogsPage-BUgx3sZr.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ReportPage.vue": {
|
"src/pages/ReportPage.vue": {
|
||||||
"file": "assets/ReportPage-BMEJM5Hr.js",
|
"file": "assets/ReportPage-2jS10KoG.js",
|
||||||
"name": "ReportPage",
|
"name": "ReportPage",
|
||||||
"src": "src/pages/ReportPage.vue",
|
"src": "src/pages/ReportPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_email-Mh1SHQbX.js",
|
"_email-CgUCpCe3.js",
|
||||||
"_tasks-B7oNpIBD.js",
|
"_tasks-C6JkguA6.js",
|
||||||
"_system-CYbWdReq.js",
|
"_system-CeiBEEoE.js",
|
||||||
"_MetricGrid-C3Xjc9mZ.js",
|
"_MetricGrid-kv-nSROj.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-misc-BeoNyvBp.js",
|
"_vendor-misc-DszMq72k.js",
|
||||||
"_vendor-axios-B9ygI19o.js"
|
"_vendor-axios-B9ygI19o.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -188,67 +213,68 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SecurityPage.vue": {
|
"src/pages/SecurityPage.vue": {
|
||||||
"file": "assets/SecurityPage-yzYEGeTN.js",
|
"file": "assets/SecurityPage-93lfkhLF.js",
|
||||||
"name": "SecurityPage",
|
"name": "SecurityPage",
|
||||||
"src": "src/pages/SecurityPage.vue",
|
"src": "src/pages/SecurityPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_MetricGrid-C3Xjc9mZ.js",
|
"_MetricGrid-kv-nSROj.js",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SecurityPage-C2-mJ7eD.css"
|
"assets/SecurityPage-C2-mJ7eD.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SettingsPage.vue": {
|
"src/pages/SettingsPage.vue": {
|
||||||
"file": "assets/SettingsPage-DF5fL8gq.js",
|
"file": "assets/SettingsPage-BbHyIZsy.js",
|
||||||
"name": "SettingsPage",
|
"name": "SettingsPage",
|
||||||
"src": "src/pages/SettingsPage.vue",
|
"src": "src/pages/SettingsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
|
"_vendor-misc-DszMq72k.js",
|
||||||
|
"_admin-VsbfHbbH.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js"
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SettingsPage-D-iYz1zh.css"
|
"assets/SettingsPage-CjIQQfeg.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SystemPage.vue": {
|
"src/pages/SystemPage.vue": {
|
||||||
"file": "assets/SystemPage-DrM9-RI5.js",
|
"file": "assets/SystemPage-D9T-fhw-.js",
|
||||||
"name": "SystemPage",
|
"name": "SystemPage",
|
||||||
"src": "src/pages/SystemPage.vue",
|
"src": "src/pages/SystemPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_system-CYbWdReq.js",
|
"_system-CeiBEEoE.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SystemPage-CTs6qr36.css"
|
"assets/SystemPage-CTs6qr36.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/UsersPage.vue": {
|
"src/pages/UsersPage.vue": {
|
||||||
"file": "assets/UsersPage-RI5S3snx.js",
|
"file": "assets/UsersPage-2-Mno2hz.js",
|
||||||
"name": "UsersPage",
|
"name": "UsersPage",
|
||||||
"src": "src/pages/UsersPage.vue",
|
"src": "src/pages/UsersPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-DzDcz9C_.js",
|
"_users-D9XvGIoE.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_vendor-element-B5S5pUKo.js",
|
"_vendor-element-CIudPaVX.js",
|
||||||
"_vendor-vue-CVxSw_oJ.js",
|
"_vendor-vue-CVxSw_oJ.js",
|
||||||
"_vendor-axios-B9ygI19o.js",
|
"_vendor-axios-B9ygI19o.js",
|
||||||
"_vendor-misc-BeoNyvBp.js"
|
"_vendor-misc-DszMq72k.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/UsersPage-CgYh6JHW.css"
|
"assets/UsersPage-CgYh6JHW.css"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
import{ax as w,r as y,o as g,aj as l,n as v,q as f,L as d,E as h,t as k,J as x}from"./vendor-vue-CVxSw_oJ.js";import{i as S}from"./admin-VsbfHbbH.js";import{_ as C}from"./index-6ynv0Z9Y.js";import{a as n}from"./vendor-element-CIudPaVX.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-DszMq72k.js";const b={class:"callback-wrap"},B={class:"callback-text"},P={__name:"AdminSocialBindCallbackPage",setup(q){const o=w(),t=y("正在完成绑定");return g(async()=>{const c=String(window.location.hash||"").split("?")[1]||"",e=new URLSearchParams(window.location.search||c),a=String(e.get("provider")||e.get("type")||"").trim().toLowerCase(),r=String(e.get("code")||"").trim(),p=String(o.query?.provider||o.query?.type||"").trim().toLowerCase(),m=String(o.query?.code||"").trim(),s=a||p,i=r||m;if(!s||!i){n.error("快捷登录回调参数不完整"),window.location.replace("/yuyx/admin#/settings");return}try{await S(s,{provider:s,code:i}),n.success("管理员快捷登录已绑定"),window.location.replace("/yuyx/admin#/settings")}catch(u){const _=u?.response?.data;t.value=_?.error||"快捷登录绑定失败",n.error(t.value),window.setTimeout(()=>{window.location.replace("/yuyx/admin#/settings")},1200)}}),(c,e)=>{const a=l("el-skeleton"),r=l("el-card");return f(),v("div",b,[d(r,{shadow:"never",class:"callback-card"},{default:h(()=>[d(a,{rows:3,animated:""}),k("div",B,x(t.value),1)]),_:1})])}}},T=C(P,[["__scopeId","data-v-647766e7"]]);export{T as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.callback-wrap[data-v-647766e7]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:#f6f7fb}.callback-card[data-v-647766e7]{width:min(420px,94vw);border-radius:12px;border:1px solid var(--app-border)}.callback-text[data-v-647766e7]{margin-top:12px;color:var(--app-muted);font-size:13px;text-align:center}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{_}from"./index-DOvMEmc8.js";import{aj as c,n as s,q as t,K as r,a3 as u,y as p,t as o,G as l,L as y,E as h,D as i,H as v,J as n,I as k,x as f}from"./vendor-vue-CVxSw_oJ.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},g={class:"metric-label"},B={class:"metric-value"},C={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,D)=>{const d=c("el-icon"),m=c("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(v(e.icon)))]),_:2},1024)])):l("",!0),o("div",g,n(e?.label||"-"),1)]),o("div",B,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[k(n(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",C,n(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},w=_(N,[["__scopeId","data-v-28727c73"]]);export{w as M};
|
import{_}from"./index-6ynv0Z9Y.js";import{aj as c,n as s,q as t,K as r,a3 as u,y as p,t as o,G as l,L as y,E as h,D as i,H as v,J as n,I as k,x as f}from"./vendor-vue-CVxSw_oJ.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},g={class:"metric-label"},B={class:"metric-value"},C={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,D)=>{const d=c("el-icon"),m=c("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(v(e.icon)))]),_:2},1024)])):l("",!0),o("div",g,n(e?.label||"-"),1)]),o("div",B,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[k(n(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",C,n(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},w=_(N,[["__scopeId","data-v-28727c73"]]);export{w as M};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/SettingsPage-BbHyIZsy.js
Normal file
1
static/admin/assets/SettingsPage-BbHyIZsy.js
Normal file
File diff suppressed because one or more lines are too long
1
static/admin/assets/SettingsPage-CjIQQfeg.css
Normal file
1
static/admin/assets/SettingsPage-CjIQQfeg.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.page-stack[data-v-e6d9cfda]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-e6d9cfda]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-e6d9cfda]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.section-head[data-v-e6d9cfda]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.section-head .section-title[data-v-e6d9cfda]{margin-bottom:0}.help[data-v-e6d9cfda]{margin-top:10px;font-size:12px;color:var(--app-muted)}.help-alert[data-v-e6d9cfda]{margin-bottom:12px}.social-list[data-v-e6d9cfda]{display:flex;flex-direction:column;gap:10px}.social-row[data-v-e6d9cfda]{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--app-border);border-radius:10px;background:#f8fafcb8}.social-provider[data-v-e6d9cfda]{display:flex;align-items:center;gap:10px;min-width:0}.social-icon[data-v-e6d9cfda]{width:30px;height:30px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;color:#fff;font-size:13px;font-weight:800}.provider-wx[data-v-e6d9cfda]{background:#16a34a}.provider-qq[data-v-e6d9cfda]{background:#2563eb}.provider-alipay[data-v-e6d9cfda]{background:#1677ff}.social-info[data-v-e6d9cfda]{min-width:0;display:flex;flex-direction:column;gap:2px}.social-info strong[data-v-e6d9cfda]{font-size:14px}.social-info span[data-v-e6d9cfda]{max-width:min(52vw,360px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--app-muted)}.social-actions[data-v-e6d9cfda]{flex:0 0 auto}.social-qr-box[data-v-e6d9cfda]{display:flex;flex-direction:column;align-items:center;gap:12px}.social-qr-prompt[data-v-e6d9cfda]{font-size:13px;color:#374151;text-align:center}@media(max-width:640px){.social-row[data-v-e6d9cfda]{align-items:flex-start;flex-direction:column}.social-actions[data-v-e6d9cfda]{width:100%;display:flex;justify-content:flex-end}}
|
||||||
@@ -1 +0,0 @@
|
|||||||
.page-stack[data-v-1418d488]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-1418d488]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-1418d488]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-1418d488]{margin-top:10px;font-size:12px;color:var(--app-muted)}.help-alert[data-v-1418d488]{margin-bottom:12px}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import{f as Fe,a as He,u as me,b as Oe,t as je}from"./system-CYbWdReq.js";import{a as ge,_ as Ge,g as Se,h as Ye,i as Je,u as fe,j as Pe,p as We}from"./index-DOvMEmc8.js";import{E as _e,a as d}from"./vendor-element-B5S5pUKo.js";import{r as s,c as de,l as Xe,R as Ze,o as el,aj as m,ap as ll,F as al,q as y,n as b,t as n,L as l,E as t,I as v,K as ol,a3 as tl,J as A,G as T,y as sl}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function nl(){const{data:k}=await ge.get("/proxy/config");return k}async function ul(k){const{data:_}=await ge.post("/proxy/config",k);return _}async function il(k){const{data:_}=await ge.post("/proxy/test",k);return _}const dl={class:"page-stack"},rl={class:"config-grid"},cl={class:"row-actions"},vl={class:"row-actions"},pl={class:"row-actions"},ml={key:0,class:"help"},fl={class:"row-actions"},_l={class:"section-head"},gl={class:"status-inline app-muted"},yl={key:0,class:"status-dots","aria-hidden":"true"},bl={class:"kdocs-inline"},kl={class:"kdocs-range"},Vl={class:"row-actions"},xl={key:0,class:"help"},wl={key:1,class:"help"},Sl={class:"kdocs-qr"},Pl=["src"],Cl={__name:"SystemPage",setup(k){const _=s(!1),B=s(2),q=s(1),M=s(3),z=s(120),L=s(!1),g=s(""),R=s(3),$=s(!1),F=s(10),H=s(7),I=s(!1),h=s("https://www.spacezs.cn/connect.php"),K=s(""),V=s(""),O=s(""),j=s(!1),x=s(["wx"]),re=s(!1),ce=s(!1),Ce=[{label:"QQ",value:"qq"},{label:"微信",value:"wx"},{label:"支付宝",value:"alipay"}],G=s(!1),Y=s(""),J=s(""),W=s(""),X=s(0),Z=s("A"),ee=s("D"),le=s(0),ae=s(0),oe=s(!1),te=s(""),ye=Se({maxAgeMs:600*1e3}),c=s(ye||{}),w=s(!1),S=s(""),ve=s(!1),P=s(!1),C=s(!1),U=s(!1),N=s(!ye),se=s("");let ne=null;const be=de(()=>P.value||C.value||U.value),pe=de(()=>N.value||P.value||ve.value),ue=de(()=>{if(pe.value)return"检测中";const o=c.value||{};return o?.logged_in===!0||o?.last_login_ok===!0?"已登录":o?.logged_in===!1||o?.last_login_ok===!1||o?.login_required===!0?"未登录":o?.last_error?"异常":"未知"}),Ue=de(()=>pe.value?"is-checking":ue.value==="已登录"?"is-online":ue.value==="未登录"?"is-offline":ue.value==="异常"?"is-error":"is-unknown");function r(o){if(!o){se.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});se.value=`${o} (${e})`}async function Ae(){_.value=!0;try{const[e,i,u]=await Promise.all([Fe(),nl(),He()]);B.value=e.max_concurrent_global??2,q.value=e.max_concurrent_per_account??1,M.value=e.max_screenshot_concurrent??3,z.value=e.db_slow_query_ms??120,$.value=(e.auto_approve_enabled??0)===1,F.value=e.auto_approve_hourly_limit??10,H.value=e.auto_approve_vip_days??7,L.value=(i.proxy_enabled??0)===1,g.value=i.proxy_api_url||"",R.value=i.proxy_expire_minutes??3,G.value=(e.kdocs_enabled??0)===1,Y.value=e.kdocs_doc_url||"",J.value=e.kdocs_default_unit||"",W.value=e.kdocs_sheet_name||"",X.value=e.kdocs_sheet_index??0,Z.value=(e.kdocs_unit_column||"A").toUpperCase(),ee.value=(e.kdocs_image_column||"D").toUpperCase(),le.value=e.kdocs_row_start??0,ae.value=e.kdocs_row_end??0,oe.value=(e.kdocs_admin_notify_enabled??0)===1,te.value=e.kdocs_admin_notify_email||"",I.value=(u.social_login_enabled??0)===1,h.value=u.social_login_endpoint||"https://www.spacezs.cn/connect.php",K.value=u.social_login_appid||"",V.value="",O.value=u.social_login_appkey_masked||"",j.value=!!u.social_login_appkey_configured,x.value=Array.isArray(u.social_login_providers)&&u.social_login_providers.length?u.social_login_providers:["wx"]}catch{}finally{_.value=!1}const o=Se({maxAgeMs:600*1e3});o&&(c.value=o,N.value=!1),Le()}async function Le(){if(!(N.value||P.value)){N.value=!0;try{const o=await We({force:!1,maxAgeMs:6e4,silent:!0,live:0});c.value=o||{}}catch{}finally{N.value=!1}}}async function Ie(){const o={max_concurrent_global:Number(B.value),max_concurrent_per_account:Number(q.value),max_screenshot_concurrent:Number(M.value),db_slow_query_ms:Number(z.value)};try{await _e.confirm(`确定更新并发配置吗?
|
import{f as Fe,a as He,u as me,b as Oe,t as je}from"./system-CeiBEEoE.js";import{a as ge,_ as Ge,g as Se,h as Ye,i as Je,u as fe,j as Pe,p as We}from"./index-6ynv0Z9Y.js";import{E as _e,a as d}from"./vendor-element-CIudPaVX.js";import{r as s,c as de,l as Xe,R as Ze,o as el,aj as m,ap as ll,F as al,q as y,n as b,t as n,L as l,E as t,I as v,K as ol,a3 as tl,J as A,G as T,y as sl}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-DszMq72k.js";async function nl(){const{data:k}=await ge.get("/proxy/config");return k}async function ul(k){const{data:_}=await ge.post("/proxy/config",k);return _}async function il(k){const{data:_}=await ge.post("/proxy/test",k);return _}const dl={class:"page-stack"},rl={class:"config-grid"},cl={class:"row-actions"},vl={class:"row-actions"},pl={class:"row-actions"},ml={key:0,class:"help"},fl={class:"row-actions"},_l={class:"section-head"},gl={class:"status-inline app-muted"},yl={key:0,class:"status-dots","aria-hidden":"true"},bl={class:"kdocs-inline"},kl={class:"kdocs-range"},Vl={class:"row-actions"},xl={key:0,class:"help"},wl={key:1,class:"help"},Sl={class:"kdocs-qr"},Pl=["src"],Cl={__name:"SystemPage",setup(k){const _=s(!1),B=s(2),q=s(1),M=s(3),z=s(120),L=s(!1),g=s(""),R=s(3),$=s(!1),F=s(10),H=s(7),I=s(!1),h=s("https://www.spacezs.cn/connect.php"),K=s(""),V=s(""),O=s(""),j=s(!1),x=s(["wx"]),re=s(!1),ce=s(!1),Ce=[{label:"QQ",value:"qq"},{label:"微信",value:"wx"},{label:"支付宝",value:"alipay"}],G=s(!1),Y=s(""),J=s(""),W=s(""),X=s(0),Z=s("A"),ee=s("D"),le=s(0),ae=s(0),oe=s(!1),te=s(""),ye=Se({maxAgeMs:600*1e3}),c=s(ye||{}),w=s(!1),S=s(""),ve=s(!1),P=s(!1),C=s(!1),U=s(!1),N=s(!ye),se=s("");let ne=null;const be=de(()=>P.value||C.value||U.value),pe=de(()=>N.value||P.value||ve.value),ue=de(()=>{if(pe.value)return"检测中";const o=c.value||{};return o?.logged_in===!0||o?.last_login_ok===!0?"已登录":o?.logged_in===!1||o?.last_login_ok===!1||o?.login_required===!0?"未登录":o?.last_error?"异常":"未知"}),Ue=de(()=>pe.value?"is-checking":ue.value==="已登录"?"is-online":ue.value==="未登录"?"is-offline":ue.value==="异常"?"is-error":"is-unknown");function r(o){if(!o){se.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});se.value=`${o} (${e})`}async function Ae(){_.value=!0;try{const[e,i,u]=await Promise.all([Fe(),nl(),He()]);B.value=e.max_concurrent_global??2,q.value=e.max_concurrent_per_account??1,M.value=e.max_screenshot_concurrent??3,z.value=e.db_slow_query_ms??120,$.value=(e.auto_approve_enabled??0)===1,F.value=e.auto_approve_hourly_limit??10,H.value=e.auto_approve_vip_days??7,L.value=(i.proxy_enabled??0)===1,g.value=i.proxy_api_url||"",R.value=i.proxy_expire_minutes??3,G.value=(e.kdocs_enabled??0)===1,Y.value=e.kdocs_doc_url||"",J.value=e.kdocs_default_unit||"",W.value=e.kdocs_sheet_name||"",X.value=e.kdocs_sheet_index??0,Z.value=(e.kdocs_unit_column||"A").toUpperCase(),ee.value=(e.kdocs_image_column||"D").toUpperCase(),le.value=e.kdocs_row_start??0,ae.value=e.kdocs_row_end??0,oe.value=(e.kdocs_admin_notify_enabled??0)===1,te.value=e.kdocs_admin_notify_email||"",I.value=(u.social_login_enabled??0)===1,h.value=u.social_login_endpoint||"https://www.spacezs.cn/connect.php",K.value=u.social_login_appid||"",V.value="",O.value=u.social_login_appkey_masked||"",j.value=!!u.social_login_appkey_configured,x.value=Array.isArray(u.social_login_providers)&&u.social_login_providers.length?u.social_login_providers:["wx"]}catch{}finally{_.value=!1}const o=Se({maxAgeMs:600*1e3});o&&(c.value=o,N.value=!1),Le()}async function Le(){if(!(N.value||P.value)){N.value=!0;try{const o=await We({force:!1,maxAgeMs:6e4,silent:!0,live:0});c.value=o||{}}catch{}finally{N.value=!1}}}async function Ie(){const o={max_concurrent_global:Number(B.value),max_concurrent_per_account:Number(q.value),max_screenshot_concurrent:Number(M.value),db_slow_query_ms:Number(z.value)};try{await _e.confirm(`确定更新并发配置吗?
|
||||||
|
|
||||||
全局并发数: ${o.max_concurrent_global}
|
全局并发数: ${o.max_concurrent_global}
|
||||||
单账号并发数: ${o.max_concurrent_per_account}
|
单账号并发数: ${o.max_concurrent_per_account}
|
||||||
File diff suppressed because one or more lines are too long
1
static/admin/assets/admin-VsbfHbbH.js
Normal file
1
static/admin/assets/admin-VsbfHbbH.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{a as s}from"./index-6ynv0Z9Y.js";async function e(a){const{data:n}=await s.put("/admin/username",{new_username:a});return n}async function r(a={}){const n=String(a.currentPassword||""),t=String(a.newPassword||""),{data:i}=await s.put("/admin/password",{current_password:n,new_password:t});return i}async function c(){const{data:a}=await s.post("/logout");return a}async function d(){const{data:a}=await s.get("/admin/passkeys");return a}async function u(a={}){const{data:n}=await s.post("/admin/passkeys/register/options",a);return n}async function m(a={}){const{data:n}=await s.post("/admin/passkeys/register/verify",a);return n}async function p(a){const{data:n}=await s.delete(`/admin/passkeys/${a}`);return n}async function l(a={}){const{data:n}=await s.post("/admin/passkeys/client-error",a);return n}async function w(){const{data:a}=await s.get("/admin/social-bindings");return a}async function y(a={}){const{data:n}=await s.post("/admin/social-login-url",a);return n}async function f(a={}){const{data:n}=await s.post("/admin/social-poll",a);return n}async function g(a,n={}){const{data:t}=await s.post(`/admin/social-bindings/${encodeURIComponent(a)}/callback`,n);return t}async function k(a){const{data:n}=await s.delete(`/admin/social-bindings/${encodeURIComponent(a)}`);return n}export{w as a,r as b,u as c,m as d,p as e,d as f,k as g,y as h,g as i,c as l,f as p,l as r,e as u};
|
||||||
@@ -1 +1 @@
|
|||||||
import{c as s,a as e}from"./index-DOvMEmc8.js";const n=s(async()=>{const{data:a}=await e.get("/email/stats");return a},1e4);async function i(){const{data:a}=await e.get("/email/settings");return a}async function r(a){const{data:t}=await e.post("/email/settings",a);return n.clear(),t}async function o(a={}){return n.run(a)}async function l(a){const{data:t}=await e.get("/email/logs",{params:a});return t}async function u(a){const{data:t}=await e.post("/email/logs/cleanup",{days:a});return n.clear(),t}export{l as a,i as b,u as c,o as f,r as u};
|
import{c as s,a as e}from"./index-6ynv0Z9Y.js";const n=s(async()=>{const{data:a}=await e.get("/email/stats");return a},1e4);async function i(){const{data:a}=await e.get("/email/settings");return a}async function r(a){const{data:t}=await e.post("/email/settings",a);return n.clear(),t}async function o(a={}){return n.run(a)}async function l(a){const{data:t}=await e.get("/email/logs",{params:a});return t}async function u(a){const{data:t}=await e.post("/email/logs/cleanup",{days:a});return n.clear(),t}export{l as a,i as b,u as c,o as f,r as u};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{c as s,a as n}from"./index-DOvMEmc8.js";const o=s(async()=>{const{data:t}=await n.get("/system/config");return t},15e3);async function i(t={}){return o.run(t)}async function e(t){const{data:a}=await n.post("/system/config",t);return o.clear(),a}async function r(){const{data:t}=await n.get("/social-login/config");return t}async function f(t){const{data:a}=await n.post("/social-login/config",t||{});return o.clear(),a}async function g(t){const{data:a}=await n.post("/social-login/test",t||{});return a}export{r as a,f as b,i as f,g as t,e as u};
|
import{c as s,a as n}from"./index-6ynv0Z9Y.js";const o=s(async()=>{const{data:t}=await n.get("/system/config");return t},15e3);async function i(t={}){return o.run(t)}async function e(t){const{data:a}=await n.post("/system/config",t);return o.clear(),a}async function r(){const{data:t}=await n.get("/social-login/config");return t}async function f(t){const{data:a}=await n.post("/social-login/config",t||{});return o.clear(),a}async function g(t){const{data:a}=await n.post("/social-login/test",t||{});return a}export{r as a,f as b,i as f,g as t,e as u};
|
||||||
@@ -1 +1 @@
|
|||||||
import{c as s,a}from"./index-DOvMEmc8.js";const c=s(async()=>{const{data:t}=await a.get("/server/info");return t},3e4),o=s(async()=>{const{data:t}=await a.get("/docker_stats");return t},8e3),u=s(async()=>{const{data:t}=await a.get("/request_metrics");return t},1e4),i=s(async()=>{const{data:t}=await a.get("/slow_sql_metrics");return t},1e4),e=s(async()=>{const{data:t}=await a.get("/task/stats");return t},4e3),r=s(async()=>{const{data:t}=await a.get("/task/running");return t},2e3);async function g(t={}){return c.run(t)}async function y(t={}){return o.run(t)}async function d(t={}){return u.run(t)}async function k(t={}){return i.run(t)}async function l(t={}){return e.run(t)}async function w(t={}){return r.run(t)}async function _(t){const{data:n}=await a.get("/task/logs",{params:t});return n}async function h(t){const{data:n}=await a.post("/task/logs/clear",{days:t});return e.clear(),r.clear(),n}export{w as a,g as b,y as c,d,k as e,l as f,_ as g,h};
|
import{c as s,a}from"./index-6ynv0Z9Y.js";const c=s(async()=>{const{data:t}=await a.get("/server/info");return t},3e4),o=s(async()=>{const{data:t}=await a.get("/docker_stats");return t},8e3),u=s(async()=>{const{data:t}=await a.get("/request_metrics");return t},1e4),i=s(async()=>{const{data:t}=await a.get("/slow_sql_metrics");return t},1e4),e=s(async()=>{const{data:t}=await a.get("/task/stats");return t},4e3),r=s(async()=>{const{data:t}=await a.get("/task/running");return t},2e3);async function g(t={}){return c.run(t)}async function y(t={}){return o.run(t)}async function d(t={}){return u.run(t)}async function k(t={}){return i.run(t)}async function l(t={}){return e.run(t)}async function w(t={}){return r.run(t)}async function _(t){const{data:n}=await a.get("/task/logs",{params:t});return n}async function h(t){const{data:n}=await a.post("/task/logs/clear",{days:t});return e.clear(),r.clear(),n}export{w as a,g as b,y as c,d,k as e,l as f,_ as g,h};
|
||||||
@@ -1 +1 @@
|
|||||||
import{a as t}from"./index-DOvMEmc8.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
import{a as t}from"./index-6ynv0Z9Y.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
static/admin/assets/vendor-misc-DszMq72k.js
Normal file
12
static/admin/assets/vendor-misc-DszMq72k.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,16 +5,15 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>后台管理 - 知识管理平台</title>
|
<title>后台管理 - 知识管理平台</title>
|
||||||
<script type="module" crossorigin src="./assets/index-DOvMEmc8.js"></script>
|
<script type="module" crossorigin src="./assets/index-6ynv0Z9Y.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="./assets/vendor-vue-CVxSw_oJ.js">
|
<link rel="modulepreload" crossorigin href="./assets/vendor-vue-CVxSw_oJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="./assets/vendor-misc-BeoNyvBp.js">
|
<link rel="modulepreload" crossorigin href="./assets/vendor-misc-DszMq72k.js">
|
||||||
<link rel="modulepreload" crossorigin href="./assets/vendor-element-B5S5pUKo.js">
|
<link rel="modulepreload" crossorigin href="./assets/vendor-element-CIudPaVX.js">
|
||||||
<link rel="modulepreload" crossorigin href="./assets/vendor-axios-B9ygI19o.js">
|
<link rel="modulepreload" crossorigin href="./assets/vendor-axios-B9ygI19o.js">
|
||||||
<link rel="stylesheet" crossorigin href="./assets/vendor-element-C68yOrAy.css">
|
<link rel="stylesheet" crossorigin href="./assets/vendor-element-C68yOrAy.css">
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-CPs_XZ2s.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-CPs_XZ2s.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user