Browse Source

Refactor authentication context to improve token management and integrate js-sha256 for password hashing. Update login page to clear default credentials and enhance user prompts. Modify .gitignore to streamline ignored files and update package.json to include new dependency.

0es 4 months ago
parent
commit
87b0f88e19
9 changed files with 482 additions and 52 deletions
  1. 5 0
      .env.example
  2. 5 9
      .gitignore
  3. 1 0
      package.json
  4. 2 2
      src/app/(auth)/login/page.tsx
  5. 105 41
      src/contexts/AuthContext.tsx
  6. 243 0
      src/lib/request.ts
  7. 44 0
      src/services/auth.ts
  8. 72 0
      src/types/api.ts
  9. 5 0
      yarn.lock

+ 5 - 0
.env.example

@@ -0,0 +1,5 @@
+# API Configuration
+NEXT_PUBLIC_API_BASE_URL=http://47.76.166.38:26001
+
+# Environment
+NEXT_PUBLIC_ENV=development

+ 5 - 9
.gitignore

@@ -3,12 +3,8 @@
 # dependencies
 /node_modules
 /.pnp
-.pnp.*
-.yarn/*
-!.yarn/patches
-!.yarn/plugins
-!.yarn/releases
-!.yarn/versions
+.pnp.js
+.yarn/install-state.gz
 
 # testing
 /coverage
@@ -28,10 +24,10 @@
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
-.pnpm-debug.log*
 
-# env files (can opt-in for committing if needed)
-.env*
+# local env files
+.env*.local
+.env.local
 
 # vercel
 .vercel

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "@ant-design/nextjs-registry": "^1.2.0",
     "@ant-design/v5-patch-for-react-19": "^1.0.3",
     "antd": "^5.28.0",
+    "js-sha256": "^0.11.1",
     "next": "16.0.1",
     "react": "19.2.0",
     "react-dom": "19.2.0"

+ 2 - 2
src/app/(auth)/login/page.tsx

@@ -57,7 +57,7 @@ export default function LoginPage() {
 
         <Form
           name="login"
-          initialValues={{ username: "admin", password: "admin123" }}
+          initialValues={{ username: "", password: "" }}
           onFinish={onFinish}
           size="large"
           layout="vertical"
@@ -106,7 +106,7 @@ export default function LoginPage() {
 
         <div style={{ textAlign: "center", marginTop: "16px" }}>
           <Text type="secondary" style={{ fontSize: "12px" }}>
-            默认账号: admin / 123456
+            请使用您的账号密码登录
           </Text>
         </div>
       </Card>

+ 105 - 41
src/contexts/AuthContext.tsx

@@ -1,16 +1,16 @@
 "use client";
 
 import { message } from "antd";
+import { sha256 } from "js-sha256";
 import { useRouter } from "next/navigation";
 import type React from "react";
 import { createContext, useContext, useEffect, useState } from "react";
-
-interface User {
-  id: string;
-  username: string;
-  email: string;
-  role: string;
-}
+import {
+  getUserInfo,
+  login as loginApi,
+  logout as logoutApi,
+} from "@/services/auth";
+import type { User } from "@/types/api";
 
 interface AuthContextType {
   user: User | null;
@@ -22,6 +22,41 @@ interface AuthContextType {
 
 const AuthContext = createContext<AuthContextType | undefined>(undefined);
 
+// In-memory token cache to avoid repeated localStorage reads
+let tokenCache: string | null = null;
+
+/**
+ * Get token from memory cache
+ * This is much faster than reading from localStorage on every request
+ */
+export function getToken(): string | null {
+  return tokenCache;
+}
+
+/**
+ * Set token in both memory cache and localStorage
+ */
+export function setToken(token: string | null): void {
+  tokenCache = token;
+  if (typeof window !== "undefined") {
+    if (token) {
+      localStorage.setItem("token", token);
+    } else {
+      localStorage.removeItem("token");
+    }
+  }
+}
+
+/**
+ * Clear token from both memory cache and localStorage
+ */
+export function clearToken(): void {
+  tokenCache = null;
+  if (typeof window !== "undefined") {
+    localStorage.removeItem("token");
+  }
+}
+
 export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
   children,
 }) => {
@@ -32,15 +67,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
   // Check if user is logged in on mount
   useEffect(() => {
     const storedUser = localStorage.getItem("user");
-    const token = localStorage.getItem("token");
+    const storedToken = localStorage.getItem("token");
 
-    if (storedUser && token) {
+    if (storedUser && storedToken) {
       try {
         setUser(JSON.parse(storedUser));
+        // Initialize token cache from localStorage on mount (only read once)
+        setToken(storedToken);
       } catch (error) {
         console.error("Failed to parse user data:", error);
         localStorage.removeItem("user");
-        localStorage.removeItem("token");
+        clearToken();
       }
     }
     setIsLoading(false);
@@ -51,45 +88,72 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
     password: string,
   ): Promise<boolean> => {
     try {
-      // TODO: Replace with actual API call
-      // Simulating API call with setTimeout
-      await new Promise((resolve) => setTimeout(resolve, 1000));
-
-      // Mock authentication logic
-      if (username === "admin" && password === "admin123") {
-        const mockUser: User = {
-          id: "1",
-          username: "admin",
-          email: "admin@lanu.com",
-          role: "admin",
-        };
-
-        const mockToken = "mock-jwt-token-" + Date.now();
-
-        setUser(mockUser);
-        localStorage.setItem("user", JSON.stringify(mockUser));
-        localStorage.setItem("token", mockToken);
-
-        message.success("登录成功!");
-        router.push("/home");
-        return true;
-      } else {
-        message.error("用户名或密码错误!");
+      // Call actual login API
+      const loginResponse = await loginApi({
+        username,
+        password: sha256(password),
+        imageCode: "",
+      });
+
+      const { token, useMfa, mfaAuthToken } = loginResponse;
+
+      // Store token in both memory cache and localStorage
+      setToken(token);
+
+      // If MFA is required, handle MFA flow
+      if (useMfa) {
+        // TODO: Implement MFA flow
+        localStorage.setItem("mfaAuthToken", mfaAuthToken);
+        message.warning("需要进行 MFA 认证");
         return false;
       }
+
+      // Get user info after successful login
+      const userInfo = await getUserInfo();
+      const userData: User = {
+        uid: userInfo.uid,
+        username: userInfo.username,
+        nickname: userInfo.nickname,
+        avatar: userInfo.avatar,
+        superAdmin: userInfo.superAdmin,
+        useMfa: userInfo.useMfa,
+        locationInfo: userInfo.locationInfo,
+        ip: userInfo.ip,
+        permissions: userInfo.permissions,
+        token: userInfo.token,
+      };
+
+      // Store user info
+      setUser(userData);
+      localStorage.setItem("user", JSON.stringify(userData));
+
+      message.success("登录成功!");
+      router.push("/home");
+      return true;
     } catch (error) {
       console.error("Login error:", error);
-      message.error("登录失败,请稍后再试!");
+      const errorMessage =
+        error instanceof Error ? error.message : "登录失败,请稍后再试!";
+      message.error(errorMessage);
       return false;
     }
   };
 
-  const logout = () => {
-    setUser(null);
-    localStorage.removeItem("user");
-    localStorage.removeItem("token");
-    message.success("已退出登录");
-    router.push("/login");
+  const logout = async () => {
+    try {
+      // Call logout API
+      await logoutApi();
+    } catch (error) {
+      console.error("Logout error:", error);
+      // Continue with local logout even if API call fails
+    } finally {
+      // Clear local state
+      setUser(null);
+      localStorage.removeItem("user");
+      clearToken();
+      message.success("已退出登录");
+      router.push("/login");
+    }
   };
 
   const value: AuthContextType = {

+ 243 - 0
src/lib/request.ts

@@ -0,0 +1,243 @@
+/**
+ * Unified API request utility
+ * Handles all HTTP requests with interceptors for authentication and error handling
+ */
+
+import { getToken } from "@/contexts/AuthContext";
+
+// API response structure
+export interface ApiResponse<T = unknown> {
+  success: boolean;
+  code: string; // API returns string type code
+  message: string;
+  data: T;
+}
+
+// Request configuration options
+export interface RequestOptions extends RequestInit {
+  params?: Record<string, unknown>;
+  skipAuth?: boolean;
+  timeout?: number;
+  returnFullResponse?: boolean; // Option to return full response instead of just data
+}
+
+// API base URL from environment variable
+const API_BASE_URL =
+  process.env.NEXT_PUBLIC_API_BASE_URL || "http://47.76.166.38:26001";
+
+/**
+ * Build URL with query parameters
+ */
+function buildUrl(url: string, params?: Record<string, unknown>): string {
+  const fullUrl = url.startsWith("http") ? url : `${API_BASE_URL}${url}`;
+
+  if (!params || Object.keys(params).length === 0) {
+    return fullUrl;
+  }
+
+  const searchParams = new URLSearchParams();
+  for (const [key, value] of Object.entries(params)) {
+    if (value !== undefined && value !== null) {
+      searchParams.append(key, String(value));
+    }
+  }
+
+  return `${fullUrl}?${searchParams.toString()}`;
+}
+
+/**
+ * Request timeout wrapper
+ */
+function fetchWithTimeout(
+  url: string,
+  options: RequestInit,
+  timeout: number = 30000
+): Promise<Response> {
+  return Promise.race([
+    fetch(url, options),
+    new Promise<Response>((_, reject) =>
+      setTimeout(() => reject(new Error("Request timeout")), timeout)
+    ),
+  ]);
+}
+
+/**
+ * Unified request function
+ */
+async function request<T = unknown>(
+  url: string,
+  options: RequestOptions = {}
+): Promise<T> {
+  const {
+    params,
+    skipAuth = false,
+    timeout = 30000,
+    returnFullResponse = false,
+    headers = {},
+    ...restOptions
+  } = options;
+
+  // Build full URL with query parameters
+  const fullUrl = buildUrl(url, params);
+
+  // Prepare headers
+  const requestHeaders: Record<string, string> = {
+    "Content-Type": "application/json",
+  };
+
+  // Add custom headers
+  // X-Language: Language code from browser
+  const language =
+    typeof window !== "undefined" && navigator.language
+      ? navigator.language
+      : "en";
+  requestHeaders["X-Language"] = language;
+
+  // X-Client-Version: Client version number
+  const clientVersion = process.env.NEXT_PUBLIC_CLIENT_VERSION || "1.0.0";
+  requestHeaders["X-Client-Version"] = clientVersion;
+
+  // X-Client-Platform: Client Platform
+  const clientPlatform = "web";
+  requestHeaders["X-Client-Platform"] = clientPlatform;
+
+  // Merge provided headers
+  if (headers) {
+    Object.entries(headers).forEach(([key, value]) => {
+      if (typeof value === "string") {
+        requestHeaders[key] = value;
+      }
+    });
+  }
+
+  // Add authentication token if not skipped
+  if (!skipAuth) {
+    const token = getToken();
+    if (token) {
+      requestHeaders["X-Auth-Token"] = token;
+    }
+  }
+
+  // Prepare fetch options
+  const fetchOptions: RequestInit = {
+    ...restOptions,
+    headers: requestHeaders,
+  };
+
+  try {
+    // Make request with timeout
+    const response = await fetchWithTimeout(fullUrl, fetchOptions, timeout);
+
+    // Parse response
+    const result: ApiResponse<T> = await response.json();
+
+    // Handle HTTP errors
+    if (!response.ok) {
+      throw new Error(result.message || `HTTP Error: ${response.status}`);
+    }
+
+    // Handle API business errors (code "1" means success)
+    if (result.code !== "1" && !result.success) {
+      throw new Error(result.message || "API Error");
+    }
+
+    // Return full response or just data based on option
+    return (returnFullResponse ? result : result.data) as T;
+  } catch (error) {
+    // Handle network errors
+    if (error instanceof Error) {
+      throw error;
+    }
+    throw new Error("Network error");
+  }
+}
+
+/**
+ * GET request
+ */
+export async function get<T = unknown>(
+  url: string,
+  params?: Record<string, unknown>,
+  options?: RequestOptions
+): Promise<T> {
+  return request<T>(url, {
+    method: "GET",
+    params,
+    ...options,
+  });
+}
+
+/**
+ * POST request
+ */
+export async function post<T = unknown>(
+  url: string,
+  data?: unknown,
+  options?: RequestOptions
+): Promise<T> {
+  return request<T>(url, {
+    method: "POST",
+    body: JSON.stringify(data),
+    ...options,
+  });
+}
+
+/**
+ * PUT request
+ */
+export async function put<T = unknown>(
+  url: string,
+  data?: unknown,
+  options?: RequestOptions
+): Promise<T> {
+  return request<T>(url, {
+    method: "PUT",
+    body: JSON.stringify(data),
+    ...options,
+  });
+}
+
+/**
+ * DELETE request
+ */
+export async function del<T = unknown>(
+  url: string,
+  options?: RequestOptions
+): Promise<T> {
+  return request<T>(url, {
+    method: "DELETE",
+    ...options,
+  });
+}
+
+/**
+ * PATCH request
+ */
+export async function patch<T = unknown>(
+  url: string,
+  data?: unknown,
+  options?: RequestOptions
+): Promise<T> {
+  return request<T>(url, {
+    method: "PATCH",
+    body: JSON.stringify(data),
+    ...options,
+  });
+}
+
+/**
+ * Get full API response (including code, message, success, data)
+ * Use this when you need to access the complete response structure
+ */
+export async function getFullResponse<T = unknown>(
+  url: string,
+  options: RequestOptions = {}
+): Promise<ApiResponse<T>> {
+  return request<ApiResponse<T>>(url, {
+    ...options,
+    returnFullResponse: true,
+  });
+}
+
+// Export default request function
+export default request;

+ 44 - 0
src/services/auth.ts

@@ -0,0 +1,44 @@
+/**
+ * Authentication API service
+ */
+
+import { get, post } from "@/lib/request";
+import type {
+  LoginRequest,
+  LoginResponse,
+  UserInfoResponse,
+} from "@/types/api";
+
+/**
+ * Login API
+ * @param data - login credentials
+ * @returns login response with token and user info
+ */
+export async function login(data: LoginRequest) {
+  return post<LoginResponse>("/ffx/auth/submit", data, {
+    skipAuth: true, // Skip auth for login endpoint
+  });
+}
+
+/**
+ * Logout API
+ */
+export async function logout() {
+  return get("/ffx/auth/logout");
+}
+
+/**
+ * Get current user info
+ */
+export async function getUserInfo() {
+  return get<UserInfoResponse>("/ffx/auth/info");
+}
+
+/**
+ * Refresh token
+ */
+export async function refreshToken(refreshToken: string) {
+  return post<{ token: string }>("/ffx/auth/refresh", {
+    refreshToken,
+  });
+}

+ 72 - 0
src/types/api.ts

@@ -0,0 +1,72 @@
+/**
+ * API type definitions
+ */
+
+// User information
+export interface User {
+  uid: string;
+  username: string;
+  nickname: string;
+  avatar: string;
+  superAdmin: boolean;
+  useMfa: boolean;
+  locationInfo: string;
+  ip: string;
+  permissions: string[];
+  token?: string;
+}
+
+// Login request (FFXAuthReq)
+export interface LoginRequest {
+  username: string;
+  password: string;
+  imageCode?: string;
+}
+
+// Login response (FFXAuthResp)
+export interface LoginResponse {
+  token: string;
+  mfaAuthToken: string;
+  useMfa: boolean;
+}
+
+// User info response (FFXAuthInfoResp)
+export interface UserInfoResponse {
+  uid: string;
+  token: string;
+  avatar: string;
+  username: string;
+  nickname: string;
+  superAdmin: boolean;
+  useMfa: boolean;
+  locationInfo: string;
+  ip: string;
+  permissions: string[];
+}
+
+// Pagination parameters
+export interface PaginationParams {
+  page?: number;
+  pageSize?: number;
+  current?: number;
+  size?: number;
+}
+
+// Pagination response
+export interface PaginationResponse<T> {
+  list: T[];
+  total: number;
+  page?: number;
+  pageSize?: number;
+  current?: number;
+  size?: number;
+}
+
+// Common list response
+export interface ListResponse<T> {
+  records: T[];
+  total: number;
+  size: number;
+  current: number;
+  pages?: number;
+}

+ 5 - 0
yarn.lock

@@ -769,6 +769,11 @@ jiti@^2.6.1:
   resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92"
   integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
 
+js-sha256@^0.11.1:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.1.tgz#712262e8fc9569d6f7f6eea72c0d8e5ccc7c976c"
+  integrity sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==
+
 json2mq@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a"