mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 04:30:06 +00:00
feat: login & check user
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
48
src/api/api.ts
Normal file
48
src/api/api.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
interface CommonResponse<T> {
|
||||
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<T>(method: FetcherMethod, path: string, data?: any): Promise<T> {
|
||||
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<T> = await response.json();
|
||||
if (!responseData.success) {
|
||||
throw new Error(responseData.error);
|
||||
}
|
||||
return responseData.data;
|
||||
}
|
||||
10
src/api/user.ts
Normal file
10
src/api/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { User } from "@/types"
|
||||
import { fetcher, FetcherMethod } from "./api"
|
||||
|
||||
export const getProfile = async (): Promise<User> => {
|
||||
return fetcher<User>(FetcherMethod.GET, '/api/v1/profile', null)
|
||||
}
|
||||
|
||||
export const login = async (username: string, password: string): Promise<any> => {
|
||||
return fetcher<any>(FetcherMethod.POST, '/api/v1/login', { username, password })
|
||||
}
|
||||
29
src/components/ui/sonner.tsx
Normal file
29
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -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<AuthContextProps>({
|
||||
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<number>(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);
|
||||
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 = () => {
|
||||
|
||||
@@ -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<typeof formSchema>) {
|
||||
console.log(values)
|
||||
login(values)
|
||||
login(values.username, values.password)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 哪吒监控
|
||||
</footer>
|
||||
</Card>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user