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:
shen 2026-06-08 20:35:05 +08:00
parent f5919a8229
commit e18f33d60f
14 changed files with 262 additions and 91 deletions

View File

@ -49,7 +49,6 @@ COPY_ITEMS = [
# 服务端口 # 服务端口
BACKEND_PORT = int(os.getenv("BACKEND_PORT", "8000")) BACKEND_PORT = int(os.getenv("BACKEND_PORT", "8000"))
FRONTEND_PORT = int(os.getenv("FRONTEND_PORT", "3000"))
# 数据库路径 # 数据库路径
DATABASE_URL = f"sqlite:///{Path(__file__).parent / 'build_server.db'}" DATABASE_URL = f"sqlite:///{Path(__file__).parent / 'build_server.db'}"
@ -69,3 +68,8 @@ BUILD_TIMEOUT_HOURS = int(os.getenv("BUILD_TIMEOUT_HOURS", "1"))
# 管理员账号配置 # 管理员账号配置
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin") ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
# JWT 配置
JWT_SECRET = os.getenv("JWT_SECRET", "ios-build-server-secret-key-change-in-production")
JWT_ALGORITHM = "HS256"
JWT_EXPIRE_HOURS = 24

20
backend/deps.py Normal file
View File

@ -0,0 +1,20 @@
"""认证依赖"""
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .config import JWT_SECRET, JWT_ALGORITHM
security = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
"""验证 JWT token返回用户信息"""
token = credentials.credentials
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return {"username": payload["sub"], "is_admin": payload.get("is_admin", False)}
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已过期,请重新登录")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据")

View File

@ -1,19 +1,35 @@
"""认证 API""" """认证 API"""
from fastapi import APIRouter import jwt
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException
from ..schemas import LoginRequest, LoginResponse from ..schemas import LoginRequest, LoginResponse
from ..config import ADMIN_USERNAME, ADMIN_PASSWORD from ..config import ADMIN_USERNAME, ADMIN_PASSWORD, JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_HOURS
from ..deps import get_current_user
router = APIRouter(prefix="/api/auth", tags=["auth"]) router = APIRouter(prefix="/api/auth", tags=["auth"])
def _create_token(username: str, is_admin: bool) -> str:
"""生成 JWT token"""
payload = {
"sub": username,
"is_admin": is_admin,
"exp": datetime.now(timezone.utc) + timedelta(hours=JWT_EXPIRE_HOURS),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
@router.post("/login", response_model=LoginResponse) @router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest): async def login(request: LoginRequest):
"""管理员登录""" """管理员登录"""
if request.username == ADMIN_USERNAME and request.password == ADMIN_PASSWORD: if request.username == ADMIN_USERNAME and request.password == ADMIN_PASSWORD:
return LoginResponse( token = _create_token(request.username, is_admin=True)
token="admin-token", return LoginResponse(token=token, username=request.username, is_admin=True)
username=request.username,
is_admin=True
)
from fastapi import HTTPException
raise HTTPException(status_code=401, detail="用户名或密码错误") raise HTTPException(status_code=401, detail="用户名或密码错误")
@router.get("/me")
async def get_me(user: dict = Depends(get_current_user)):
"""校验 token 有效性,返回当前用户信息"""
return {"username": user["username"], "is_admin": user["is_admin"]}

View File

