feat: 全站登录保护,管理后台仅管理员可访问
- tasks 路由级认证保护,所有 API 需登录 - 未登录时显示登录页面,不暴露导航和功能 - ConfigView 仅管理员可访问,普通用户显示权限提示 - BuildView/HistoryView 所有 API 请求携带 token
This commit is contained in:
parent
a0c857f3c2
commit
1cecf66d94
@ -15,7 +15,7 @@ from ..models import Task
|
|||||||
from ..schemas import TaskCreate, TaskResponse
|
from ..schemas import TaskCreate, TaskResponse
|
||||||
from ..deps import get_current_user
|
from ..deps import get_current_user
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
|
router = APIRouter(prefix="/api/tasks", tags=["tasks"], dependencies=[Depends(get_current_user)])
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=TaskResponse)
|
@router.post("", response_model=TaskResponse)
|
||||||
@ -106,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}", dependencies=[Depends(get_current_user)])
|
@router.delete("/{task_id}")
|
||||||
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()
|
||||||
@ -137,7 +137,7 @@ async def cancel_task(task_id: str, db: Session = Depends(get_db)):
|
|||||||
return {"message": "任务已取消"}
|
return {"message": "任务已取消"}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{task_id}/delete", dependencies=[Depends(get_current_user)])
|
@router.delete("/{task_id}/delete")
|
||||||
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
|
||||||
|
|||||||
@ -3,35 +3,46 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>iOS 自动打包服务</h1>
|
<h1>iOS 自动打包服务</h1>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<nav class="nav">
|
<nav v-if="isLoggedIn" class="nav">
|
||||||
<router-link to="/build">打包</router-link>
|
<router-link to="/build">打包</router-link>
|
||||||
<router-link to="/history">历史</router-link>
|
<router-link to="/history">历史</router-link>
|
||||||
<router-link v-if="isLoggedIn" to="/admin">管理</router-link>
|
<router-link v-if="isAdmin" to="/admin">管理</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<div v-if="!isLoggedIn" class="user-info">
|
<div v-if="!isLoggedIn" class="user-info">
|
||||||
<button class="btn-login" @click="showLogin = true">登录</button>
|
<button class="btn-login" @click="showLogin = true">登录</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="user-info">
|
<div v-else class="user-info">
|
||||||
<div class="user-avatar">A</div>
|
<div class="user-avatar">{{ isAdmin ? 'A' : 'U' }}</div>
|
||||||
<span class="user-name">管理员</span>
|
<span class="user-name">{{ isAdmin ? '管理员' : '用户' }}</span>
|
||||||
<button class="btn-logout" @click="logout">退出</button>
|
<button class="btn-logout" @click="logout">退出</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<router-view />
|
<template v-if="isLoggedIn">
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="login-required">
|
||||||
|
<div class="login-card">
|
||||||
|
<h2>请先登录</h2>
|
||||||
|
<p>使用此服务需要登录账号</p>
|
||||||
|
<button class="login-btn" @click="showLogin = true">立即登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 登录弹窗 -->
|
<!-- 登录弹窗 -->
|
||||||
<div v-if="showLogin" class="modal-overlay" @click.self="showLogin = false">
|
<div v-if="showLogin" class="modal-overlay" @click.self="showLogin = false">
|
||||||
<div class="login-modal">
|
<div class="login-modal">
|
||||||
<h2>管理员登录</h2>
|
<h2>用户登录</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>用户名</label>
|
<label>用户名</label>
|
||||||
<input v-model="loginForm.username" type="text" placeholder="请输入用户名">
|
<input v-model="loginForm.username" type="text" placeholder="请输入用户名" @keyup.enter="login">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>密码</label>
|
<label>密码</label>
|
||||||
<input v-model="loginForm.password" type="password" placeholder="请输入密码">
|
<input v-model="loginForm.password" type="password" placeholder="请输入密码" @keyup.enter="login">
|
||||||
</div>
|
</div>
|
||||||
<button class="login-btn" @click="login">登录</button>
|
<button class="login-btn" @click="login">登录</button>
|
||||||
<div class="login-hint">默认账号: admin / admin123</div>
|
<div class="login-hint">默认账号: admin / admin123</div>
|
||||||
@ -44,12 +55,14 @@
|
|||||||
import { ref, onMounted, provide } from 'vue'
|
import { ref, onMounted, provide } from 'vue'
|
||||||
|
|
||||||
const isLoggedIn = ref(false)
|
const isLoggedIn = ref(false)
|
||||||
|
const isAdmin = ref(false)
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
const loginForm = ref({ username: '', password: '' })
|
const loginForm = ref({ username: '', password: '' })
|
||||||
|
|
||||||
// 提供给子组件使用
|
// 提供给子组件使用
|
||||||
provide('showLogin', showLogin)
|
provide('showLogin', showLogin)
|
||||||
provide('isLoggedIn', isLoggedIn)
|
provide('isLoggedIn', isLoggedIn)
|
||||||
|
provide('isAdmin', isAdmin)
|
||||||
|
|
||||||
// 获取存储的 token
|
// 获取存储的 token
|
||||||
const getToken = () => localStorage.getItem('authToken') || ''
|
const getToken = () => localStorage.getItem('authToken') || ''
|
||||||
@ -67,9 +80,9 @@ const login = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
isLoggedIn.value = true
|
isLoggedIn.value = true
|
||||||
|
isAdmin.value = data.is_admin
|
||||||
showLogin.value = false
|
showLogin.value = false
|
||||||
localStorage.setItem('authToken', data.token)
|
localStorage.setItem('authToken', data.token)
|
||||||
localStorage.setItem('isAdmin', 'true')
|
|
||||||
} else {
|
} else {
|
||||||
alert('用户名或密码错误')
|
alert('用户名或密码错误')
|
||||||
}
|
}
|
||||||
@ -80,27 +93,31 @@ const login = async () => {
|
|||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
isLoggedIn.value = false
|
isLoggedIn.value = false
|
||||||
|
isAdmin.value = false
|
||||||
localStorage.removeItem('authToken')
|
localStorage.removeItem('authToken')
|
||||||
localStorage.removeItem('isAdmin')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查登录状态(验证 token 有效性)
|
// 检查登录状态(验证 token 有效性)
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const token = localStorage.getItem('authToken')
|
const token = localStorage.getItem('authToken')
|
||||||
if (!token) return
|
if (!token) {
|
||||||
|
showLogin.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/auth/me', {
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
isLoggedIn.value = true
|
isLoggedIn.value = true
|
||||||
|
isAdmin.value = data.is_admin
|
||||||
} else {
|
} else {
|
||||||
// token 无效或过期,清除
|
|
||||||
localStorage.removeItem('authToken')
|
localStorage.removeItem('authToken')
|
||||||
localStorage.removeItem('isAdmin')
|
showLogin.value = true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 网络错误时不清除,保持登录状态
|
// 网络错误时保持当前状态
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -116,12 +133,17 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
.nav a.router-link-active { background: #16213e; color: white; }
|
.nav a.router-link-active { background: #16213e; color: white; }
|
||||||
.nav a:hover { color: white; }
|
.nav a:hover { color: white; }
|
||||||
.user-info { display: flex; align-items: center; gap: 10px; }
|
.user-info { display: flex; align-items: center; gap: 10px; }
|
||||||
.user-avatar { width: 32px; height: 32px; background: #1890ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
.user-avatar { width: 32px; height: 32px; background: #1890ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; }
|
||||||
.user-name { font-size: 14px; }
|
.user-name { font-size: 14px; }
|
||||||
.btn-login { background: #1890ff; color: white; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
.btn-login { background: #1890ff; color: white; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||||
.btn-logout { background: transparent; color: #a0a0a0; border: 1px solid #444; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
.btn-logout { background: transparent; color: #a0a0a0; border: 1px solid #444; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||||
.btn-logout:hover { border-color: #ff4d4f; color: #ff4d4f; }
|
.btn-logout:hover { border-color: #ff4d4f; color: #ff4d4f; }
|
||||||
|
|
||||||
|
.login-required { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - 64px); }
|
||||||
|
.login-card { background: white; border-radius: 12px; padding: 48px; text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
|
||||||
|
.login-card h2 { font-size: 20px; color: #1a1a2e; margin-bottom: 8px; }
|
||||||
|
.login-card p { color: #999; margin-bottom: 24px; }
|
||||||
|
|
||||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
.login-modal { background: white; border-radius: 12px; padding: 32px; width: 400px; }
|
.login-modal { background: white; border-radius: 12px; padding: 32px; width: 400px; }
|
||||||
.login-modal h2 { font-size: 20px; margin-bottom: 24px; text-align: center; color: #1a1a2e; }
|
.login-modal h2 { font-size: 20px; margin-bottom: 24px; text-align: center; color: #1a1a2e; }
|
||||||
|
|||||||
@ -138,9 +138,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
import { ref, onMounted, nextTick, watch, onUnmounted, inject } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const getToken = inject('getToken')
|
||||||
|
|
||||||
|
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 branches = ref(['main'])
|
const branches = ref(['main'])
|
||||||
@ -164,7 +174,7 @@ let skipNextConnect = false
|
|||||||
|
|
||||||
const fetchTaskDetail = async (taskId) => {
|
const fetchTaskDetail = async (taskId) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/tasks/${taskId}`)
|
const res = await authFetch(`/api/tasks/${taskId}`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
completedTask.value = await res.json()
|
completedTask.value = await res.json()
|
||||||
}
|
}
|
||||||
@ -175,7 +185,7 @@ const fetchTaskDetail = async (taskId) => {
|
|||||||
|
|
||||||
const fetchBuildLog = async (taskId) => {
|
const fetchBuildLog = async (taskId) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/tasks/${taskId}/log`)
|
const res = await authFetch(`/api/tasks/${taskId}/log`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.log) {
|
if (data.log) {
|
||||||
@ -257,10 +267,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const [appsRes, schemesRes, branchesRes, tasksRes] = await Promise.all([
|
const [appsRes, schemesRes, branchesRes, tasksRes] = await Promise.all([
|
||||||
fetch('/api/apps'),
|
authFetch('/api/apps'),
|
||||||
fetch('/api/schemes'),
|
authFetch('/api/schemes'),
|
||||||
fetch('/api/branches'),
|
authFetch('/api/branches'),
|
||||||
fetch('/api/tasks'),
|
authFetch('/api/tasks'),
|
||||||
])
|
])
|
||||||
apps.value = await appsRes.json()
|
apps.value = await appsRes.json()
|
||||||
schemes.value = await schemesRes.json()
|
schemes.value = await schemesRes.json()
|
||||||
@ -302,7 +312,7 @@ const submitTask = async () => {
|
|||||||
}
|
}
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/tasks', {
|
const res = await authFetch('/api/tasks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(form.value),
|
body: JSON.stringify(form.value),
|
||||||
@ -329,7 +339,7 @@ const selectTask = (taskId) => {
|
|||||||
const cancelTask = async () => {
|
const cancelTask = async () => {
|
||||||
if (!currentTaskId.value) return
|
if (!currentTaskId.value) return
|
||||||
if (!confirm('确定取消当前任务?')) return
|
if (!confirm('确定取消当前任务?')) return
|
||||||
const res = await fetch(`/api/tasks/${currentTaskId.value}`, { method: 'DELETE' })
|
const res = await authFetch(`/api/tasks/${currentTaskId.value}`, { method: 'DELETE' })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({}))
|
const err = await res.json().catch(() => ({}))
|
||||||
alert(err.detail || '取消失败')
|
alert(err.detail || '取消失败')
|
||||||
@ -342,7 +352,7 @@ const cancelTask = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshTasks = async () => {
|
const refreshTasks = async () => {
|
||||||
const res = await fetch('/api/tasks?limit=50')
|
const res = await authFetch('/api/tasks?limit=50')
|
||||||
const all = await res.json()
|
const all = await res.json()
|
||||||
// 显示未完成的任务 + 当前选中的任务
|
// 显示未完成的任务 + 当前选中的任务
|
||||||
tasks.value = all.filter(t =>
|
tasks.value = all.filter(t =>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div v-if="!isLoggedIn" class="access-denied">
|
<div v-if="!isAdmin" class="access-denied">
|
||||||
<h3>需要管理员权限</h3>
|
<h3>需要管理员权限</h3>
|
||||||
<p>请先登录管理员账号以访问配置管理页面</p>
|
<p>{{ isLoggedIn ? '当前账号无管理员权限' : '请先登录管理员账号' }}</p>
|
||||||
<button class="btn-login" @click="showLogin = true">登录管理员账号</button>
|
<button v-if="!isLoggedIn" 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">
|
||||||
@ -448,6 +448,7 @@ import { ref, onMounted, inject } from 'vue'
|
|||||||
const showLogin = inject('showLogin')
|
const showLogin = inject('showLogin')
|
||||||
const getToken = inject('getToken')
|
const getToken = inject('getToken')
|
||||||
const isLoggedIn = inject('isLoggedIn')
|
const isLoggedIn = inject('isLoggedIn')
|
||||||
|
const isAdmin = inject('isAdmin')
|
||||||
const tab = ref('servers')
|
const tab = ref('servers')
|
||||||
|
|
||||||
// 带认证的 fetch 封装
|
// 带认证的 fetch 封装
|
||||||
|
|||||||
@ -176,7 +176,7 @@ const filteredTasks = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const res = await fetch('/api/tasks?limit=100')
|
const res = await authFetch('/api/tasks?limit=100')
|
||||||
tasks.value = await res.json()
|
tasks.value = await res.json()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ const viewLogs = async (taskId) => {
|
|||||||
} else {
|
} else {
|
||||||
// 已完成/失败的任务,先从 REST API 获取完整日志
|
// 已完成/失败的任务,先从 REST API 获取完整日志
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/tasks/${taskId}/log`)
|
const res = await authFetch(`/api/tasks/${taskId}/log`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.log) {
|
if (data.log) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user