Bläddra i källkod

feat: update folder structure and route guard

Mohammad Mahdi Salimi 1 månad sedan
förälder
incheckning
827980e936

+ 13 - 10
src/features/auth/sign-in/components/user-auth-form.tsx

@@ -1,4 +1,3 @@
-import { useState } from 'react'
 import { z } from 'zod'
 import { useForm } from 'react-hook-form'
 import { zodResolver } from '@hookform/resolvers/zod'
@@ -6,7 +5,7 @@ import { useNavigate } from '@tanstack/react-router'
 import { Loader2, LogIn } from 'lucide-react'
 import { toast } from 'sonner'
 
-import { useAuthStore } from '@/stores/auth-store'
+import { useLogin } from '@/hooks/useAuth'
 import { cn } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import {
@@ -40,9 +39,8 @@ export function UserAuthForm({
   redirectTo,
   ...props
 }: UserAuthFormProps) {
-  const [isLoading, setIsLoading] = useState(false)
   const navigate = useNavigate()
-  const login = useAuthStore((s) => s.login)
+  const { mutateAsync: login, isPending } = useLogin()
 
   const form = useForm<z.infer<typeof formSchema>>({
     resolver: zodResolver(formSchema),
@@ -53,16 +51,17 @@ export function UserAuthForm({
   })
 
   const onSubmit = async (data: z.infer<typeof formSchema>) => {
-    setIsLoading(true)
     try {
       await login(data)
 
       toast.success('ورود با موفقیت انجام شد')
-      navigate({ to: redirectTo || '/', replace: true })
+
+      navigate({
+        to: redirectTo || '/',
+        replace: true,
+      })
     } catch {
       toast.error('نام کاربری یا رمز عبور اشتباه است')
-    } finally {
-      setIsLoading(false)
     }
   }
 
@@ -79,6 +78,7 @@ export function UserAuthForm({
           render={({ field }) => (
             <FormItem>
               <FormLabel>نام کاربری</FormLabel>
+
               <FormControl>
                 <Input
                   placeholder='نام کاربری یا شماره موبایل'
@@ -86,6 +86,7 @@ export function UserAuthForm({
                   {...field}
                 />
               </FormControl>
+
               <FormMessage />
             </FormItem>
           )}
@@ -97,6 +98,7 @@ export function UserAuthForm({
           render={({ field }) => (
             <FormItem>
               <FormLabel>رمز عبور</FormLabel>
+
               <FormControl>
                 <PasswordInput
                   placeholder='********'
@@ -104,13 +106,14 @@ export function UserAuthForm({
                   {...field}
                 />
               </FormControl>
+
               <FormMessage />
             </FormItem>
           )}
         />
 
-        <Button className='mt-2' disabled={isLoading}>
-          {isLoading ? (
+        <Button className='mt-2' disabled={isPending}>
+          {isPending ? (
             <Loader2 className='mr-2 h-4 w-4 animate-spin' />
           ) : (
             <LogIn className='mr-2 h-4 w-4' />

+ 17 - 0
src/hooks/useAuth.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query'
+import { authService } from '@/services/auth.service'
+import { useAuthStore } from '@/stores/auth-store'
+
+export function useLogin() {
+  const setToken = useAuthStore((s) => s.setToken)
+  const setUser = useAuthStore((s) => s.setUser)
+
+  return useMutation({
+    mutationFn: authService.login,
+
+    onSuccess: (data) => {
+      setToken(data.token)
+      setUser(data.admin)
+    },
+  })
+}

+ 13 - 3
src/routes/_authenticated/route.tsx

@@ -1,6 +1,7 @@
 import { createFileRoute, redirect } from '@tanstack/react-router'
 import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
 import { useAuthStore } from '@/stores/auth-store'
+import { authService } from '@/services/auth.service'
 
 export const Route = createFileRoute('/_authenticated')({
   beforeLoad: async ({ location }) => {
@@ -23,8 +24,14 @@ export const Route = createFileRoute('/_authenticated')({
 
     const hasPersistedToken = persisted && JSON.parse(persisted)?.state?.token
 
-    const { token, isAuthenticated, fetchProfile, logout, profileFetched } =
-      authState
+    const {
+      token,
+      isAuthenticated,
+      profileFetched,
+      logout,
+      setUser,
+      setProfileFetched,
+    } = authState
 
     if (!token || !isAuthenticated || !hasPersistedToken) {
       logout()
@@ -39,7 +46,10 @@ export const Route = createFileRoute('/_authenticated')({
 
     if (!profileFetched) {
       try {
-        await fetchProfile()
+        const profile = await authService.profile()
+
+        setUser(profile.admin)
+        setProfileFetched(true)
       } catch {
         logout()
         throw redirect({

+ 17 - 11
src/services/auth.service.ts

@@ -1,17 +1,23 @@
 import http from '@/core/api/http'
-import { type LoginPayload, type LoginResponse } from '@/types/auth.types'
-
-export interface AuthUser {
-  id: string
-  name: string
-  email: string
-}
+import {
+  type ProfileResponse,
+  type LoginPayload,
+  type LoginResponse,
+} from '@/types/auth.types'
 
 export const authService = {
-  login: (data: LoginPayload) =>
-    http.post<LoginResponse, LoginPayload>('/auth/login', data),
+  login: async (data: LoginPayload) => {
+    const res = await http.post<LoginResponse, LoginPayload>(
+      'admin/auth/login',
+      data
+    )
+    return res.data
+  },
 
-  profile: () => http.get<AuthUser>('/auth/profile'),
+  profile: async () => {
+    const res = await http.get<ProfileResponse>('admin/auth/profile')
+    return res.data
+  },
 
-  logout: () => http.post('/auth/logout'),
+  logout: () => http.post('admin/auth/logout'),
 }

+ 33 - 71
src/stores/auth-store.ts

@@ -1,106 +1,70 @@
+import { type AdminUser } from '@/types/auth.types'
 import { create } from 'zustand'
 import { persist } from 'zustand/middleware'
-import type { AdminUser, LoginResponse } from '@/types/auth.types'
-import { http } from '@/core/api/http'
-
-interface LoginPayload {
-  username: string
-  password: string
-}
 
 interface AuthState {
   user: AdminUser | null
   token: string | null
   isAuthenticated: boolean
-  loading: boolean
-  error: string | null
   isHydrated: boolean
   profileFetched: boolean
-
-  login: (data: LoginPayload) => Promise<void>
-  logout: () => void
-  fetchProfile: () => Promise<void>
+  setToken: (token: string | null) => void
+  setUser: (user: AdminUser | null) => void
+  setHydrated: (state: boolean) => void
+  setProfileFetched: (state: boolean) => void
+  clearAuth: () => void
+  logout: () => Promise<void>
 }
 
 export const useAuthStore = create<AuthState>()(
   persist(
-    (set, get) => ({
-      user: null,
+    (set) => ({
       token: null,
+      user: null,
+
       isAuthenticated: false,
-      loading: false,
-      error: null,
       isHydrated: false,
       profileFetched: false,
 
-      login: async (data: LoginPayload) => {
-        set({ loading: true, error: null })
-
-        try {
-          const response = await http.post<LoginResponse>(
-            '/admin/auth/login',
-            data
-          )
-
-          const { token, admin } = response.data
+      setToken: (token) =>
+        set({
+          token,
+          isAuthenticated: !!token,
+        }),
 
-          set({
-            token,
-            user: admin,
-            isAuthenticated: true,
-            loading: false,
-            error: null,
-            profileFetched: true,
-          })
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        } catch (error: any) {
-          const message = error?.response?.data?.message || 'Login failed'
+      setUser: (user) =>
+        set({
+          user,
+        }),
 
-          set({
-            loading: false,
-            error: message,
-          })
+      setHydrated: (state) =>
+        set({
+          isHydrated: state,
+        }),
 
-          // eslint-disable-next-line preserve-caught-error
-          throw new Error(message)
-        }
-      },
+      setProfileFetched: (state) =>
+        set({
+          profileFetched: state,
+        }),
 
-      logout: () => {
+      clearAuth: () =>
         set({
           user: null,
           token: null,
           isAuthenticated: false,
-          error: null,
           profileFetched: false,
-        })
-      },
-
-      fetchProfile: async () => {
-        const { profileFetched } = get()
-
-        if (profileFetched) return
+        }),
 
+      logout: async () => {
         try {
-          const res = await http.get<{ data: AdminUser }>('/admin/auth/profile')
-
-          set({
-            user: res.data,
-            isAuthenticated: true,
-            profileFetched: true,
-          })
-        } catch (error) {
-          // eslint-disable-next-line no-console
-          console.log('Profile fetch failed', error)
-
+          // optional api logout
+        } finally {
           set({
             user: null,
             token: null,
             isAuthenticated: false,
             profileFetched: false,
           })
-
-          throw error
         }
       },
     }),
@@ -114,9 +78,7 @@ export const useAuthStore = create<AuthState>()(
       }),
 
       onRehydrateStorage: () => (state) => {
-        if (state) {
-          state.isHydrated = true
-        }
+        state?.setHydrated(true)
       },
     }
   )

+ 8 - 0
src/types/auth.types.ts

@@ -20,3 +20,11 @@ export interface LoginResponse {
     admin: AdminUser
   }
 }
+
+export interface ProfileResponse {
+  success: boolean
+  message: string
+  data: {
+    admin: AdminUser
+  }
+}