Files
happy-export/frontend/src/auth.jsx
T
Drsakura 13d67c804f feat: 多用户权限体系、AI 助手、SKU 目录与合同清洗(开源准备)
本地长期开发成果的整体入库快照,包含后端鉴权/权限、AI 接入、合同解析、
SKU 目录等模块,以及前端相应页面与样式改版。

后端
- 新增鉴权与会话:scrypt 密码哈希、HttpOnly cookie(happy_sid)、登录失败锁定
- 新增「角色 → 权限组 → 权限点」三级权限模型与审计日志
- 新增 AI 提供商接入与预设(兼容 OpenAI 协议,内置 DeepSeek / 通义千问等)
- 新增合同解析、合同文件保留策略、客户归属与访问控制
- 新增 SKU 目录、组织品牌、新客开发、系统日志等路由

前端
- 新增登录页与用户 / 权限组 / 系统日志管理页
- 新增客户详情、客户公海、询盘详情、报价编辑器、合同清洗中心
- 新增 AI 配置、组织品牌、包装参数、SKU 选择器等组件
- 新增英文大写金额、未保存改动拦截、抽屉与骨架屏
- 新增 stage1 样式层;产品、供应商、订单等页面改版
- EmbeddedSku 页面并入现有产品体系后删除

开源准备
- 新增 AGPL-3.0 LICENSE
- README:移除未启用的 SQLCipher 加密声明(改为如实的「已知限制」)、
  补充环境变量说明、许可证由「私有项目」改为 AGPL-3.0
- 移除历史遗留的空文件 _index_backup.txt
- 文档中的项目路径更新为 happy-export
2026-09-17 14:53:02 +08:00

104 lines
3.1 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import axios from 'axios'
/**
* 会话上下文:全局唯一的「当前用户是谁」答案来源。
*
* 为什么不把用户信息塞进 localStorage:
* 改了个人资料 / 换了角色之后,前端必须立刻拿到新值。所以只依赖后端的会话 cookie,
* 启动时打一次 /api/auth/me,之后由各处的 setUser / refresh 回写。
*/
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
/* 启动时确定登录态 */
useEffect(() => {
let alive = true
;(async () => {
try {
const { data } = await axios.get('/api/auth/me')
if (alive) setUser(data?.user || null)
} catch {
if (alive) setUser(null)
} finally {
if (alive) setLoading(false)
}
})()
return () => { alive = false }
}, [])
/* 任何接口返回 401(会话过期 / 被踢),就地清空登录态,界面自然回登录页 */
useEffect(() => {
const id = axios.interceptors.response.use(
response => response,
error => {
const status = error?.response?.status
const url = String(error?.config?.url || '')
const isAuthProbe = url.includes('/api/auth/login') || url.includes('/api/auth/me')
if (status === 401 && !isAuthProbe) setUser(null)
return Promise.reject(error)
}
)
return () => axios.interceptors.response.eject(id)
}, [])
const refresh = useCallback(async () => {
try {
const { data } = await axios.get('/api/auth/me')
setUser(data?.user || null)
return data?.user || null
} catch {
setUser(null)
return null
}
}, [])
const login = useCallback(async (username, password) => {
const { data } = await axios.post('/api/auth/login', { username, password })
setUser(data.user)
return data.user
}, [])
const logout = useCallback(async () => {
try {
await axios.post('/api/auth/logout')
} catch { /* 会话可能已过期,忽略 */ }
setUser(null)
}, [])
const value = useMemo(
() => ({ user, loading, login, logout, refresh, setUser }),
[user, loading, login, logout, refresh]
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth 必须在 AuthProvider 内使用')
return context
}
/** 权限判断:系统管理员兜底全通过,其余按角色所属权限组的权限点判断 */
export function hasPermission(user, key) {
if (!user) return false
if (user.role === 'admin') return true
return Array.isArray(user.permissions) && user.permissions.includes(key)
}
/** 角色英文值 → 中文标签 */
export const ROLE_LABELS = {
admin: '系统管理员',
chairman: '董事长',
general_manager: '总经理',
sales_manager: '销售经理',
sales_rep: '销售业务员'
}
export function roleLabel(role) {
return ROLE_LABELS[role] || role || '—'
}