fix(auth): 修复启动脚本与前端认证流程
修复 start.sh 未记录后台进程 PID 导致启动后误判失败的问题。\n调整前端认证逻辑,支持开发环境下优先读取 URL token,并使用 POST 请求直连外部认证接口解析新的返回结构。\n补充认证回归测试,覆盖请求方法、请求头和用户信息映射。
This commit is contained in:
parent
a130e0d3f5
commit
4a434f9580
54
src/__tests__/authStore.test.ts
Normal file
54
src/__tests__/authStore.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@ -16,17 +16,26 @@ const DEV_BYPASS_USER: UserInfo = {
|
||||
nickname: '开发环境用户',
|
||||
};
|
||||
|
||||
// 认证接口返回格式
|
||||
interface AuthResponse {
|
||||
code: string;
|
||||
msg: string;
|
||||
success: boolean;
|
||||
timestamp: number;
|
||||
data: UserInfo | null;
|
||||
}
|
||||
|
||||
// 认证接口
|
||||
const AUTH_CHECK_URL = '/api/auth/check/checkTokenRn';
|
||||
// 认证接口返回格式
|
||||
interface AuthResponse {
|
||||
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 = 'http://test.xueai.art/newapi/api/login/validateToken';
|
||||
const AUTH_TOKEN_STORAGE_KEY = 'DEV_DEFAULT_TOKEN';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
@ -42,21 +51,33 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
/**
|
||||
* 验证 token 并获取用户信息
|
||||
*/
|
||||
async function checkToken(tokenToCheck: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
const response = await fetch(`${AUTH_CHECK_URL}/${tokenToCheck}`);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const data: AuthResponse = await response.json();
|
||||
|
||||
|
||||
if (data.success && data.data) {
|
||||
return data.data;
|
||||
}else{
|
||||
window.$toast?.('[Auth] Token 验证失败:Token无效');
|
||||
}
|
||||
return null;
|
||||
async function checkToken(tokenToCheck: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
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.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;
|
||||
} catch (error) {
|
||||
|
||||
console.error('[Auth] Token 验证失败:', error);
|
||||
@ -68,21 +89,21 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
* 初始化 - 从 URL 参数获取 token,验证后设置用户信息
|
||||
*/
|
||||
async function init() {
|
||||
if (DEV_AUTH_BYPASS) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const urlToken = searchParams.get('token');
|
||||
|
||||
// 获取 token:URL > localStorage > 默认值
|
||||
const tokenValue = urlToken
|
||||
|| 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;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const urlToken = searchParams.get('token');
|
||||
|
||||
// 获取 token:URL > localStorage > 默认值
|
||||
const tokenValue = urlToken
|
||||
|| localStorage.getItem(AUTH_TOKEN_STORAGE_KEY)
|
||||
|| DEV_DEFAULT_TOKEN;
|
||||
|
||||
if (!tokenValue) {
|
||||
isInitialized.value = true;
|
||||
window.$toast?.('未登录,请先登录', 'error');
|
||||
@ -122,10 +143,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 初始化(不等待,让调用方通过 isInitialized 判断)
|
||||
init();
|
||||
|
||||
return {
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
user,
|
||||
|
||||
102
start.sh
102
start.sh
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user