@ -10,8 +10,9 @@ from ..database import get_db
from ..models import BuildConfig from ..models import BuildConfig
from ..schemas import BuildConfigUpdate from ..schemas import BuildConfigUpdate
from ..config import CONFIG_JSON_PATH, DEFAULT_MAX_CONCURRENT_BUILDS, DEFAULT_BUILD_DIR_RETENTION_HOURS from ..config import CONFIG_JSON_PATH, DEFAULT_MAX_CONCURRENT_BUILDS, DEFAULT_BUILD_DIR_RETENTION_HOURS
from ..deps import get_current_user
router = APIRouter(prefix="/api/config", tags=["config"]) router = APIRouter(prefix="/api/config", tags=["config"], dependencies=[Depends(get_current_user)])
_config_lock = asyncio.Lock() _config_lock = asyncio.Lock()
@ -314,6 +315,12 @@ async def update_build_settings(settings: BuildConfigUpdate, db: Session = Depen
db.add(BuildConfig(key="build_base_dir", value=settings.build_base_dir)) db.add(BuildConfig(key="build_base_dir", value=settings.build_base_dir))
db.commit() db.commit()
# 运行时更新并发数
if settings.max_concurrent_builds is not None:
from ..services.build_queue import build_queue
build_queue.update_max_concurrent(settings.max_concurrent_builds)
return {"message": "打包设置已更新"} return {"message": "打包设置已更新"}

View File

@ -13,6 +13,7 @@ from sqlalchemy.orm import Session
from ..database import get_db from ..database import get_db
from ..models import Task from ..models import Task
from ..schemas import TaskCreate, TaskResponse from ..schemas import TaskCreate, TaskResponse
from ..deps import get_current_user
router = APIRouter(prefix="/api/tasks", tags=["tasks"]) router = APIRouter(prefix="/api/tasks", tags=["tasks"])
@ -105,7 +106,7 @@ async def get_task(task_id: str, db: Session = Depends(get_db)):
return _enrich_tasks([task])[0] return _enrich_tasks([task])[0]
@router.delete("/{task_id}") @router.delete("/{task_id}", dependencies=[Depends(get_current_user)])
async def cancel_task(task_id: str, db: Session = Depends(get_db)): async def cancel_task(task_id: str, db: Session = Depends(get_db)):
"""取消任务""" """取消任务"""
task = db.query(Task).filter(Task.id == task_id).first() task = db.query(Task).filter(Task.id == task_id).first()
@ -136,7 +137,7 @@ async def cancel_task(task_id: str, db: Session = Depends(get_db)):
return {"message": "任务已取消"} return {"message": "任务已取消"}
@router.delete("/{task_id}/delete") @router.delete("/{task_id}/delete", dependencies=[Depends(get_current_user)])
async def delete_task(task_id: str, db: Session = Depends(get_db)): async def delete_task(task_id: str, db: Session = Depends(get_db)):
"""删除打包记录""" """删除打包记录"""
import shutil import shutil

View File

@ -62,10 +62,15 @@ class BuildQueue:
task.cancel() task.cancel()
def update_max_concurrent(self, new_max: int): def update_max_concurrent(self, new_max: int):
"""更新最大并发数""" """更新最大并发数(运行时生效)"""
if new_max == self._max_concurrent:
return
old = self._max_concurrent
self._max_concurrent = new_max self._max_concurrent = new_max
# 注意:运行时修改需要重建 semaphore这里简化处理 # 调整信号量:增加并发时 release 额外的许可,减少时由自然消费收敛
# 实际使用时建议重启服务生效 if new_max > old:
for _ in range(new_max - old):
self._semaphore.release()
@property @property
def queue_size(self) -> int: def queue_size(self) -> int:

View File

@ -309,6 +309,11 @@ async def run_build_task(task_id: str):
if task.oss_url: if task.oss_url:
await log_streamer.emit(task_id, f"下载链接: {task.oss_url}") await log_streamer.emit(task_id, f"下载链接: {task.oss_url}")
# 钉钉通知
from .notification import notify_build_result
dingtalk = full_config.get("upload", {}).get("dingtalk", {})
await notify_build_result(task, dingtalk)
# 带超时执行打包 # 带超时执行打包
await asyncio.wait_for(_do_build(), timeout=timeout_seconds) await asyncio.wait_for(_do_build(), timeout=timeout_seconds)
@ -330,6 +335,11 @@ async def run_build_task(task_id: str):
await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}") await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}")
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir) await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
# 钉钉通知
from .notification import notify_build_result
dingtalk = full_config.get("upload", {}).get("dingtalk", {})
await notify_build_result(task, dingtalk)
except asyncio.CancelledError: except asyncio.CancelledError:
await asyncio.to_thread(_db_update, db, task, await asyncio.to_thread(_db_update, db, task,
status="cancelled", completed_at=datetime.utcnow()) status="cancelled", completed_at=datetime.utcnow())
@ -362,6 +372,11 @@ async def run_build_task(task_id: str):
await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}") await log_streamer.emit(task_id, f"临时文件保留在: {build_dir}")
await asyncio.to_thread(log_streamer.save_log, task_id, build_dir) await asyncio.to_thread(log_streamer.save_log, task_id, build_dir)
# 钉钉通知
from .notification import notify_build_result
dingtalk = full_config.get("upload", {}).get("dingtalk", {})
await notify_build_result(task, dingtalk)
finally: finally:
log_streamer.complete(task_id) log_streamer.complete(task_id)

