diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 42c1b1d..131b08a 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -544,9 +544,9 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "ff" diff --git a/frontend/lib/store/authStore.ts b/frontend/lib/store/authStore.ts new file mode 100644 index 0000000..c73b2c1 --- /dev/null +++ b/frontend/lib/store/authStore.ts @@ -0,0 +1,105 @@ +import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; + +export interface AdminUser { + id: string; + email: string; + firstName: string; + lastName: string; + role: string; + profilePicture?: string; +} + +interface AuthState { + user: AdminUser | null; + token: string | null; + isAuthenticated: boolean; + isLoading: boolean; + _hasHydrated: boolean; + + setAuth: (user: AdminUser, token: string) => void; + clearAuth: () => void; + setLoading: (loading: boolean) => void; + updateUser: (user: Partial) => void; + setHasHydrated: (hydrated: boolean) => void; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + _hasHydrated: false, + + setAuth: (user, token) => { + set({ + user, + token, + isAuthenticated: true, + isLoading: false, + }); + }, + + clearAuth: () => { + set({ + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + }); + }, + + setLoading: (loading) => { + set({ isLoading: loading }); + }, + + updateUser: (updates) => { + set((state) => ({ + user: state.user ? { ...state.user, ...updates } : null, + })); + }, + + setHasHydrated: (hydrated) => { + set({ _hasHydrated: hydrated }); + }, + }), + { + name: "admin-auth", + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ + user: state.user, + token: state.token, + isAuthenticated: state.isAuthenticated, + }), + onRehydrateStorage: () => { + return (state, error) => { + if (!error) { + state?.setHasHydrated(true); + } + }; + }, + }, + ), +); + +export const useAuthRehydrated = () => + useAuthStore((state) => state._hasHydrated); + +export const useAuthState = () => + useAuthStore((state) => ({ + user: state.user, + token: state.token, + isAuthenticated: state.isAuthenticated, + isLoading: state.isLoading, + _hasHydrated: state._hasHydrated, + })); + +export const useAuthActions = () => + useAuthStore((state) => ({ + setAuth: state.setAuth, + clearAuth: state.clearAuth, + setLoading: state.setLoading, + updateUser: state.updateUser, + })); diff --git a/frontend/providers/ProtectedRoute.tsx b/frontend/providers/ProtectedRoute.tsx new file mode 100644 index 0000000..af92e02 --- /dev/null +++ b/frontend/providers/ProtectedRoute.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuthStore, useAuthRehydrated } from "@/lib/store/authStore"; + +function SkeletonLoader() { + return ( +
+
+
+
+
+
+
+ ); +} + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export default function ProtectedRoute({ children }: ProtectedRouteProps) { + const router = useRouter(); + const hasHydrated = useAuthRehydrated(); + const { isAuthenticated, isLoading } = useAuthStore((state) => ({ + isAuthenticated: state.isAuthenticated, + isLoading: state.isLoading, + })); + + useEffect(() => { + if (hasHydrated && !isLoading && !isAuthenticated) { + router.push("/login"); + } + }, [hasHydrated, isLoading, isAuthenticated, router]); + + if (!hasHydrated || isLoading) { + return ; + } + + if (!isAuthenticated) { + return null; + } + + return <>{children}; +}