From b1a0b607da0a78dea1c19d02d89e85d1a72204d8 Mon Sep 17 00:00:00 2001 From: naiba Date: Sun, 3 Nov 2024 23:29:32 +0800 Subject: [PATCH] feat: login & check user --- package-lock.json | 22 +++++++++++++++++ package.json | 2 ++ src/api/api.ts | 48 ++++++++++++++++++++++++++++++++++++ src/api/user.ts | 10 ++++++++ src/components/ui/sonner.tsx | 29 ++++++++++++++++++++++ src/hooks/useAuth.tsx | 36 +++++++++++++++++++++++---- src/routes/login.tsx | 9 +++---- src/routes/root.tsx | 2 ++ src/types/authContext.tsx | 2 +- vite.config.ts | 8 ++++++ 10 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 src/api/api.ts create mode 100644 src/api/user.ts create mode 100644 src/components/ui/sonner.tsx diff --git a/package-lock.json b/package-lock.json index 1af13f3..657ed59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,12 @@ "clsx": "^2.1.1", "jotai-zustand": "^0.6.0", "lucide-react": "^0.454.0", + "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.1", "react-router-dom": "^6.27.0", + "sonner": "^1.6.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8", @@ -3824,6 +3826,16 @@ "dev": true, "license": "MIT" }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -4539,6 +4551,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sonner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.6.1.tgz", + "integrity": "sha512-0iD+eDJHyJitl069BC6wVDykQD56FMKk4TD6XkcCcikcDYaGsFKlSU0mZQXYWKPpFof3jlV/u4vGZc2KCqz8OQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 3f6eec1..5af2d6f 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "clsx": "^2.1.1", "jotai-zustand": "^0.6.0", "lucide-react": "^0.454.0", + "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.1", "react-router-dom": "^6.27.0", + "sonner": "^1.6.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8", diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..843280c --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,48 @@ +interface CommonResponse { + success: boolean; + error: string; + data: T; +} + +function buildUrl(path: string, data?: any): string { + if (!data) + return path + const url = new URL(path); + for (const key in data) { + url.searchParams.append(key, data[key]); + } + return url.toString(); +} + +export enum FetcherMethod { + GET = "GET", + POST = "POST", + PUT = "PUT", + PATCH = "PATCH", + DELETE = "DELETE", +} + +export async function fetcher(method: FetcherMethod, path: string, data?: any): Promise { + let response; + if (method === FetcherMethod.GET || method === FetcherMethod.DELETE) { + response = await fetch(buildUrl(path, data), { + method: "GET", + }); + } else { + response = await fetch(path, { + method: method, + headers: { + "Content-Type": "application/json", + }, + body: data ? JSON.stringify(data) : null, + }); + } + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData: CommonResponse = await response.json(); + if (!responseData.success) { + throw new Error(responseData.error); + } + return responseData.data; +} diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..f65fe44 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,10 @@ +import { User } from "@/types" +import { fetcher, FetcherMethod } from "./api" + +export const getProfile = async (): Promise => { + return fetcher(FetcherMethod.GET, '/api/v1/profile', null) +} + +export const login = async (username: string, password: string): Promise => { + return fetcher(FetcherMethod.POST, '/api/v1/login', { username, password }) +} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..1128edf --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,29 @@ +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 30717d1..047028b 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,7 +1,9 @@ -import { createContext, useContext, useMemo } from "react"; +import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useMainStore } from "./useMainStore"; -import { AuthContextProps, User } from "@/types"; +import { AuthContextProps } from "@/types"; +import { getProfile, login as loginRequest } from "@/api/user"; +import { toast } from "sonner"; const AuthContext = createContext({ profile: undefined, @@ -14,11 +16,35 @@ export const AuthProvider = ({ children }: { }) => { const profile = useMainStore(store => store.profile) const setProfile = useMainStore(store => store.setProfile) + const [lastUpdatedAt, setLastUpdatedAt] = useState(0); + + // FIXME @naiba 触发了两次 + useEffect(() => { + if (profile && Date.now() - lastUpdatedAt > 1000 * 60 * 5) { + console.log(profile, Date.now(), lastUpdatedAt) + getProfile().then((data) => { + setLastUpdatedAt(Date.now()) + if (data && data.username !== profile.username) { + console.log('bingo', data.username); + setProfile(data) + } + }).catch(() => { + setLastUpdatedAt(Date.now()) + setProfile(undefined) + }) + } + }, [profile]) + const navigate = useNavigate(); - const login = async (profile: User | undefined) => { - setProfile(profile); - navigate("/dashboard"); + const login = async (username: string, password: string) => { + try { + await loginRequest(username, password) + setProfile({ username: username }); + navigate("/dashboard"); + } catch (error: any) { + toast(error.message); + } }; const logout = () => { diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 5a29747..9f46f87 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -19,9 +19,9 @@ const formSchema = z.object({ username: z.string().min(2, { message: "Username must be at least 2 characters.", }), - password: z.string().min(8, { - message: "Password must be at least 8 characters.", - }), + password: z.string().min(1, { + message: "Password cannot be empty.", + }) }) export default () => { @@ -36,8 +36,7 @@ export default () => { }) function onSubmit(values: z.infer) { - console.log(values) - login(values) + login(values.username, values.password) } return ( diff --git a/src/routes/root.tsx b/src/routes/root.tsx index d06ad55..391a72f 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom"; import { Card } from "@/components/ui/card"; import { ThemeProvider } from "@/components/theme-provider"; import Header from "@/components/header"; +import { Toaster } from "@/components/ui/sonner"; export default function Root() { return ( @@ -16,6 +17,7 @@ export default function Root() { © 2019-2024 哪吒监控 + ); } diff --git a/src/types/authContext.tsx b/src/types/authContext.tsx index a1db2b8..2d1c6da 100644 --- a/src/types/authContext.tsx +++ b/src/types/authContext.tsx @@ -2,6 +2,6 @@ import { User } from "./user"; export interface AuthContextProps { profile: User | undefined; - login: (profile: User | undefined) => void; + login: (username: string, password: string) => void; logout: () => void; } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 5348367..3fcd24c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,14 @@ import { defineConfig } from "vite" export default defineConfig({ base: '/dashboard', plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8008', + changeOrigin: true, + }, + }, + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"),