mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 12:40:08 +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",
|
"clsx": "^2.1.1",
|
||||||
"jotai-zustand": "^0.6.0",
|
"jotai-zustand": "^0.6.0",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.53.1",
|
||||||
"react-router-dom": "^6.27.0",
|
"react-router-dom": "^6.27.0",
|
||||||
|
"sonner": "^1.6.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
@@ -3824,6 +3826,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.18",
|
"version": "2.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||||
@@ -4539,6 +4551,16 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jotai-zustand": "^0.6.0",
|
"jotai-zustand": "^0.6.0",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.53.1",
|
||||||
"react-router-dom": "^6.27.0",
|
"react-router-dom": "^6.27.0",
|
||||||
|
"sonner": "^1.6.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8",
|
"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 { useNavigate } from "react-router-dom";
|
||||||
import { useMainStore } from "./useMainStore";
|
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>({
|
const AuthContext = createContext<AuthContextProps>({
|
||||||
profile: undefined,
|
profile: undefined,
|
||||||
@@ -14,11 +16,35 @@ export const AuthProvider = ({ children }: {
|
|||||||
}) => {
|
}) => {
|
||||||
const profile = useMainStore(store => store.profile)
|
const profile = useMainStore(store => store.profile)
|
||||||
const setProfile = useMainStore(store => store.setProfile)
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
const login = async (profile: User | undefined) => {
|
const login = async (username: string, password: string) => {
|
||||||
setProfile(profile);
|
try {
|
||||||
|
await loginRequest(username, password)
|
||||||
|
setProfile({ username: username });
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
|
} catch (error: any) {
|
||||||
|
toast(error.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ const formSchema = z.object({
|
|||||||
username: z.string().min(2, {
|
username: z.string().min(2, {
|
||||||
message: "Username must be at least 2 characters.",
|
message: "Username must be at least 2 characters.",
|
||||||
}),
|
}),
|
||||||
password: z.string().min(8, {
|
password: z.string().min(1, {
|
||||||
message: "Password must be at least 8 characters.",
|
message: "Password cannot be empty.",
|
||||||
}),
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@@ -36,8 +36,7 @@ export default () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
console.log(values)
|
login(values.username, values.password)
|
||||||
login(values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
return (
|
return (
|
||||||
@@ -16,6 +17,7 @@ export default function Root() {
|
|||||||
© 2019-2024 哪吒监控
|
© 2019-2024 哪吒监控
|
||||||
</footer>
|
</footer>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Toaster />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import { User } from "./user";
|
|||||||
|
|
||||||
export interface AuthContextProps {
|
export interface AuthContextProps {
|
||||||
profile: User | undefined;
|
profile: User | undefined;
|
||||||
login: (profile: User | undefined) => void;
|
login: (username: string, password: string) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,14 @@ import { defineConfig } from "vite"
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/dashboard',
|
base: '/dashboard',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8008',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
Reference in New Issue
Block a user