fix: 认证系统、登录兼容性、运行时配置、钉钉通知及代码清理
- 实现 JWT 认证中间件,保护配置管理和任务删除接口 - 修复 ConfigView 登录按钮(Vue 3 inject 替代 $root) - 页面刷新时通过 /api/auth/me 校验 token 有效性 - max_concurrent_builds 修改后运行时即时生效 - 实现钉钉 webhook 通知(构建成功/失败自动推送) - 删除未使用的 useWebSocket composable - log_streamer 使用 LOG_QUEUE_MAX_SIZE 配置常量
This commit is contained in:
+33
-5
@@ -41,12 +41,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, provide } from 'vue'
|
||||
|
||||
const isLoggedIn = ref(false)
|
||||
const showLogin = ref(false)
|
||||
const loginForm = ref({ username: '', password: '' })
|
||||
|
||||
// 提供给子组件使用
|
||||
provide('showLogin', showLogin)
|
||||
provide('isLoggedIn', isLoggedIn)
|
||||
|
||||
// 获取存储的 token
|
||||
const getToken = () => localStorage.getItem('authToken') || ''
|
||||
|
||||
// 对外暴露 token 获取方法,供 API 请求使用
|
||||
provide('getToken', getToken)
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
@@ -55,8 +65,10 @@ const login = async () => {
|
||||
body: JSON.stringify(loginForm.value),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
isLoggedIn.value = true
|
||||
showLogin.value = false
|
||||
localStorage.setItem('authToken', data.token)
|
||||
localStorage.setItem('isAdmin', 'true')
|
||||
} else {
|
||||
alert('用户名或密码错误')
|
||||
@@ -68,13 +80,29 @@ const login = async () => {
|
||||
|
||||
const logout = () => {
|
||||
isLoggedIn.value = false
|
||||
localStorage.removeItem('authToken')
|
||||
localStorage.removeItem('isAdmin')
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (localStorage.getItem('isAdmin') === 'true') {
|
||||
isLoggedIn.value = true
|
||||
}
|
||||
// 检查登录状态(验证 token 有效性)
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (!token) return
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
isLoggedIn.value = true
|
||||
} else {
|
||||
// token 无效或过期,清除
|
||||
localStorage.removeItem('authToken')
|
||||
localStorage.removeItem('isAdmin')
|
||||
}
|
||||
} catch {
|
||||
// 网络错误时不清除,保持登录状态
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
export function useWebSocket(taskId) {
|
||||
const logs = ref([])
|
||||
const connected = ref(false)
|
||||
let ws = null
|
||||
|
||||
const connect = () => {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
ws = new WebSocket(`${protocol}//${location.host}/ws/tasks/${taskId}`)
|
||||
|
||||
ws.onopen = () => {
|
||||
connected.value = true
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.level !== 'heartbeat') {
|
||||
logs.value.push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
connected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(disconnect)
|
||||
|
||||
return { logs, connected, connect, disconnect }
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<div v-if="!isLoggedIn" class="access-denied">
|
||||
<h3>需要管理员权限</h3>
|
||||
<p>请先登录管理员账号以访问配置管理页面</p>
|
||||
<button class="btn-login" @click="$root.showLogin = true">登录管理员账号</button>
|
||||
<button class="btn-login" @click="showLogin = true">登录管理员账号</button>
|
||||
</div>
|
||||
<div v-else class="admin-page">
|
||||
<div class="sidebar">
|
||||
@@ -443,10 +443,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, inject } from 'vue'
|
||||
|
||||
const isLoggedIn = ref(localStorage.getItem('isAdmin') === 'true')
|
||||
const showLogin = inject('showLogin')
|
||||
const getToken = inject('getToken')
|
||||
const isLoggedIn = inject('isLoggedIn')
|
||||
const tab = ref('servers')
|
||||
|
||||
// 带认证的 fetch 封装
|
||||
const authFetch = (url, options = {}) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
options.headers = { ...options.headers, 'Authorization': `Bearer ${token}` }
|
||||
}
|
||||
return fetch(url, options)
|
||||
}
|
||||
const apps = ref({})
|
||||
const schemes = ref({})
|
||||
const servers = ref({})
|
||||
@@ -475,14 +486,14 @@ onMounted(async () => {
|
||||
|
||||
const loadData = async () => {
|
||||
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes] = await Promise.all([
|
||||
fetch('/api/config/apps'),
|
||||
fetch('/api/config/schemes'),
|
||||
fetch('/api/config/servers'),
|
||||
fetch('/api/config/branches'),
|
||||
fetch('/api/config/build'),
|
||||
fetch('/api/config/upload'),
|
||||
fetch('/api/config'),
|
||||
fetch('/api/config/versions'),
|
||||
authFetch('/api/config/apps'),
|
||||
authFetch('/api/config/schemes'),
|
||||
authFetch('/api/config/servers'),
|
||||
authFetch('/api/config/branches'),
|
||||
authFetch('/api/config/build'),
|
||||
authFetch('/api/config/upload'),
|
||||
authFetch('/api/config'),
|
||||
authFetch('/api/config/versions'),
|
||||
])
|
||||
apps.value = await appsRes.json()
|
||||
schemes.value = await schemesRes.json()
|
||||
@@ -509,7 +520,7 @@ const saveServer = async () => {
|
||||
|
||||
try {
|
||||
if (editingServerName.value) {
|
||||
const res = await fetch(`/api/config/servers/${encodeURIComponent(editingServerName.value)}`, {
|
||||
const res = await authFetch(`/api/config/servers/${encodeURIComponent(editingServerName.value)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(serverForm.value),
|
||||
@@ -519,7 +530,7 @@ const saveServer = async () => {
|
||||
throw new Error(err.detail || '保存失败')
|
||||
}
|
||||
} else {
|
||||
const res = await fetch('/api/config/servers', {
|
||||
const res = await authFetch('/api/config/servers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(serverForm.value),
|
||||
@@ -540,7 +551,7 @@ const saveServer = async () => {
|
||||
const deleteServer = async (name) => {
|
||||
if (!confirm(`确定删除环境「${name}」?`)) return
|
||||
try {
|
||||
const res = await fetch(`/api/config/servers/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
const res = await authFetch(`/api/config/servers/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || '删除失败')
|
||||
@@ -626,13 +637,13 @@ const saveApp = async () => {
|
||||
try {
|
||||
let res
|
||||
if (editingAppId.value) {
|
||||
res = await fetch(`/api/config/apps/${editingAppId.value}`, {
|
||||
res = await authFetch(`/api/config/apps/${editingAppId.value}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(cleanData),
|
||||
})
|
||||
} else {
|
||||
res = await fetch('/api/config/apps', {
|
||||
res = await authFetch('/api/config/apps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(cleanData),
|
||||
@@ -652,7 +663,7 @@ const saveApp = async () => {
|
||||
|
||||
const deleteApp = async (id) => {
|
||||
if (!confirm('确定删除此 App?')) return
|
||||
await fetch(`/api/config/apps/${id}`, { method: 'DELETE' })
|
||||
await authFetch(`/api/config/apps/${id}`, { method: 'DELETE' })
|
||||
await loadData()
|
||||
}
|
||||
|
||||
@@ -671,14 +682,14 @@ const saveScheme = async () => {
|
||||
|
||||
try {
|
||||
if (editingSchemeId.value) {
|
||||
const res = await fetch(`/api/config/schemes/${editingSchemeId.value}`, {
|
||||
const res = await authFetch(`/api/config/schemes/${editingSchemeId.value}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(schemeForm.value),
|
||||
})
|
||||
if (!res.ok) throw new Error('保存失败')
|
||||
} else {
|
||||
const res = await fetch('/api/config/schemes', {
|
||||
const res = await authFetch('/api/config/schemes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(schemeForm.value),
|
||||
@@ -695,7 +706,7 @@ const saveScheme = async () => {
|
||||
|
||||
const deleteScheme = async (id) => {
|
||||
if (!confirm('确定删除此 Scheme?')) return
|
||||
await fetch(`/api/config/schemes/${id}`, { method: 'DELETE' })
|
||||
await authFetch(`/api/config/schemes/${id}`, { method: 'DELETE' })
|
||||
await loadData()
|
||||
}
|
||||
|
||||
@@ -704,7 +715,7 @@ const addBranch = async () => {
|
||||
const name = newBranch.value.trim()
|
||||
if (!name) return
|
||||
try {
|
||||
const res = await fetch('/api/config/branches', {
|
||||
const res = await authFetch('/api/config/branches', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
@@ -723,7 +734,7 @@ const addBranch = async () => {
|
||||
const deleteBranch = async (name) => {
|
||||
if (!confirm(`确定删除分支「${name}」?`)) return
|
||||
try {
|
||||
const res = await fetch(`/api/config/branches/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
const res = await authFetch(`/api/config/branches/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
if (!res.ok) throw new Error('删除失败')
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
@@ -733,7 +744,7 @@ const deleteBranch = async (name) => {
|
||||
|
||||
// 其他
|
||||
const saveBuildSettings = async () => {
|
||||
await fetch('/api/config/build', {
|
||||
await authFetch('/api/config/build', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(buildSettings.value),
|
||||
@@ -743,7 +754,7 @@ const saveBuildSettings = async () => {
|
||||
|
||||
const saveVersions = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/config/versions', {
|
||||
const res = await authFetch('/api/config/versions', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(versions.value),
|
||||
@@ -763,7 +774,7 @@ const saveVersions = async () => {
|
||||
|
||||
const saveUploadConfig = async () => {
|
||||
try {
|
||||
await fetch('/api/config/upload', {
|
||||
await authFetch('/api/config/upload', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(uploadConfig.value),
|
||||
@@ -777,7 +788,7 @@ const saveUploadConfig = async () => {
|
||||
const saveJson = async () => {
|
||||
try {
|
||||
const config = JSON.parse(jsonContent.value)
|
||||
await fetch('/api/config', {
|
||||
await authFetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
|
||||
@@ -144,9 +144,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, nextTick, onUnmounted, inject } from 'vue'
|
||||
|
||||
const getToken = inject('getToken')
|
||||
const tasks = ref([])
|
||||
|
||||
const authFetch = (url, options = {}) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
options.headers = { ...options.headers, 'Authorization': `Bearer ${token}` }
|
||||
}
|
||||
return fetch(url, options)
|
||||
}
|
||||
const filterBuildType = ref('')
|
||||
const filterStatus = ref('')
|
||||
const showLogModal = ref(false)
|
||||
@@ -268,7 +277,7 @@ const downloadObfMaps = (taskId) => {
|
||||
const deleteTask = async (taskId) => {
|
||||
if (!confirm('确定要删除这条打包记录吗?')) return
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}/delete`, { method: 'DELETE' })
|
||||
const res = await authFetch(`/api/tasks/${taskId}/delete`, { method: 'DELETE' })
|
||||
if (res.ok) tasks.value = tasks.value.filter(t => t.id !== taskId)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user