feat: login & check user

This commit is contained in:
naiba
2024-11-03 23:29:32 +08:00
parent 772d66334e
commit b1a0b607da
10 changed files with 157 additions and 11 deletions

22
package-lock.json generated
View File

@@ -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",

View File

@@ -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
View 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
View 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 })
}

View 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 }

View File

@@ -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 = () => {

View File

@@ -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 (

View File

@@ -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() {
&copy; 2019-2024 &copy; 2019-2024
</footer> </footer>
</Card> </Card>
<Toaster />
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -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;
} }

View File

@@ -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"),