fix(auth): 修复启动脚本与前端认证流程

修复 start.sh 未记录后台进程 PID 导致启动后误判失败的问题。\n调整前端认证逻辑,支持开发环境下优先读取 URL token,并使用 POST 请求直连外部认证接口解析新的返回结构。\n补充认证回归测试,覆盖请求方法、请求头和用户信息映射。
This commit is contained in:
肖应宇 2026-06-12 14:09:19 +08:00
parent a130e0d3f5
commit 4a434f9580
3 changed files with 195 additions and 57 deletions

View File

@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
describe('Auth Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
vi.clearAllMocks()
window.history.replaceState({}, '', '/')
})
it('prefers URL token and validates it even in dev mode', async () => {
window.history.replaceState({}, '', '/?token=url-token')
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
status: 1000,
message: '返回成功',
data: {
token: 'url-token',
userInfo: {
userId: 19,
userName: 'test20',
realName: '测试学生3',
email: '',
userIcon: 'defaultUserIcon.png',
},
},
}),
} as Response)
const { useAuthStore } = await import('@/stores/auth')
const store = useAuthStore()
await store.init()
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(
'http://test.xueai.art/newapi/api/login/validateToken',
{
method: 'POST',
headers: {
authorization: 'url-token',
},
},
)
expect(store.token).toBe('url-token')
expect(store.user?.id).toBe('19')
expect(store.user?.username).toBe('test20')
expect(store.user?.nickname).toBe('测试学生3')
})
})

View File

