feat: 支持多因素认证登录,更新相关类型定义
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import webClient from "@/client/web-client"
|
import webClient from "@/client/web-client"
|
||||||
import { HttpStatus } from "@/constant"
|
import { HttpStatus } from "@/constant"
|
||||||
import type { CaptchaResponse, UserAuthResponse } from "@/types/web/response"
|
import type { CaptchaResponse, MfaAuthResponse, UserAuthResponse } from "@/types/web/response"
|
||||||
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,8 +21,8 @@ async function getCaptcha(): Promise<CaptchaResponse | null> {
|
|||||||
*/
|
*/
|
||||||
async function usernamePasswordLogin(
|
async function usernamePasswordLogin(
|
||||||
request: UsernamePasswordLoginRequest
|
request: UsernamePasswordLoginRequest
|
||||||
): Promise<UserAuthResponse | null> {
|
): Promise<UserAuthResponse | MfaAuthResponse | null> {
|
||||||
const { data } = await webClient.post<UserAuthResponse>("/auth/login", request)
|
const { data } = await webClient.post<UserAuthResponse | MfaAuthResponse>("/auth/login", request)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react"
|
import { type ReactNode, useEffect, useMemo, useState } from "react"
|
||||||
import { useNavigate } from "react-router"
|
import { Link, Navigate, useNavigate } from "react-router"
|
||||||
import {
|
import { App, Avatar, Breadcrumb, Dropdown, Layout, Menu, type MenuProps, Space } from "antd"
|
||||||
App,
|
|
||||||
Avatar,
|
|
||||||
Breadcrumb,
|
|
||||||
Dropdown,
|
|
||||||
Layout,
|
|
||||||
Menu,
|
|
||||||
type MenuProps,
|
|
||||||
message,
|
|
||||||
Space,
|
|
||||||
} from "antd"
|
|
||||||
import { DownOutlined } from "@ant-design/icons"
|
import { DownOutlined } from "@ant-design/icons"
|
||||||
import { ApplicationLogo } from "@/components/icon"
|
import { ApplicationLogo } from "@/components/icon"
|
||||||
import { useAppDispatch, useAppSelector } from "@/store"
|
import { useAppDispatch, useAppSelector } from "@/store"
|
||||||
import { useAntBreadcrumbs } from "@/hooks"
|
import { useAntBreadcrumbs } from "@/hooks"
|
||||||
import { logout } from "@/store/auth-slice"
|
import { logout } from "@/store/auth-slice"
|
||||||
import { MenuApi } from "@/api"
|
import { MenuApi } from "@/api"
|
||||||
import axios, { type AxiosError } from "axios"
|
import axios from "axios"
|
||||||
import type { TreeNode } from "@/types/tree"
|
import type { TreeNode } from "@/types/tree"
|
||||||
import type { MenuItem } from "@/types/entity"
|
import type { MenuItem } from "@/types/entity"
|
||||||
import { AppUtils } from "@/utils"
|
import { AppUtils } from "@/utils"
|
||||||
import type { GeneralErrorResponse } from "@/types/web/response"
|
import type { GeneralErrorResponse } from "@/types/web/response"
|
||||||
|
|
||||||
const { Header, Footer, Sider, Content } = Layout
|
|
||||||
type AntMenuItem = Required<MenuProps>["items"][number]
|
type AntMenuItem = Required<MenuProps>["items"][number]
|
||||||
|
|
||||||
|
export interface DashboardLayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
function transformMenuData(nodes: TreeNode<MenuItem>[]): AntMenuItem[] {
|
function transformMenuData(nodes: TreeNode<MenuItem>[]): AntMenuItem[] {
|
||||||
if (!nodes || nodes.length === 0) {
|
if (!nodes || nodes.length === 0) {
|
||||||
return []
|
return []
|
||||||
@@ -37,21 +30,32 @@ function transformMenuData(nodes: TreeNode<MenuItem>[]): AntMenuItem[] {
|
|||||||
const { item, children } = node
|
const { item, children } = node
|
||||||
const hasChildren = children && children.length > 0
|
const hasChildren = children && children.length > 0
|
||||||
|
|
||||||
|
if (item.isVisible) {
|
||||||
const menuItem: AntMenuItem = {
|
const menuItem: AntMenuItem = {
|
||||||
key: item.code,
|
key: item.code,
|
||||||
label: item.name,
|
label: item.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.path) {
|
||||||
|
if (item.isExternalLink) {
|
||||||
|
menuItem.extra = <a href={item.path} target="_blank"/>
|
||||||
|
} else {
|
||||||
|
menuItem.extra = <Link to={item.path} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
// Append children
|
// Append children
|
||||||
return { ...menuItem, children: transformMenuData(children) }
|
return { ...menuItem, children: transformMenuData(children) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuItem
|
return menuItem
|
||||||
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
const { modal, message } = App.useApp()
|
const { modal, message } = App.useApp()
|
||||||
const user = useAppSelector((store) => store.auth.user!)
|
const user = useAppSelector((store) => store.auth.user!)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -105,7 +109,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="h-full">
|
<Layout className="h-full">
|
||||||
<Header className="flex items-center justify-between bg-linear-to-br from-blue-50 to-indigo-100">
|
<Layout.Header className="flex items-center justify-between bg-linear-to-br from-blue-50 to-indigo-100">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<ApplicationLogo className="text-4xl" />
|
<ApplicationLogo className="text-4xl" />
|
||||||
<span className="text-xl">{appTitle}</span>
|
<span className="text-xl">{appTitle}</span>
|
||||||
@@ -122,32 +126,18 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Avatar src={user.avatarUrl} alt="用户头像" />
|
<Avatar src={user.avatarUrl} alt="用户头像" />
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Layout.Header>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Sider width={200} className="bg-white">
|
<Layout.Sider width={200} className="bg-white">
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
className="h-full max-h-full border-e-0"
|
className="h-full max-h-full border-e-0"
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
onSelect={({ key }) => {
|
|
||||||
console.log(`key = ${key}`)
|
|
||||||
switch (key) {
|
|
||||||
case "user-mgmt":
|
|
||||||
void navigate("/users")
|
|
||||||
break
|
|
||||||
case "role-mgmt":
|
|
||||||
void navigate("/roles")
|
|
||||||
break
|
|
||||||
case "menu-mgmt":
|
|
||||||
void navigate("/menus")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Layout.Sider>
|
||||||
<Layout className="pt-0 px-6 pb-6">
|
<Layout className="pt-0 px-6 pb-6">
|
||||||
<Breadcrumb items={breadcrumbItems} className="my-4 mx-0" />
|
<Breadcrumb items={breadcrumbItems} className="my-4 mx-0" />
|
||||||
<Content className="p-6 m-0 min-h-70 bg-white">{children}</Content>
|
<Layout.Content className="p-6 m-0 min-h-70 bg-white">{children}</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
+19
-19
@@ -1,9 +1,10 @@
|
|||||||
import { type MouseEvent, useCallback, useEffect, useState } from "react"
|
import { type MouseEvent, useCallback, useEffect, useState } from "react"
|
||||||
import { Link, useNavigate } from "react-router"
|
import { Link, useNavigate } from "react-router"
|
||||||
import { Form, Input, Button, Card, message, Divider } from "antd"
|
import { Form, Input, Button, Card, message, Divider, App } from "antd"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import type { AxiosError } from "axios"
|
import type { AxiosError } from "axios"
|
||||||
// import { useMsal } from "@azure/msal-react"
|
import { GithubFilled } from "@ant-design/icons"
|
||||||
|
import { useMsal } from "@azure/msal-react"
|
||||||
import { AuthApi } from "@/api"
|
import { AuthApi } from "@/api"
|
||||||
import { useAppDispatch, useAppSelector } from "@/store"
|
import { useAppDispatch, useAppSelector } from "@/store"
|
||||||
import { loginSuccess, updateRegistrationEnabled } from "@/store/auth-slice"
|
import { loginSuccess, updateRegistrationEnabled } from "@/store/auth-slice"
|
||||||
@@ -18,8 +19,6 @@ import {
|
|||||||
DiscordFilled,
|
DiscordFilled,
|
||||||
EmailFilled,
|
EmailFilled,
|
||||||
} from "@/components/icon"
|
} from "@/components/icon"
|
||||||
|
|
||||||
import { GithubFilled } from "@ant-design/icons"
|
|
||||||
import { fetchRegisterEnabled } from "@/api/auth"
|
import { fetchRegisterEnabled } from "@/api/auth"
|
||||||
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
||||||
import type { CaptchaResponse, GeneralErrorResponse } from "@/types/web/response"
|
import type { CaptchaResponse, GeneralErrorResponse } from "@/types/web/response"
|
||||||
@@ -29,9 +28,7 @@ export default function LoginPage() {
|
|||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// const msalContext = useMsal()
|
const { message } = App.useApp()
|
||||||
|
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
|
||||||
const [form] = Form.useForm<UsernamePasswordLoginRequest>()
|
const [form] = Form.useForm<UsernamePasswordLoginRequest>()
|
||||||
|
|
||||||
// const [isLoading, setIsLoading] = useState<boolean>(false)
|
// const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
@@ -77,28 +74,33 @@ export default function LoginPage() {
|
|||||||
console.log("Login values:", values)
|
console.log("Login values:", values)
|
||||||
|
|
||||||
const loginResponse = await AuthApi.usernamePasswordLogin(values)
|
const loginResponse = await AuthApi.usernamePasswordLogin(values)
|
||||||
if (loginResponse) {
|
|
||||||
|
if (loginResponse == null) {
|
||||||
|
message.error("登录失败:服务器响应异常。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('mfaToken' in loginResponse) {
|
||||||
|
|
||||||
|
} else {
|
||||||
dispatch(
|
dispatch(
|
||||||
loginSuccess({
|
loginSuccess({
|
||||||
user: loginResponse.user,
|
user: loginResponse
|
||||||
token: loginResponse.accessToken,
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
messageApi.success("登录成功", dayjs.duration({ seconds: 3 }).asSeconds())
|
message.success("登录成功", dayjs.duration({ seconds: 3 }).asSeconds())
|
||||||
await navigate("/")
|
await navigate("/")
|
||||||
} else {
|
|
||||||
messageApi.error("登录失败:服务器响应异常。")
|
|
||||||
}
|
}
|
||||||
} catch (errorInfo: unknown) {
|
} catch (errorInfo: unknown) {
|
||||||
const error = errorInfo as AxiosError<GeneralErrorResponse>
|
const error = errorInfo as AxiosError<GeneralErrorResponse>
|
||||||
console.log(error)
|
console.log(error)
|
||||||
messageApi.error(
|
message.error(
|
||||||
error.response?.data.message ?? "登录失败,请稍后再试",
|
error.response?.data.message ?? "登录失败,请稍后再试",
|
||||||
dayjs.duration({ seconds: 3 }).asSeconds()
|
dayjs.duration({ seconds: 3 }).asSeconds()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, navigate, messageApi]
|
[dispatch, navigate]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,9 +192,7 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-linear-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
||||||
{contextHolder}
|
|
||||||
|
|
||||||
{/* 背景装饰元素 */}
|
{/* 背景装饰元素 */}
|
||||||
<div className="absolute top-0 left-0 w-72 h-72 bg-blue-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
<div className="absolute top-0 left-0 w-72 h-72 bg-blue-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||||
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||||
@@ -275,7 +275,7 @@ export default function LoginPage() {
|
|||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
block
|
block
|
||||||
size="large"
|
size="large"
|
||||||
className="h-12 rounded-lg font-semibold text-base bg-gradient-to-r from-blue-500 to-purple-600 border-0 hover:from-blue-600 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl">
|
className="h-12 rounded-lg font-semibold text-base bg-linear-to-r from-blue-500 to-purple-600 border-0 hover:from-blue-600 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl">
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface MenuItem {
|
|||||||
parentId: number | null
|
parentId: number | null
|
||||||
code: string
|
code: string
|
||||||
sort: number
|
sort: number
|
||||||
|
path: string | null
|
||||||
isExternalLink: boolean
|
isExternalLink: boolean
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
status: Status
|
status: Status
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ export interface PageResponse<T> {
|
|||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAuthResponse {
|
export interface UserAuthResponse extends User {
|
||||||
user: User
|
}
|
||||||
accessToken: string
|
|
||||||
|
export interface MfaAuthResponse {
|
||||||
|
mfaToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDetailResponse extends User {
|
export interface UserDetailResponse extends User {
|
||||||
|
|||||||
Reference in New Issue
Block a user