diff --git a/Cunkebao/app/components/AuthProvider.tsx b/Cunkebao/app/components/AuthProvider.tsx index f57f4beb..4658091d 100644 --- a/Cunkebao/app/components/AuthProvider.tsx +++ b/Cunkebao/app/components/AuthProvider.tsx @@ -2,19 +2,51 @@ import { createContext, useContext, useEffect, useState, type ReactNode } from "react" import { useRouter } from "next/navigation" +import { validateToken } from "@/lib/api" + +// 安全的localStorage访问方法 +const safeLocalStorage = { + getItem: (key: string): string | null => { + if (typeof window !== 'undefined') { + return localStorage.getItem(key) + } + return null + }, + setItem: (key: string, value: string): void => { + if (typeof window !== 'undefined') { + localStorage.setItem(key, value) + } + }, + removeItem: (key: string): void => { + if (typeof window !== 'undefined') { + localStorage.removeItem(key) + } + } +} + +interface User { + id: number; + username: string; + account?: string; + avatar?: string; +} interface AuthContextType { isAuthenticated: boolean token: string | null - login: (token: string) => void + user: User | null + login: (token: string, userData: User) => void logout: () => void + updateToken: (newToken: string) => void } const AuthContext = createContext({ isAuthenticated: false, token: null, + user: null, login: () => {}, logout: () => {}, + updateToken: () => {} }) export const useAuth = () => useContext(AuthContext) @@ -25,43 +57,82 @@ interface AuthProviderProps { export function AuthProvider({ children }: AuthProviderProps) { const [token, setToken] = useState(null) + const [user, setUser] = useState(null) const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) const router = useRouter() + // 检查token有效性并初始化认证状态 useEffect(() => { - // 客户端检查token - if (typeof window !== "undefined") { - const storedToken = localStorage.getItem("token") + const initAuth = async () => { + setIsLoading(true) + const storedToken = safeLocalStorage.getItem("token") + if (storedToken) { - setToken(storedToken) - setIsAuthenticated(true) - } else { - setIsAuthenticated(false) - // 暂时禁用重定向逻辑,允许访问所有页面 - // 将来需要恢复登录验证时,取消下面注释 - /* - if (pathname !== "/login") { - router.push("/login") + try { + // 验证token是否有效 + const isValid = await validateToken() + + if (isValid) { + // 从localStorage获取用户信息 + const userDataStr = safeLocalStorage.getItem("user") + if (userDataStr) { + const userData = JSON.parse(userDataStr) as User + setToken(storedToken) + setUser(userData) + setIsAuthenticated(true) + } else { + // token有效但没有用户信息,清除token + handleLogout() + } + } else { + // token无效,清除 + handleLogout() + } + } catch (error) { + console.error("验证token时出错:", error) + handleLogout() } - */ } + + setIsLoading(false) } + + initAuth() }, []) - const login = (newToken: string) => { - localStorage.setItem("token", newToken) + const handleLogout = () => { + safeLocalStorage.removeItem("token") + safeLocalStorage.removeItem("user") + setToken(null) + setUser(null) + setIsAuthenticated(false) + } + + const login = (newToken: string, userData: User) => { + safeLocalStorage.setItem("token", newToken) + safeLocalStorage.setItem("user", JSON.stringify(userData)) setToken(newToken) + setUser(userData) setIsAuthenticated(true) } const logout = () => { - localStorage.removeItem("token") - setToken(null) - setIsAuthenticated(false) + handleLogout() // 登出后不强制跳转到登录页 // router.push("/login") } - return {children} + // 用于刷新 token 的方法 + const updateToken = (newToken: string) => { + safeLocalStorage.setItem("token", newToken) + setToken(newToken) + } + + return ( + + {isLoading ?
加载中...
: children} +
+ ) } diff --git a/Cunkebao/app/login/page.tsx b/Cunkebao/app/login/page.tsx index f316c8f8..ba7f1e16 100644 --- a/Cunkebao/app/login/page.tsx +++ b/Cunkebao/app/login/page.tsx @@ -11,19 +11,10 @@ import { useRouter } from "next/navigation" import { WeChatIcon } from "@/components/icons/wechat-icon" import { AppleIcon } from "@/components/icons/apple-icon" import { useToast } from "@/components/ui/use-toast" +import { useAuth } from "@/app/components/AuthProvider" +import { loginApi } from "@/lib/api" -// 使用环境变量获取API域名 -const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com" - -// 定义登录响应类型 -interface LoginResponse { - code: number - message: string - data?: { - token: string - } -} - +// 定义登录表单类型 interface LoginForm { phone: string password: string @@ -44,6 +35,7 @@ export default function LoginPage() { const router = useRouter() const { toast } = useToast() + const { login, isAuthenticated } = useAuth() const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target @@ -101,38 +93,40 @@ export default function LoginPage() { setIsLoading(true) try { - // 创建FormData对象 - const formData = new FormData() - formData.append("phone", form.phone) - if (activeTab === "password") { - formData.append("password", form.password) + // 发送账号密码登录请求 + const response = await loginApi.login(form.phone, form.password) + + if (response.code === 200 && response.data) { + // 获取用户信息和token + const { token, token_expired, member } = response.data + + // 保存token和用户信息 + login(token, { + id: member.id, + username: member.username || member.account || '', + account: member.account, + avatar: member.avatar + }) + + // 显示成功提示 + toast({ + title: "登录成功", + description: "欢迎回来!", + }) + + // 跳转到首页 + router.push("/") + } else { + throw new Error(response.msg || "登录失败") + } } else { - formData.append("verificationCode", form.verificationCode) - } - - // 发送登录请求 - const response = await fetch(`${API_BASE_URL}/auth/login`, { - method: "POST", - body: formData, - // 不需要设置Content-Type,浏览器会自动设置为multipart/form-data并添加boundary - }) - - const result: LoginResponse = await response.json() - - if (result.code === 10000 && result.data?.token) { - // 保存token到localStorage - localStorage.setItem("token", result.data.token) - - // 成功后跳转 - router.push("/profile") - + // 验证码登录逻辑保持原样,未来可以实现 toast({ - title: "登录成功", - description: "欢迎回来!", + variant: "destructive", + title: "功能未实现", + description: "验证码登录功能尚未实现,请使用密码登录", }) - } else { - throw new Error(result.message || "登录失败") } } catch (error) { toast({ @@ -155,46 +149,19 @@ export default function LoginPage() { return } - setIsLoading(true) - try { - // 创建FormData对象 - const formData = new FormData() - formData.append("phone", form.phone) - - // 发送验证码请求 - const response = await fetch(`${API_BASE_URL}/auth/send-code`, { - method: "POST", - body: formData, - }) - - const result = await response.json() - - if (result.code === 10000) { - toast({ - title: "验证码已发送", - description: "请查看手机短信", - }) - } else { - throw new Error(result.message || "发送失败") - } - } catch (error) { - toast({ - variant: "destructive", - title: "发送失败", - description: error instanceof Error ? error.message : "请稍后重试", - }) - } finally { - setIsLoading(false) - } + toast({ + variant: "destructive", + title: "功能未实现", + description: "验证码发送功能尚未实现", + }) } useEffect(() => { - // 检查是否已登录 - const token = localStorage.getItem("token") - if (token) { - router.push("/profile") + // 检查是否已登录,如果已登录则跳转到首页 + if (isAuthenticated) { + router.push("/") } - }, [router]) + }, [isAuthenticated, router]) return (
@@ -261,25 +228,24 @@ export default function LoginPage() { -
+
- + 获取验证码 +
@@ -288,65 +254,35 @@ export default function LoginPage() { id="terms" checked={form.agreeToTerms} onCheckedChange={handleCheckboxChange} - className="border-gray-300 data-[state=checked]:bg-blue-500" disabled={isLoading} /> -
- -
-
- -
-
- -
+
+
+ 其他登录方式 +
-
- - +
+ +
- -
diff --git a/Cunkebao/lib/api.ts b/Cunkebao/lib/api.ts new file mode 100644 index 00000000..e5c7c605 --- /dev/null +++ b/Cunkebao/lib/api.ts @@ -0,0 +1,129 @@ +import { handleApiResponse, handleApiError } from "./http-interceptors" + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com'; + +// 安全地获取token +const getToken = (): string | null => { + if (typeof window !== 'undefined') { + return localStorage.getItem('token'); + } + return null; +}; + +// 安全地设置token +const setToken = (token: string): void => { + if (typeof window !== 'undefined') { + localStorage.setItem('token', token); + } +}; + +// 创建请求头 +const createHeaders = (withAuth: boolean = true): HeadersInit => { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (withAuth) { + const token = getToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + } + + return headers; +}; + +// 基础请求函数 +export const request = async ( + url: string, + method: string = 'GET', + data?: any, + withAuth: boolean = true +): Promise => { + const options: RequestInit = { + method, + headers: createHeaders(withAuth), + }; + + if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + options.body = JSON.stringify(data); + } + + try { + const response = await fetch(`${API_BASE_URL}${url}`, options); + const result = await response.json(); + + // 使用响应拦截器处理响应 + return handleApiResponse(response, result); + } catch (error) { + // 使用错误拦截器处理错误 + return handleApiError(error); + } +}; + +// 导出便捷的请求方法 +export const api = { + get: (url: string, withAuth: boolean = true) => + request(url, 'GET', undefined, withAuth), + + post: (url: string, data: any, withAuth: boolean = true) => + request(url, 'POST', data, withAuth), + + put: (url: string, data: any, withAuth: boolean = true) => + request(url, 'PUT', data, withAuth), + + delete: (url: string, withAuth: boolean = true) => + request(url, 'DELETE', undefined, withAuth), +}; + +// 登录API +export const loginApi = { + // 账号密码登录 + login: (account: string, password: string) => + api.post('/v1/auth/login', { + account, + password, + typeId: 1 // 默认使用用户类型1 + }, false), + + // 获取用户信息 + getUserInfo: () => + api.get('/v1/auth/info'), + + // 刷新Token + refreshToken: () => + api.post('/v1/auth/refresh', {}), +}; + +// 验证 Token 是否有效 +export const validateToken = async (): Promise => { + try { + const response = await loginApi.getUserInfo(); + return response.code === 200; + } catch (error) { + return false; + } +}; + +// 刷新令牌 +export const refreshAuthToken = async (): Promise => { + try { + const response = await loginApi.refreshToken(); + if (response.code === 200 && response.data?.token) { + // 更新本地存储的token + setToken(response.data.token); + return true; + } + return false; + } catch (error) { + console.error('刷新Token失败:', error); + return false; + } +}; + +// 定义响应类型接口 +export interface ApiResponse { + code: number; + msg: string; + data?: any; +} \ No newline at end of file diff --git a/Cunkebao/lib/http-interceptors.ts b/Cunkebao/lib/http-interceptors.ts new file mode 100644 index 00000000..02ce83d8 --- /dev/null +++ b/Cunkebao/lib/http-interceptors.ts @@ -0,0 +1,39 @@ +import { useRouter } from "next/navigation"; + +// Token过期处理 +export const handleTokenExpired = () => { + if (typeof window !== 'undefined') { + // 清除本地存储 + localStorage.removeItem('token'); + localStorage.removeItem('user'); + + // 跳转到登录页 + window.location.href = '/login'; + } +}; + +// 响应拦截器 +export const handleApiResponse = (response: Response, result: any): T => { + // 处理token过期情况 + if (result && (result.code === 401 || result.msg?.includes('token'))) { + handleTokenExpired(); + throw new Error(result.msg || '登录已过期,请重新登录'); + } + + return result; +}; + +// 处理API错误 +export const handleApiError = (error: unknown): never => { + console.error('API请求错误:', error); + + if (error instanceof Error) { + // 如果是未授权错误,可能是token过期 + if (error.message.includes('401') || error.message.includes('token') || error.message.includes('授权')) { + handleTokenExpired(); + } + throw error; + } + + throw new Error('未知错误,请稍后重试'); +}; \ No newline at end of file