@ -18,15 +18,24 @@ const DEV_BYPASS_USER: UserInfo = {
// 认证接口返回格式
interface AuthResponse {
code: string;
msg: string;
success: boolean;
timestamp: number;
data: UserInfo | null;
status: number;
message: string;
data: {
token: string;
userInfo: {
userId: number | string;
userName?: string;
realName?: string;
email?: string;
userIcon?: string;
[key: string]: unknown;
};
[key: string]: unknown;
} | null;
}
// 认证接口
const AUTH_CHECK_URL = '/api/auth/check/checkTokenRn';
const AUTH_CHECK_URL = 'http://test.xueai.art/newapi/api/login/validateToken';
const AUTH_TOKEN_STORAGE_KEY = 'DEV_DEFAULT_TOKEN';
export const useAuthStore = defineStore('auth', () => {
@ -44,16 +53,28 @@ export const useAuthStore = defineStore('auth', () => {
*/
async function checkToken(tokenToCheck: string): Promise<UserInfo | null> {
try {
const response = await fetch(`${AUTH_CHECK_URL}/${tokenToCheck}`);
const response = await fetch(AUTH_CHECK_URL, {
method: 'POST',
headers: {
authorization: tokenToCheck,
},
});
if (!response.ok) {
return null;
}
const data: AuthResponse = await response.json();
if (data.success && data.data) {
return data.data;
}else{
if (data.status === 1000 && data.data?.userInfo) {
const { userInfo } = data.data;
return {
...userInfo,
id: String(userInfo.userId),
username: userInfo.userName,
nickname: userInfo.realName,
email: userInfo.email,
avatar: userInfo.userIcon,
};
} else {
window.$toast?.('[Auth] Token 验证失败:Token无效');
}
return null;
@ -68,13 +89,6 @@ export const useAuthStore = defineStore('auth', () => {
* - URL token
*/
async function init() {
if (DEV_AUTH_BYPASS) {
token.value = null;
user.value = DEV_BYPASS_USER;
isInitialized.value = true;
return;
}
const searchParams = new URLSearchParams(window.location.search);
const urlToken = searchParams.get('token');
@ -83,6 +97,13 @@ export const useAuthStore = defineStore('auth', () => {
|| localStorage.getItem(AUTH_TOKEN_STORAGE_KEY)
|| DEV_DEFAULT_TOKEN;
if (DEV_AUTH_BYPASS && !tokenValue) {
token.value = null;
user.value = DEV_BYPASS_USER;
isInitialized.value = true;
return;
}
if (!tokenValue) {
isInitialized.value = true;
window.$toast?.('未登录,请先登录', 'error');
@ -122,9 +143,6 @@ export const useAuthStore = defineStore('auth', () => {
return {};
}
// 初始化(不等待,让调用方通过 isInitialized 判断)
init();
return {
// 状态
token,

102
start.sh
View File

@ -1,46 +1,112 @@
#!/bin/bash
#!/usr/bin/env bash
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$SCRIPT_DIR"
SERVER_DIR="$ROOT_DIR/server"
BACKEND_PORT="${PORT:-8002}"
FRONTEND_PORT="${FRONTEND_PORT:-5173}"
BACKEND_PID=""
FRONTEND_PID=""
echo "=========================================="
echo " 启动 AI Chat 平台 (前端 + 后端) "
echo " 启动 AI Chat 平台 (前端 + 后端)"
echo "=========================================="
# 设置清理函数,在收到 Ctrl+C 时关闭所有子进程
cleanup() {
echo ""
echo "正在关闭所有服务..."
kill $(jobs -p) 2>/dev/null
exit
require_file() {
local path="$1"
local message="$2"
if [[ ! -e "$path" ]]; then
echo "[错误] $message"
exit 1
fi
}
# 清除旧进程
lsof -i :8000 -t | xargs -r kill -9; lsof -i :5173 -t | xargs -r kill -9;
kill_port() {
local port="$1"
local pids
if ! command -v lsof >/dev/null 2>&1; then
return 0
fi
pids="$(lsof -ti :"$port" 2>/dev/null || true)"
if [[ -n "$pids" ]]; then
echo "[系统] 端口 $port 已被占用,正在停止旧进程..."
kill $pids 2>/dev/null || true
sleep 1
pids="$(lsof -ti :"$port" 2>/dev/null || true)"
if [[ -n "$pids" ]]; then
kill -9 $pids 2>/dev/null || true
fi
fi
}
cleanup() {
local exit_code=$?
trap - SIGINT SIGTERM EXIT
echo
echo "[系统] 正在关闭所有服务..."
if [[ -n "$FRONTEND_PID" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
kill "$FRONTEND_PID" 2>/dev/null || true
fi
if [[ -n "$BACKEND_PID" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then
kill "$BACKEND_PID" 2>/dev/null || true
fi
wait "$FRONTEND_PID" 2>/dev/null || true
wait "$BACKEND_PID" 2>/dev/null || true
exit "$exit_code"
}
# 捕获退出信号
trap cleanup SIGINT SIGTERM EXIT
# 启动后端
require_file "$ROOT_DIR/package.json" "未找到前端 package.json"
require_file "$SERVER_DIR/main.py" "未找到后端入口 server/main.py"
require_file "$SERVER_DIR/.venv/bin/python" "未找到后端虚拟环境,请先在 server/.venv 安装依赖"
if ! command -v npm >/dev/null 2>&1; then
echo "[错误] 未找到 npm"
exit 1
fi
kill_port "$BACKEND_PORT"
kill_port "$FRONTEND_PORT"
echo "[系统] 正在启动后端服务器..."
cd /home/mt/Project/ai-chat-ui/server
cd /home/mt/Projects/ai-chat-ui/server
if [ -d ".venv" ]; then
source .venv/bin/activate
# 使用 -u 参数强制不缓冲输出,实时显示日志
python3 -u main.py &
BACKEND_PID=$!
else
echo "[错误] 未找到虚拟环境 (.venv)。请先创建。"
fi
# 等待一小段时间确保后端启动
sleep 2
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo "[错误] 后端启动失败"
exit 1
fi
# 启动前端
echo "[系统] 正在启动前端服务器..."
cd /home/mt/Project/ai-chat-ui
cd /home/mt/Projects/ai-chat-ui
# 启动 vite 开发服务器
npm run dev &
FRONTEND_PID=$!
echo "=========================================="
echo " 服务已启动,按 Ctrl+C 停止 "
echo " 服务已启动,按 Ctrl+C 停止"
echo "------------------------------------------"
echo " 前端: http://localhost:$FRONTEND_PORT"
echo " 后端: http://localhost:$BACKEND_PORT"
echo "=========================================="
# 使用 wait 阻塞主进程,保持脚本运行,这样可以看到调试打印信息
wait
wait "$BACKEND_PID" "$FRONTEND_PID"