diff --git a/internal/server/middleware/jwt.go b/internal/server/middleware/jwt.go index c00b40a..8625298 100644 --- a/internal/server/middleware/jwt.go +++ b/internal/server/middleware/jwt.go @@ -2,11 +2,13 @@ package middleware import ( "context" + "encoding/json" "errors" "strconv" "strings" "time" + "github.com/go-kratos/kratos/v2/metadata" "github.com/go-kratos/kratos/v2/middleware" "github.com/go-kratos/kratos/v2/transport" "github.com/go-kratos/kratos/v2/transport/http" @@ -14,7 +16,14 @@ import ( pkgjwt "kra/pkg/jwt" ) -type authKey struct{} +const ( + // metadata keys for claims + mdKeyUserID = "x-md-user-id" + mdKeyUsername = "x-md-username" + mdKeyAuthorityID = "x-md-authority-id" + mdKeyUUID = "x-md-uuid" + mdKeyClaims = "x-md-claims" +) var ( ErrMissingToken = errors.New("未登录或非法访问") @@ -65,8 +74,15 @@ func JWTAuth(cfg JWTAuthConfig) middleware.Middleware { return nil, ErrInvalidToken } - // 将claims存入context - ctx = context.WithValue(ctx, authKey{}, claims) + // 将claims存入metadata + claimsJSON, _ := json.Marshal(claims) + ctx = metadata.AppendToClientContext(ctx, + mdKeyUserID, strconv.FormatUint(uint64(claims.BaseClaims.ID), 10), + mdKeyUsername, claims.Username, + mdKeyAuthorityID, strconv.FormatUint(uint64(claims.AuthorityID), 10), + mdKeyUUID, claims.UUID, + mdKeyClaims, string(claimsJSON), + ) // 检查是否需要刷新token if cfg.JWT.NeedRefresh(claims) { @@ -127,30 +143,46 @@ func setToken(ht http.Transporter, token string, maxAge int) { // GetClaims 从context获取claims func GetClaims(ctx context.Context) (*pkgjwt.CustomClaims, bool) { - claims, ok := ctx.Value(authKey{}).(*pkgjwt.CustomClaims) - return claims, ok + if md, ok := metadata.FromServerContext(ctx); ok { + claimsStr := md.Get(mdKeyClaims) + if claimsStr != "" { + var claims pkgjwt.CustomClaims + if err := json.Unmarshal([]byte(claimsStr), &claims); err == nil { + return &claims, true + } + } + } + return nil, false } // GetUserID 从context获取用户ID func GetUserID(ctx context.Context) uint { - if claims, ok := GetClaims(ctx); ok { - return claims.BaseClaims.ID + if md, ok := metadata.FromServerContext(ctx); ok { + if idStr := md.Get(mdKeyUserID); idStr != "" { + if id, err := strconv.ParseUint(idStr, 10, 64); err == nil { + return uint(id) + } + } } return 0 } // GetUsername 从context获取用户名 func GetUsername(ctx context.Context) string { - if claims, ok := GetClaims(ctx); ok { - return claims.Username + if md, ok := metadata.FromServerContext(ctx); ok { + return md.Get(mdKeyUsername) } return "" } // GetAuthorityID 从context获取角色ID func GetAuthorityID(ctx context.Context) uint { - if claims, ok := GetClaims(ctx); ok { - return claims.AuthorityID + if md, ok := metadata.FromServerContext(ctx); ok { + if idStr := md.Get(mdKeyAuthorityID); idStr != "" { + if id, err := strconv.ParseUint(idStr, 10, 64); err == nil { + return uint(id) + } + } } return 0 } diff --git a/web/CODE_OF_CONDUCT.md b/web/CODE_OF_CONDUCT.md deleted file mode 100644 index 2b4571c..0000000 --- a/web/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/web/config/config.ts b/web/config/config.ts index 40e29df..417cfa6 100644 --- a/web/config/config.ts +++ b/web/config/config.ts @@ -24,6 +24,12 @@ export default defineConfig({ */ hash: true, + /** + * @name esbuild minify IIFE + * @description 解决 esbuild helpers 冲突问题 + */ + esbuildMinifyIIFE: true, + publicPath: PUBLIC_PATH, /** @@ -83,7 +89,7 @@ export default defineConfig({ * @name layout 插件 * @doc https://umijs.org/docs/max/layout-menu */ - title: "Ant Design Pro", + title: "KRA Admin", layout: { locale: true, ...defaultSettings, diff --git a/web/config/defaultSettings.ts b/web/config/defaultSettings.ts index 6710cad..b079a83 100644 --- a/web/config/defaultSettings.ts +++ b/web/config/defaultSettings.ts @@ -1,27 +1,47 @@ import type { ProLayoutProps } from '@ant-design/pro-components'; /** - * @name + * @name KRA 后台管理系统布局配置 + * @description 参考 GVA 的布局配置,适配 Ant Design Pro */ const Settings: ProLayoutProps & { pwa?: boolean; logo?: string; + showWatermark?: boolean; } = { navTheme: 'light', - // 拂晓蓝 - colorPrimary: '#1890ff', + // 主题色 - 与 GVA 保持一致的蓝色 + colorPrimary: '#3b82f6', + // 布局模式: side | top | mix layout: 'mix', contentWidth: 'Fluid', - fixedHeader: false, + // 固定头部 + fixedHeader: true, + // 固定侧边栏 fixSiderbar: true, + // 色弱模式 colorWeak: false, - title: 'Ant Design Pro', + // 应用标题 + title: 'KRA Admin', pwa: true, - logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', + // Logo + logo: '/logo.svg', iconfontUrl: '', + // 显示水印 + showWatermark: true, + // 侧边栏宽度配置 + siderWidth: 256, token: { - // 参见ts声明,demo 见文档,通过token 修改样式 - //https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F + // 头部高度 + header: { + heightLayoutHeader: 64, + }, + // 侧边栏配置 + sider: { + colorMenuBackground: '#fff', + colorTextMenuSelected: '#3b82f6', + colorBgMenuItemSelected: '#e6f4ff', + }, }, }; diff --git a/web/public/logo.svg b/web/public/logo.svg index 239bf69..52aafa5 100644 --- a/web/public/logo.svg +++ b/web/public/logo.svg @@ -1 +1,4 @@ -Group 28 Copy 5Created with Sketch. \ No newline at end of file + + + K + diff --git a/web/src/access.ts b/web/src/access.ts index 373d9fa..3fb22f0 100644 --- a/web/src/access.ts +++ b/web/src/access.ts @@ -1,11 +1,42 @@ /** + * 权限配置 - 参考GVA的权限验证逻辑 * @see https://umijs.org/docs/max/access#access - * */ + */ + export default function access( - initialState: { currentUser?: API.CurrentUser } | undefined, + initialState: { currentUser?: API.UserInfo } | undefined, ) { const { currentUser } = initialState ?? {}; + + // 获取当前用户的角色ID + const authorityId = currentUser?.authorityId || currentUser?.authority?.authorityId; + + // 超级管理员角色ID(与GVA保持一致,888为超级管理员) + const isSuperAdmin = authorityId === 888; + + // 普通管理员 + const isAdmin = !!currentUser && !!authorityId; + return { - canAdmin: currentUser && currentUser.access === 'admin', + // 是否为超级管理员 + canSuperAdmin: isSuperAdmin, + // 是否为管理员(已登录且有角色) + canAdmin: isAdmin, + // 是否已登录 + isLogin: !!currentUser, + // 按钮权限检查函数 - 参考GVA的btnAuth + canAccess: (btnKey: string) => { + // 超级管理员拥有所有权限 + if (isSuperAdmin) return true; + // 检查当前路由的按钮权限 + const btns = (currentUser as any)?.btns || {}; + return !!btns[btnKey]; + }, }; } + +/** + * 按钮权限Hook - 参考GVA的useBtnAuth + * 在组件中使用: const { canAccess } = useAccess(); + * 然后: canAccess('add') 检查是否有添加权限 + */ diff --git a/web/src/app.tsx b/web/src/app.tsx index 759c004..3031e42 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -2,7 +2,6 @@ import { AvatarDropdown, AvatarName, Footer, - Question, SelectLang, } from "@/components"; import { getUserInfo } from "@/services/system/user"; @@ -18,38 +17,42 @@ const isDev = process.env.NODE_ENV === "development"; const isDevOrTest = isDev || process.env.CI; const loginPath = "/user/login"; +/** + * 获取用户信息 - 参考GVA的GetUserInfo + */ +const fetchUserInfo = async (): Promise => { + try { + const token = localStorage.getItem('token'); + if (!token) { + return undefined; + } + const res = await getUserInfo(); + // 与GVA一致,code === 0 表示成功 + if (res.code === 0 && res.data?.userInfo) { + // 同步更新localStorage中的userInfo - 与GVA一致 + localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo)); + return res.data.userInfo; + } + return undefined; + } catch (error) { + // 错误已在requestErrorConfig中处理,这里不需要额外处理 + // 401错误会弹出Modal,用户点击确定后跳转登录页 + return undefined; + } +}; + /** * @see https://umijs.org/docs/api/runtime-config#getinitialstate - * */ + */ export async function getInitialState(): Promise<{ - settings?: Partial; + settings?: Partial & { showWatermark?: boolean }; currentUser?: API.UserInfo; loading?: boolean; fetchUserInfo?: () => Promise; }> { - const fetchUserInfo = async () => { - try { - const token = localStorage.getItem('token'); - if (!token) { - return undefined; - } - const res = await getUserInfo(); - if (res.code === 0 && res.data?.userInfo) { - return res.data.userInfo; - } - return undefined; - } catch (error) { - history.push(loginPath); - } - return undefined; - }; - // 如果不是登录页面,执行 + // 如果不是登录页面,获取用户信息 - 与GVA的permission.js逻辑一致 const { location } = history; - if ( - ![loginPath, "/user/register", "/user/register-result"].includes( - location.pathname - ) - ) { + if (![loginPath, "/user/register", "/user/register-result"].includes(location.pathname)) { const currentUser = await fetchUserInfo(); return { fetchUserInfo, @@ -62,98 +65,3 @@ export async function getInitialState(): Promise<{ settings: defaultSettings as Partial, }; } - -// ProLayout 支持的api https://procomponents.ant.design/components/layout -export const layout: RunTimeLayoutConfig = ({ - initialState, - setInitialState, -}) => { - return { - actionsRender: () => [ - , - , - ], - avatarProps: { - src: initialState?.currentUser?.avatar, - title: , - render: (_, avatarChildren) => ( - {avatarChildren} - ), - }, - waterMarkProps: { - content: initialState?.currentUser?.name, - }, - footerRender: () =>