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:
parent
f5919a8229
commit
e18f33d60f
@ -49,7 +49,6 @@ COPY_ITEMS = [
|
||||
|
||||
# 服务端口
|
||||
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'}"
|
||||
@ -69,3 +68,8 @@ BUILD_TIMEOUT_HOURS = int(os.getenv("BUILD_TIMEOUT_HOURS", "1"))
|
||||
# 管理员账号配置
|
||||
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
|
||||
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
20
backend/deps.py
Normal 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="无效的认证凭据")
|
||||
@ -1,19 +1,35 @@
|
||||
"""认证 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 ..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"])
|
||||
|
||||
|
||||
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)
|
||||
async def login(request: LoginRequest):
|
||||
"""管理员登录"""
|
||||
if request.username == ADMIN_USERNAME and request.password == ADMIN_PASSWORD:
|
||||
return LoginResponse(
|
||||
token="admin-token",
|
||||
username=request.username,
|
||||
is_admin=True
|
||||
)
|
||||
from fastapi import HTTPException
|
||||
token = _create_token(request.username, is_admin=True)
|
||||
return LoginResponse(token=token, username=request.username, is_admin=True)
|
||||
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"]}
|
||||
|
||||
@ -10,8 +10,9 @@ from ..database import get_db
|
||||
from ..models import BuildConfig
|
||||
from ..schemas import BuildConfigUpdate
|
||||
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()
|
||||
|
||||
@ -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.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": "打包设置已更新"}
|
||||
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models import Task
|
||||
from ..schemas import TaskCreate, TaskResponse
|
||||
from ..deps import get_current_user
|
||||
|
||||
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]
|
||||
|
||||
|
||||
@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)):
|
||||
"""取消任务"""
|
||||
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": "任务已取消"}
|
||||
|
||||
|
||||
@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)):
|
||||
"""删除打包记录"""
|
||||
import shutil
|
||||
|
||||
@ -62,10 +62,15 @@ class BuildQueue:
|
||||
task.cancel()
|
||||
|
||||
def update_max_concurrent(self, new_max: int):
|
||||
"""更新最大并发数"""
|
||||
"""更新最大并发数(运行时生效)"""
|
||||
if new_max == self._max_concurrent:
|
||||
return
|
||||
old = self._max_concurrent
|
||||
self._max_concurrent = new_max
|
||||
# 注意:运行时修改需要重建 semaphore,这里简化处理
|
||||
# 实际使用时建议重启服务生效
|
||||
# 调整信号量:增加并发时 release 额外的许可,减少时由自然消费收敛
|
||||
if new_max > old:
|
||||
for _ in range(new_max - old):
|
||||
self._semaphore.release()
|
||||
|
||||
@property
|
||||
def queue_size(self) -> int:
|
||||
|
||||
@ -309,6 +309,11 @@ async def run_build_task(task_id: str):
|
||||
if 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)
|
||||
|
||||
@ -330,6 +335,11 @@ async def run_build_task(task_id: str):
|
||||
await log_streamer.emit(task_id, f"临时文件保留在: {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:
|
||||
await asyncio.to_thread(_db_update, db, task,
|
||||
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 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:
|
||||
log_streamer.complete(task_id)
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, AsyncGenerator
|
||||
|
||||
from ..config import LOG_QUEUE_MAX_SIZE
|
||||
|
||||
|
||||
class LogStreamer:
|
||||
"""日志流管理器"""
|
||||
@ -16,7 +18,7 @@ class LogStreamer:
|
||||
|
||||
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._log_lines[task_id] = []
|
||||
return queue
|
||||
|
||||
93
backend/services/notification.py
Normal file
93
backend/services/notification.py
Normal 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}×tamp={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", ""),
|
||||
)
|
||||
@ -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 {}
|
||||
}
|
||||
|
||||
@ -3,3 +3,5 @@ uvicorn[standard]>=0.24.0
|
||||
sqlalchemy>=2.0.0
|
||||
pydantic>=2.0.0
|
||||
python-multipart>=0.0.6
|
||||
PyJWT>=2.8.0
|
||||
httpx>=0.25.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user