diff --git a/backend/src/main/java/com/imeeting/controller/UserController.java b/backend/src/main/java/com/imeeting/controller/UserController.java index bdf01c6..35b00e2 100644 --- a/backend/src/main/java/com/imeeting/controller/UserController.java +++ b/backend/src/main/java/com/imeeting/controller/UserController.java @@ -2,6 +2,7 @@ package com.imeeting.controller; import com.imeeting.auth.JwtTokenProvider; import com.imeeting.common.ApiResponse; +import com.imeeting.dto.PasswordUpdateDTO; import com.imeeting.dto.UserProfile; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.imeeting.security.LoginUser; @@ -101,6 +102,7 @@ public class UserController { profile.setStatus(user.getStatus()); profile.setAdmin(userId == 1L); profile.setIsPlatformAdmin(user.getIsPlatformAdmin()); + profile.setPwdResetRequired(user.getPwdResetRequired()); return ApiResponse.ok(profile); } @@ -182,6 +184,41 @@ public class UserController { return ApiResponse.ok(sysUserService.removeById(id)); } + @PutMapping("/profile") + public ApiResponse updateProfile(@RequestBody SysUser user) { + Long userId = getCurrentUserId(); + SysUser existing = sysUserService.getByIdIgnoreTenant(userId); + if (existing == null) return ApiResponse.error("用户不存在"); + + existing.setDisplayName(user.getDisplayName()); + existing.setEmail(user.getEmail()); + existing.setPhone(user.getPhone()); + return ApiResponse.ok(sysUserService.updateById(existing)); + } + + @PutMapping("/password") + public ApiResponse updatePassword(@RequestBody PasswordUpdateDTO dto) { + Long userId = getCurrentUserId(); + SysUser user = sysUserService.getByIdIgnoreTenant(userId); + if (user == null) return ApiResponse.error("用户不存在"); + + if (!passwordEncoder.matches(dto.getOldPassword(), user.getPasswordHash())) { + return ApiResponse.error("旧密码不正确"); + } + + user.setPasswordHash(passwordEncoder.encode(dto.getNewPassword())); + user.setPwdResetRequired(0); // 重置标志位 + return ApiResponse.ok(sysUserService.updateById(user)); + } + + private Long getCurrentUserId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getPrincipal() instanceof LoginUser) { + return ((LoginUser) auth.getPrincipal()).getUserId(); + } + return null; + } + @GetMapping("/{id}/roles") @PreAuthorize("@ss.hasPermi('sys:user:role:list')") public ApiResponse> listUserRoles(@PathVariable Long id) { diff --git a/backend/src/main/java/com/imeeting/dto/UserProfile.java b/backend/src/main/java/com/imeeting/dto/UserProfile.java index cd927cd..824606f 100644 --- a/backend/src/main/java/com/imeeting/dto/UserProfile.java +++ b/backend/src/main/java/com/imeeting/dto/UserProfile.java @@ -14,4 +14,5 @@ public class UserProfile { @JsonProperty("isAdmin") private boolean isAdmin; private Boolean isPlatformAdmin; + private Integer pwdResetRequired; } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index c7ee20b..59e9f92 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -95,6 +95,16 @@ export async function getCurrentUser() { return resp.data.data as UserProfile; } +export async function updateMyProfile(payload: Partial) { + const resp = await http.put("/api/users/profile", payload); + return resp.data.data as boolean; +} + +export async function updateMyPassword(payload: any) { + const resp = await http.put("/api/users/password", payload); + return resp.data.data as boolean; +} + export async function createPermission(payload: Partial) { const resp = await http.post("/api/permissions", payload); return resp.data.data as boolean; diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 21d0fc3..4cef054 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -1,4 +1,5 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; +import { UserProfile } from "../types"; export function useAuth() { const [accessToken, setAccessToken] = useState(() => localStorage.getItem("accessToken")); @@ -9,13 +10,19 @@ export function useAuth() { return () => window.removeEventListener("storage", handler); }, []); + const profile = useMemo(() => { + const data = sessionStorage.getItem("userProfile"); + return data ? JSON.parse(data) : null; + }, [accessToken]); + const isAuthed = !!accessToken; const logout = () => { localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); sessionStorage.removeItem("userProfile"); + setAccessToken(null); }; - return { accessToken, isAuthed, logout }; + return { accessToken, isAuthed, profile, logout }; } diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index fb035a0..3619a34 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -1,5 +1,5 @@ import { Layout, Menu, Button, Space, Avatar, Dropdown, message, type MenuProps, Select } from "antd"; -import { useEffect, useState, useMemo } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { @@ -49,7 +49,7 @@ export default function AppLayout() { const location = useLocation(); const navigate = useNavigate(); const { logout } = useAuth(); - const { load: loadPermissions } = usePermission(); + const { load: loadPermissions, can } = usePermission(); const fetchInitialData = async () => { try { @@ -111,17 +111,17 @@ export default function AppLayout() { } }; - const handleLogout = () => { + const handleLogout = useCallback(() => { logout(); navigate("/login"); - }; + }, [logout, navigate]); - const changeLanguage = (lng: string) => { + const changeLanguage = useCallback((lng: string) => { i18n.changeLanguage(lng); message.success(lng === 'zh-CN' ? '已切换至中文' : 'Switched to English'); - }; + }, [i18n]); - const buildMenuTree = (list: SysPermission[]) => { + const buildMenuTree = useCallback((list: SysPermission[]) => { const map = new Map(); const roots: (SysPermission & { children?: SysPermission[] })[] = []; list.forEach((m) => map.set(m.permId, { ...m, children: [] })); @@ -138,9 +138,9 @@ export default function AppLayout() { }; sortNodes(roots); return roots; - }; + }, []); - const toMenuItems = (nodes: (SysPermission & { children?: SysPermission[] })[]) => + const toMenuItems = useCallback((nodes: (SysPermission & { children?: SysPermission[] })[]): any[] => nodes.map((m) => { const key = m.path || m.code || String(m.permId); const icon = m.icon ? (iconMap[m.icon] || ) : ; @@ -157,16 +157,45 @@ export default function AppLayout() { icon, label: {m.name}, }; + }), []); // 移除 [toMenuItems] 依赖项以解决 TDZ 错误 + + const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [menus, buildMenuTree, toMenuItems]); + + const userMenuItems: MenuProps["items"] = useMemo(() => { + const items: any[] = [ + { + key: 'profile', + label: {t('layout.profile')}, + icon: + }, + ]; + + let profile: any = {}; + try { + const stored = sessionStorage.getItem("userProfile"); + if (stored) profile = JSON.parse(stored) || {}; + } catch (e) { + profile = {}; + } + + if (profile.isPlatformAdmin || can("sys_platform:config:update")) { + items.push({ + key: 'settings', + label: {t('layout.settings')}, + icon: + }); + } + + items.push({ type: 'divider', key: 'd1' }); + items.push({ + key: 'logout', + label: t('layout.logout'), + icon: , + onClick: handleLogout }); - const menuItems = toMenuItems(buildMenuTree(menus)); - - const userMenuItems: MenuProps["items"] = [ - { key: 'profile', label: t('layout.profile'), icon: }, - { key: 'settings', label: t('layout.settings'), icon: }, - { type: 'divider' }, - { key: 'logout', label: t('layout.logout'), icon: , onClick: handleLogout }, - ]; + return items; + }, [t, can, handleLogout]); const langMenuItems: MenuProps["items"] = [ { key: 'zh-CN', label: '简体中文', onClick: () => changeLanguage('zh-CN') }, diff --git a/frontend/src/pages/Orgs.tsx b/frontend/src/pages/Orgs.tsx index 99567bc..1f19ea4 100644 --- a/frontend/src/pages/Orgs.tsx +++ b/frontend/src/pages/Orgs.tsx @@ -82,14 +82,18 @@ export default function Orgs() { const [form] = Form.useForm(); const loadTenants = async () => { + // 优化:如果不是平台模式,直接设置当前租户,不发请求 + if (!isPlatformMode) { + setSelectedTenantId(activeTenantId); + return; + } + try { const resp = await listTenants({ current: 1, size: 100 }); const list = resp.records || []; setTenants(list); - if (!isPlatformMode) { - setSelectedTenantId(activeTenantId); - } else if (list.length > 0 && selectedTenantId === undefined) { + if (list.length > 0 && selectedTenantId === undefined) { setSelectedTenantId(list[0].id); } } catch (e) { diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index de8292e..fcfa07c 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,14 +1,19 @@ import { Navigate, Route, Routes } from "react-router-dom"; import Login from "../pages/Login"; +import ResetPassword from "../pages/ResetPassword"; import AppLayout from "../layouts/AppLayout"; import { menuRoutes } from "./routes"; import { useAuth } from "../hooks/useAuth"; function RequireAuth({ children }: { children: JSX.Element }) { - const { isAuthed } = useAuth(); + const { isAuthed, profile } = useAuth(); if (!isAuthed) { return ; } + // 强制改密拦截 + if (profile?.pwdResetRequired === 1) { + return ; + } return children; } @@ -16,6 +21,7 @@ export default function AppRoutes() { return ( } /> + } /> , perm: "menu:dashboard" }, + { path: "/profile", label: "个人中心", element: }, { path: "/tenants", label: "租户管理", element: , perm: "menu:tenants" }, { path: "/orgs", label: "组织管理", element: , perm: "menu:orgs" }, { path: "/users", label: "用户管理", element: , perm: "menu:users" }, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 33c9aa2..56047c8 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -29,6 +29,7 @@ export interface UserProfile { status?: number; isAdmin: boolean; isPlatformAdmin?: boolean; + pwdResetRequired?: number; }