feat: implement user authentication with login functionality
- Add auth-api for handling login requests. - Update index.ts to export AuthApi. - Modify HeroLayout to display username or login link based on authentication state. - Create LoginPage component for user login. - Update router to include login route with EmptyLayout. - Configure WebClient to include credentials in requests. - Add auth-slice for managing authentication state in Redux. - Update Redux store to include auth reducer. - Define LoginRequest and User types in types/index.ts. - Configure Vite to proxy API requests to the backend server.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { LoginRequest, User } from "@/types"
|
||||
import { WebClient } from "@/shared/web-client"
|
||||
|
||||
export async function login(loginRequest: LoginRequest): Promise<User> {
|
||||
const { data } = await WebClient.post<User>("/auth/login", {
|
||||
...loginRequest,
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * as FirearmApi from "./firearm-api"
|
||||
export * as ModificationApi from "./modification-api"
|
||||
export * as TagApi from "./tag-api"
|
||||
export * as AuthApi from "./auth-api"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Outlet, Link } from "react-router-dom"
|
||||
import { useMemo } from "react"
|
||||
import dayjs from "dayjs"
|
||||
import { useAppSelector } from "@/store"
|
||||
|
||||
/**
|
||||
* Main application component that serves as the root layout.
|
||||
@@ -8,6 +9,7 @@ import dayjs from "dayjs"
|
||||
*/
|
||||
export default function HeroLayout() {
|
||||
const today = useMemo(() => dayjs(), [])
|
||||
const user = useAppSelector((state) => state.auth.user)
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
@@ -33,6 +35,18 @@ export default function HeroLayout() {
|
||||
>
|
||||
改枪码
|
||||
</Link>
|
||||
{user ? (
|
||||
<span className="text-gray-700 px-3 py-2 rounded-md text-sm font-medium">
|
||||
{user.username}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
登录
|
||||
</Link>
|
||||
)}
|
||||
<a
|
||||
href="https://github.com/zihluwang/delta-force-firearm-modification-codes"
|
||||
target="_blank"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { App, Button, Card, Form, Input, Typography } from "antd"
|
||||
import { AuthApi } from "@/api"
|
||||
import { useAppDispatch } from "@/store"
|
||||
import { setCurrentUser } from "@/store/auth-slice"
|
||||
import { LoginRequest } from "@/types"
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const { message } = App.useApp()
|
||||
const dispatch = useAppDispatch()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function onFinish(values: LoginRequest) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const user = await AuthApi.login(values)
|
||||
dispatch(setCurrentUser(user))
|
||||
message.success(`欢迎回来,${user.username}`)
|
||||
navigate("/firearms")
|
||||
} catch {
|
||||
message.error("登录失败,请检查帐号或密码")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 px-4 py-10 sm:py-16">
|
||||
<div className="mx-auto max-w-md">
|
||||
<Card bordered={false} className="shadow-sm">
|
||||
<Typography.Title level={3} className="!mb-2 text-center">
|
||||
登录
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="!mb-6 text-center !text-gray-500">
|
||||
使用你的帐号登录后即可继续操作
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Form<LoginRequest> layout="vertical" onFinish={onFinish} requiredMark={false}>
|
||||
<Form.Item<LoginRequest>
|
||||
name="principle"
|
||||
label="帐号"
|
||||
rules={[{ required: true, message: "请输入帐号" }]}
|
||||
>
|
||||
<Input autoComplete="username" placeholder="请输入帐号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<LoginRequest>
|
||||
name="credential"
|
||||
label="密码"
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
>
|
||||
<Input.Password autoComplete="current-password" placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="!mb-0">
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentType } from "react"
|
||||
import { createBrowserRouter } from "react-router-dom"
|
||||
import ErrorPage from "@/components/error-page"
|
||||
import EmptyLayout from "@/layout/empty-layout"
|
||||
import HeroLayout from "@/layout/hero-layout"
|
||||
|
||||
function lazy<T extends { default: ComponentType<unknown> }>(importer: () => Promise<T>) {
|
||||
@@ -37,6 +38,16 @@ const router = createBrowserRouter(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <EmptyLayout />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
lazy: lazy(() => import("@/page/login")),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
basename: "/",
|
||||
|
||||
@@ -4,6 +4,7 @@ import dayjs from "dayjs"
|
||||
const WebClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(),
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
export { WebClient }
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { User } from "@/types"
|
||||
|
||||
interface AuthState {
|
||||
user: User | null
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
}
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentUser(state, action: PayloadAction<User>) {
|
||||
state.user = action.payload
|
||||
},
|
||||
clearCurrentUser(state) {
|
||||
state.user = null
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setCurrentUser, clearCurrentUser } = authSlice.actions
|
||||
export const authReducer = authSlice.reducer
|
||||
+3
-1
@@ -11,6 +11,7 @@ import {
|
||||
REGISTER,
|
||||
} from "redux-persist"
|
||||
import createWebStorage from "redux-persist/es/storage/createWebStorage"
|
||||
import { authReducer } from "./auth-slice"
|
||||
import { firearmsReducer } from "./firearms-slice"
|
||||
|
||||
const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local")
|
||||
@@ -18,10 +19,11 @@ const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local")
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
whitelist: ["firearms"],
|
||||
whitelist: ["auth", "firearms"],
|
||||
}
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
firearms: firearmsReducer
|
||||
})
|
||||
|
||||
|
||||
@@ -47,3 +47,14 @@ export interface PageQueryParams {
|
||||
sortBy?: string
|
||||
direction?: Direction
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
principle: string
|
||||
credential: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user