View File

@ -5,6 +5,8 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, AsyncGenerator from typing import Dict, AsyncGenerator
from ..config import LOG_QUEUE_MAX_SIZE
class LogStreamer: class LogStreamer:
"""日志流管理器""" """日志流管理器"""
@ -16,7 +18,7 @@ class LogStreamer:
def create_queue(self, task_id: str) -> asyncio.Queue: def create_queue(self, task_id: str) -> asyncio.Queue:
"""创建任务的日志队列""" """创建任务的日志队列"""
queue = asyncio.Queue(maxsize=1000) queue = asyncio.Queue(maxsize=LOG_QUEUE_MAX_SIZE)
self._queues[task_id] = queue self._queues[task_id] = queue
self._log_lines[task_id] = [] self._log_lines[task_id] = []
return queue return queue

View File

@ -0,0 +1,93 @@
"""钉钉通知服务"""
import asyncio
import hashlib
import hmac
import base64
import time
import urllib.parse
import json
import logging
logger = logging.getLogger(__name__)
def _sign(secret: str) -> tuple[str, str]:
"""生成钉钉加签参数"""
timestamp = str(round(time.time() * 1000))
string_to_sign = f"{timestamp}\n{secret}"
hmac_code = hmac.new(
secret.encode("utf-8"),
string_to_sign.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
return timestamp, sign
async def send_dingtalk(webhook_url: str, title: str, text: str, secret: str = ""):
"""发送钉钉 Markdown 通知"""
import httpx
url = webhook_url
if secret:
timestamp, sign = _sign(secret)
url = f"{url}&timestamp={timestamp}&sign={sign}"
payload = {
"msgtype": "markdown",
"markdown": {"title": title, "text": text},
}
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(url, json=payload)
data = resp.json()
if data.get("errcode") != 0:
logger.warning(f"钉钉通知发送失败: {data}")
except Exception as e:
logger.warning(f"钉钉通知异常: {e}")
async def notify_build_result(task, dingtalk_config: dict):
"""发送打包结果通知"""
if not dingtalk_config.get("enabled") or not dingtalk_config.get("webhook_url"):
return
status = task.status
app_name = task.app_name
build_type = task.build_type
scheme_name = task.scheme_name
if status == "completed":
title = f"{app_name} 打包成功"
lines = [
f"### ✅ {app_name} 打包成功",
f"- **应用**: {app_name}",
f"- **类型**: {build_type}",
f"- **Scheme**: {scheme_name}",
f"- **分支**: {task.branch}",
]
if task.oss_url:
lines.append(f"- **下载链接**: [点击下载]({task.oss_url})")
text = "\n".join(lines)
elif status == "failed":
title = f"{app_name} 打包失败"
error = task.error_message or "未知错误"
lines = [
f"### ❌ {app_name} 打包失败",
f"- **应用**: {app_name}",
f"- **类型**: {build_type}",
f"- **Scheme**: {scheme_name}",
f"- **分支**: {task.branch}",
f"- **错误**: {error}",
]
text = "\n".join(lines)
else:
return
await send_dingtalk(
dingtalk_config["webhook_url"],
title,
text,
dingtalk_config.get("secret", ""),
)

View File

@ -41,12 +41,22 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted, provide } from 'vue'
const isLoggedIn = ref(false) const isLoggedIn = ref(false)
const showLogin = ref(false) const showLogin = ref(false)
const loginForm = ref({ username: '', password: '' }) 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 () => { const login = async () => {
try { try {
const res = await fetch('/api/auth/login', { const res = await fetch('/api/auth/login', {
@ -55,8 +65,10 @@ const login = async () => {
body: JSON.stringify(loginForm.value), body: JSON.stringify(loginForm.value),
}) })
if (res.ok) { if (res.ok) {
const data = await res.json()
isLoggedIn.value = true isLoggedIn.value = true
showLogin.value = false showLogin.value = false
localStorage.setItem('authToken', data.token)
localStorage.setItem('isAdmin', 'true') localStorage.setItem('isAdmin', 'true')
} else { } else {
alert('用户名或密码错误') alert('用户名或密码错误')
@ -68,13 +80,29 @@ const login = async () => {
const logout = () => { const logout = () => {
isLoggedIn.value = false isLoggedIn.value = false
localStorage.removeItem('authToken')
localStorage.removeItem('isAdmin') localStorage.removeItem('isAdmin')
} }
// // token
if (localStorage.getItem('isAdmin') === 'true') { 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 isLoggedIn.value = true
} } else {
// token
localStorage.removeItem('authToken')
localStorage.removeItem('isAdmin')
}
} catch {
//
}
})
</script> </script>
<style> <style>

View File

@ -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 }
}

View File

@ -3,7 +3,7 @@
<div v-if="!isLoggedIn" class="access-denied"> <div v-if="!isLoggedIn" class="access-denied">
<h3>需要管理员权限</h3> <h3>需要管理员权限</h3>
<p>请先登录管理员账号以访问配置管理页面</p> <p>请先登录管理员账号以访问配置管理页面</p>
<button class="btn-login" @click="$root.showLogin = true">登录管理员账号</button> <button class="btn-login" @click="showLogin = true">登录管理员账号</button>
</div> </div>
<div v-else class="admin-page"> <div v-else class="admin-page">
<div class="sidebar"> <div class="sidebar">
@ -443,10 +443,21 @@
</template> </template>
<script setup> <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') 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 apps = ref({})
const schemes = ref({}) const schemes = ref({})
const servers = ref({}) const servers = ref({})
@ -475,14 +486,14 @@ onMounted(async () => {
const loadData = async () => { const loadData = async () => {
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes] = await Promise.all([ const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes] = await Promise.all([
fetch('/api/config/apps'), authFetch('/api/config/apps'),
fetch('/api/config/schemes'), authFetch('/api/config/schemes'),
fetch('/api/config/servers'), authFetch('/api/config/servers'),
fetch('/api/config/branches'), authFetch('/api/config/branches'),
fetch('/api/config/build'), authFetch('/api/config/build'),
fetch('/api/config/upload'), authFetch('/api/config/upload'),
fetch('/api/config'), authFetch('/api/config'),
fetch('/api/config/versions'), authFetch('/api/config/versions'),
]) ])
apps.value = await appsRes.json() apps.value = await appsRes.json()
schemes.value = await schemesRes.json() schemes.value = await schemesRes.json()
@ -509,7 +520,7 @@ const saveServer = async () => {
try { try {
if (editingServerName.value) { 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', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serverForm.value), body: JSON.stringify(serverForm.value),
@ -519,7 +530,7 @@ const saveServer = async () => {
throw new Error(err.detail || '保存失败') throw new Error(err.detail || '保存失败')
} }
} else { } else {
const res = await fetch('/api/config/servers', { const res = await authFetch('/api/config/servers', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serverForm.value), body: JSON.stringify(serverForm.value),
@ -540,7 +551,7 @@ const saveServer = async () => {
const deleteServer = async (name) => { const deleteServer = async (name) => {
if (!confirm(`确定删除环境「${name}」?`)) return if (!confirm(`确定删除环境「${name}」?`)) return
try { 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) { if (!res.ok) {
const err = await res.json() const err = await res.json()
throw new Error(err.detail || '删除失败') throw new Error(err.detail || '删除失败')
@ -626,13 +637,13 @@ const saveApp = async () => {
try { try {
let res let res
if (editingAppId.value) { if (editingAppId.value) {
res = await fetch(`/api/config/apps/${editingAppId.value}`, { res = await authFetch(`/api/config/apps/${editingAppId.value}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cleanData), body: JSON.stringify(cleanData),
}) })
} else { } else {
res = await fetch('/api/config/apps', { res = await authFetch('/api/config/apps', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cleanData), body: JSON.stringify(cleanData),
@ -652,7 +663,7 @@ const saveApp = async () => {
const deleteApp = async (id) => { const deleteApp = async (id) => {
if (!confirm('确定删除此 App')) return if (!confirm('确定删除此 App')) return
await fetch(`/api/config/apps/${id}`, { method: 'DELETE' }) await authFetch(`/api/config/apps/${id}`, { method: 'DELETE' })
await loadData() await loadData()
} }
@ -671,14 +682,14 @@ const saveScheme = async () => {
try { try {
if (editingSchemeId.value) { if (editingSchemeId.value) {
const res = await fetch(`/api/config/schemes/${editingSchemeId.value}`, { const res = await authFetch(`/api/config/schemes/${editingSchemeId.value}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(schemeForm.value), body: JSON.stringify(schemeForm.value),
}) })
if (!res.ok) throw new Error('保存失败') if (!res.ok) throw new Error('保存失败')
} else { } else {
const res = await fetch('/api/config/schemes', { const res = await authFetch('/api/config/schemes', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(schemeForm.value), body: JSON.stringify(schemeForm.value),
@ -695,7 +706,7 @@ const saveScheme = async () => {
const deleteScheme = async (id) => { const deleteScheme = async (id) => {
if (!confirm('确定删除此 Scheme')) return if (!confirm('确定删除此 Scheme')) return
await fetch(`/api/config/schemes/${id}`, { method: 'DELETE' }) await authFetch(`/api/config/schemes/${id}`, { method: 'DELETE' })
await loadData() await loadData()
} }
@ -704,7 +715,7 @@ const addBranch = async () => {
const name = newBranch.value.trim() const name = newBranch.value.trim()
if (!name) return if (!name) return
try { try {
const res = await fetch('/api/config/branches', { const res = await authFetch('/api/config/branches', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
@ -723,7 +734,7 @@ const addBranch = async () => {
const deleteBranch = async (name) => { const deleteBranch = async (name) => {
if (!confirm(`确定删除分支「${name}」?`)) return if (!confirm(`确定删除分支「${name}」?`)) return
try { 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('删除失败') if (!res.ok) throw new Error('删除失败')
await loadData() await loadData()
} catch (e) { } catch (e) {
@ -733,7 +744,7 @@ const deleteBranch = async (name) => {
// //
const saveBuildSettings = async () => { const saveBuildSettings = async () => {
await fetch('/api/config/build', { await authFetch('/api/config/build', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildSettings.value), body: JSON.stringify(buildSettings.value),
@ -743,7 +754,7 @@ const saveBuildSettings = async () => {
const saveVersions = async () => { const saveVersions = async () => {
try { try {
const res = await fetch('/api/config/versions', { const res = await authFetch('/api/config/versions', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(versions.value), body: JSON.stringify(versions.value),
@ -763,7 +774,7 @@ const saveVersions = async () => {
const saveUploadConfig = async () => { const saveUploadConfig = async () => {
try { try {
await fetch('/api/config/upload', { await authFetch('/api/config/upload', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(uploadConfig.value), body: JSON.stringify(uploadConfig.value),
@ -777,7 +788,7 @@ const saveUploadConfig = async () => {
const saveJson = async () => { const saveJson = async () => {
try { try {
const config = JSON.parse(jsonContent.value) const config = JSON.parse(jsonContent.value)
await fetch('/api/config', { await authFetch('/api/config', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config), body: JSON.stringify(config),

View File

@ -144,9 +144,18 @@
</template> </template>
<script setup> <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 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 filterBuildType = ref('')
const filterStatus = ref('') const filterStatus = ref('')
const showLogModal = ref(false) const showLogModal = ref(false)
@ -268,7 +277,7 @@ const downloadObfMaps = (taskId) => {
const deleteTask = async (taskId) => { const deleteTask = async (taskId) => {
if (!confirm('确定要删除这条打包记录吗?')) return if (!confirm('确定要删除这条打包记录吗?')) return
try { 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) if (res.ok) tasks.value = tasks.value.filter(t => t.id !== taskId)
} catch {} } catch {}
} }

View File

@ -3,3 +3,5 @@ uvicorn[standard]>=0.24.0
sqlalchemy>=2.0.0 sqlalchemy>=2.0.0
pydantic>=2.0.0 pydantic>=2.0.0
python-multipart>=0.0.6 python-multipart>=0.0.6
PyJWT>=2.8.0
httpx>=0.25.0