auth.ts 10.1 KB
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import {
  apiLogin,
  apiRegister,
  apiForgotPassword,
  apiResetPassword,
  apiResendActivation,
  apiProfile,
  apiVerify,
  apiRefreshToken,
  apiLogout,
} from "@/api/user";
import { service as request } from "@/utils/request.ts";

const TOKEN_KEY = "auth_token";
const REFRESH_TOKEN_KEY = "refresh_token";
const LOGOUT_FLAG_KEY = "user_logged_out";

export const useAuthStore = defineStore("auth", () => {
  // State
  const token = ref(
    (() => {
      if (localStorage.getItem(LOGOUT_FLAG_KEY)) {
        return "";
      }
      return localStorage.getItem(TOKEN_KEY) || "";
    })(),
  );

  const refreshToken = ref(
    (() => {
      if (localStorage.getItem(LOGOUT_FLAG_KEY)) {
        return "";
      }
      return localStorage.getItem(REFRESH_TOKEN_KEY) || "";
    })(),
  );

  const user = ref<any>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);
  const isLoggingOut = ref(false);
  let profileFetchPromise: Promise<any> | null = null; // 用于防止重复请求

  // Getters
  const isAuthenticated = computed(() => !!token.value || !!refreshToken.value);
  const hasRefreshToken = computed(() => !!refreshToken.value);
  const me = computed(() => user.value);

  // Actions
  const clearError = () => {
    error.value = null;
  };

  const login = async (payload: {
    email: string;
    password: string;
    remember?: boolean;
  }) => {
    loading.value = true;
    error.value = null;
    try {
      const response = await apiLogin(payload);
      
      // 适配不同的响应格式
      // 格式1: { data: { token: ... } } (标准格式)
      // 格式2: { token: ... } (直接返回)
      // 格式3: response.data.data.token (嵌套格式)
      let responseData = response.data;
      
      // 如果 response.data 有 data 属性,可能是嵌套格式
      if (responseData && typeof responseData === 'object' && 'data' in responseData && responseData.data) {
        responseData = responseData.data;
      }
      
      const authToken = responseData?.token || responseData?.accessToken || responseData?.access_token;
      if (!authToken) {
        throw new Error("No token in response");
      }

      localStorage.removeItem(LOGOUT_FLAG_KEY);
      localStorage.setItem(TOKEN_KEY, authToken);
      if (request.defaults && request.defaults.headers) {
        request.defaults.headers.common["Authorization"] =
          `Bearer ${authToken}`;
      }
      token.value = authToken;

      // 如果有刷新令牌,保存它(适配不同的字段名)
      const refreshTokenValue = responseData.refreshToken || responseData.refresh_token;
      if (refreshTokenValue) {
        localStorage.setItem(REFRESH_TOKEN_KEY, refreshTokenValue);
        refreshToken.value = refreshTokenValue;
      }

      // 适配不同的用户信息字段
      user.value = {
        id: responseData.userId || responseData.user_id || responseData.id,
        email: responseData.email,
        role: responseData.role || responseData.roleName,
      };
      
      return { success: true };
    } catch (e: any) {
      const msg = e?.response?.data?.message || e?.response?.data?.error || e.message || "Login failed";
      error.value = msg;
      return { success: false, message: msg };
    } finally {
      loading.value = false;
    }
  };

  const loginWithToken = async (tokenValue: string) => {
    if (!tokenValue) return { success: false, message: "empty token" };
    localStorage.removeItem(LOGOUT_FLAG_KEY);
    localStorage.setItem(TOKEN_KEY, tokenValue);
    if (request.defaults && request.defaults.headers) {
      request.defaults.headers.common["Authorization"] = `Bearer ${tokenValue}`;
    }
    token.value = tokenValue;
    try {
      const { data } = await apiProfile();
      user.value = data || null;
    } catch {
      // 忽略 profile 失败
    }
    return { success: true };
  };

  // 用于邮箱激活
  const verifyAndLogin = async (tokenFromMail: string) => {
    loading.value = true;
    error.value = null;
    try {
      const { data } = await apiVerify(tokenFromMail);
      const authToken = data?.token;
      if (!authToken) throw new Error("No token in verify response");

      localStorage.setItem(TOKEN_KEY, authToken);
      localStorage.removeItem(LOGOUT_FLAG_KEY);
      if (request.defaults && request.defaults.headers) {
        request.defaults.headers.common["Authorization"] =
          `Bearer ${authToken}`;
      }
      token.value = authToken;

      user.value = {
        id: data.userId,
        email: data.email,
        role: data.role,
      };
      return { success: true };
    } catch (e: any) {
      const msg = e?.response?.data?.message || e.message || "Verify failed";
      error.value = msg;
      return { success: false, message: msg };
    } finally {
      loading.value = false;
    }
  };

  const register = async (payload: any) => {
    loading.value = true;
    error.value = null;
    try {
      await apiRegister(payload);
      return { success: true };
    } catch (e: any) {
      const msg = e?.response?.data?.message || e.message || "Register failed";
      error.value = msg;
      return { success: false, message: msg };
    } finally {
      loading.value = false;
    }
  };

  const sendResetCode = async (payload: { email: string }) => {
    loading.value = true;
    error.value = null;
    try {
      await apiForgotPassword(payload);
      return { success: true };
    } catch (e: any) {
      const msg =
        e?.response?.data?.message || e.message || "Forgot password failed";
      error.value = msg;
      return { success: false, message: msg };
    } finally {
      loading.value = false;
    }
  };

  const resetPassword = async (payload: {
    token: string;
    newPassword: string;
  }) => {
    loading.value = true;
    error.value = null;
    try {
      await apiResetPassword(payload);
      return { success: true };
    } catch (e: any) {
      const msg =
        e?.response?.data?.message || e.message || "Reset password failed";
      error.value = msg;
      return { success: false, message: msg };
    } finally {
      loading.value = false;
    }
  };

  const resendActivation = async (email: string) => {
    loading.value = true;
    error.value = null;
    try {
      await apiResendActivation(email);
      return { success: true };
    } catch (e: any) {
      const msg =
        e?.response?.data?.message || e.message || "Resend activation failed";
      error.value = msg;
      return { success: false, message: msg };
    } finally {
      loading.value = false;
    }
  };

  const fetchProfile = async () => {
    // 如果已经有用户信息,直接返回
    if (user.value) {
      return { success: true, data: user.value };
    }

    // 如果正在请求中,等待现有请求完成
    if (profileFetchPromise) {
      return profileFetchPromise;
    }

    // 创建新的请求 Promise
    profileFetchPromise = (async () => {
      loading.value = true;
      error.value = null;
      try {
        const { data } = await apiProfile();
        user.value = data || null;

        return { success: true, data };
      } catch (e: any) {
        const msg =
          e?.response?.data?.message || e.message || "Fetch profile failed";
        error.value = msg;
        return { success: false, message: msg };
      } finally {
        loading.value = false;
        profileFetchPromise = null; // 清除 Promise 缓存
      }
    })();

    return profileFetchPromise;
  };

  const refreshAccessToken = async () => {
    if (!refreshToken.value) {
      return { success: false, message: "No refresh token" };
    }
    try {
      const { data } = await apiRefreshToken(refreshToken.value);
      const newToken = data?.token;
      const newRefreshToken = data?.refreshToken;

      if (!newToken) throw new Error("No token in refresh response");

      // 更新令牌
      localStorage.setItem(TOKEN_KEY, newToken);
      if (request.defaults && request.defaults.headers) {
        request.defaults.headers.common["Authorization"] = `Bearer ${newToken}`;
      }
      token.value = newToken;

      // 更新刷新令牌
      if (newRefreshToken) {
        localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken);
        refreshToken.value = newRefreshToken;
      }

      // 更新用户信息
      if (data.userId && data.email && data.role) {
        user.value = {
          id: data.userId,
          email: data.email,
          role: data.role,
        };
      }

      return { success: true };
    } catch (e: any) {
      const msg =
        e?.response?.data?.message || e.message || "Token refresh failed";
      // 刷新失败,清除所有令牌
      logout();
      return { success: false, message: msg };
    }
  };

  const logout = () => {
    // 设置登出状态,防止路由守卫冲突
    isLoggingOut.value = true;

    // 先设置登出标记,防止路由守卫冲突
    localStorage.setItem(LOGOUT_FLAG_KEY, "true");

    // 在清理前读取旧的刷新令牌
    const oldRefreshToken =
      localStorage.getItem(REFRESH_TOKEN_KEY) || refreshToken.value;

    // 清除所有令牌和状态
    localStorage.removeItem(TOKEN_KEY);
    localStorage.removeItem(REFRESH_TOKEN_KEY);
    if (
      request.defaults &&
      request.defaults.headers &&
      request.defaults.headers.common
    ) {
      delete request.defaults.headers.common["Authorization"];
    }
    token.value = "";
    refreshToken.value = "";
    user.value = null;
    error.value = null;

    // 如果有刷新令牌,尝试通知后端清除(异步,不阻塞)
    if (oldRefreshToken) {
      apiLogout(oldRefreshToken).catch(() => {
        // 忽略登出API的错误
      });
    }

    // 延迟重置登出状态,确保路由守卫能检测到
    setTimeout(() => {
      isLoggingOut.value = false;
    }, 100);
  };

  return {
    // state
    token,
    refreshToken,
    user,
    loading,
    error,
    isLoggingOut,
    // getters
    isAuthenticated,
    hasRefreshToken,
    me,
    // actions
    clearError,
    login,
    loginWithToken,
    verifyAndLogin,
    register,
    sendResetCode,
    resetPassword,
    resendActivation,
    fetchProfile,
    refreshAccessToken,
    logout,
  };
});