diff --git a/src/__tests__/authStore.test.ts b/src/__tests__/authStore.test.ts new file mode 100644 index 0000000..f65428b --- /dev/null +++ b/src/__tests__/authStore.test.ts @@ -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') + }) +}) diff --git a/src/stores/auth.ts b/src/stores/auth.ts index a579212..7ce5cb7 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -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 { - 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 { + 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, diff --git a/start.sh b/start.sh index 7072e6e..d129379 100644 --- a/start.sh +++ b/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"