Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/hooks/__tests__/useAuth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { renderHook, waitFor } from "@testing-library/react";
import { useWalletStore } from "@/store/walletStore";
import { useAuth } from "../useAuth";

jest.mock("@/store/walletStore", () => ({
useWalletStore: jest.fn(),
}));

const mockUseWalletStore = useWalletStore as jest.MockedFunction<
typeof useWalletStore
>;

const connectedAddress = "0x1234567890123456789012345678901234567890";

const setWalletState = (
overrides: Partial<ReturnType<typeof useWalletStore>> = {},
) => {
mockUseWalletStore.mockReturnValue({
address: null,
isConnected: false,
...overrides,
} as ReturnType<typeof useWalletStore>);
};

describe("useAuth", () => {
beforeEach(() => {
jest.clearAllMocks();
document.cookie = "";
setWalletState();
});

it("reports an unauthenticated state with no connected wallet and no cookie", async () => {
const { result } = renderHook(() => useAuth());

await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.isAuthenticated).toBe(false);
expect(result.current.userAddress).toBeNull();
});

it("does not authenticate from cookie presence alone", async () => {
document.cookie = "auth-token=garbage-token";
const { result } = renderHook(() => useAuth());

await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.isAuthenticated).toBe(false);
expect(result.current.userAddress).toBeNull();
});

it("does not authenticate from an expired or invalid cookie payload", async () => {
document.cookie = "auth-token=eyJhbGciOiJIUzI1NiJ9.invalid-payload";
const { result } = renderHook(() => useAuth());

await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.isAuthenticated).toBe(false);
expect(result.current.userAddress).toBeNull();
});

it("authenticates with the connected wallet address", async () => {
setWalletState({ address: connectedAddress, isConnected: true });
const { result } = renderHook(() => useAuth());

await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.isAuthenticated).toBe(true);
expect(result.current.userAddress).toBe(connectedAddress);
expect(result.current.userAddress).not.toBe("0x...");
});

it("does not authenticate when the wallet has an address but is disconnected", async () => {
setWalletState({ address: connectedAddress, isConnected: false });
const { result } = renderHook(() => useAuth());

await waitFor(() => expect(result.current.isLoading).toBe(false));

expect(result.current.isAuthenticated).toBe(false);
expect(result.current.userAddress).toBeNull();
});
});
68 changes: 16 additions & 52 deletions src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';
"use client";

import { useState, useEffect } from 'react';
import { useEffect, useState } from "react";
import { useWalletStore } from "@/store/walletStore";

const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minutes
const WARN_BEFORE_MS = 5 * 60 * 1000; // warn 5 minutes before expiry
const WARN_BEFORE_MS = 5 * 60 * 1000;

interface AuthState {
isAuthenticated: boolean;
Expand All @@ -13,56 +13,20 @@ interface AuthState {
}

export function useAuth() {
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
isLoading: true,
userAddress: null,
sessionExpiresAt: null,
});
const { address, isConnected } = useWalletStore();
const [isHydrated, setIsHydrated] = useState(false);

useEffect(() => {
const checkAuth = async () => {
try {
const tokenMatch = document.cookie.match(/auth-token=([^;]+)/);
const token = tokenMatch ? tokenMatch[1] : null;
let isValid = false;
let expiresAt = null;
let address = null;

if (token) {
try {
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
if (payload.exp && payload.exp * 1000 > Date.now()) {
isValid = true;
expiresAt = payload.exp * 1000;
address = payload.address || '0x...';
}
}
} catch (e) {
// Invalid token format
}
}

setAuthState({
isAuthenticated: isValid,
isLoading: false,
userAddress: address,
sessionExpiresAt: expiresAt,
});
} catch {
setAuthState({
isAuthenticated: false,
isLoading: false,
userAddress: null,
sessionExpiresAt: null,
});
}
};

checkAuth();
setIsHydrated(true);
}, []);

return { ...authState, WARN_BEFORE_MS };
const hasVerifiedWallet = isHydrated && isConnected && Boolean(address);

return {
isAuthenticated: hasVerifiedWallet,
isLoading: !isHydrated,
userAddress: hasVerifiedWallet ? address : null,
sessionExpiresAt: null,
WARN_BEFORE_MS,
} satisfies AuthState & { WARN_BEFORE_MS: number };
}
48 changes: 1 addition & 47 deletions useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1 @@
'use client';

import { useState, useEffect } from 'react';
import { useWalletConnector } from './useWalletConnector';

interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
userAddress: string | null;
}

export function useAuth() {
const { connectWallet } = useWalletConnector();
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
isLoading: true,
userAddress: null,
});

useEffect(() => {
// Check for existing session/token on mount
const checkAuth = async () => {
try {
// In a real Web3 app, we'd check if the wallet is still connected
// and if a valid session token exists in cookies
const hasToken = document.cookie.includes('auth-token=');

// Mocking check - in production, validate JWT or wallet state here
setAuthState({
isAuthenticated: hasToken,
isLoading: false,
userAddress: hasToken ? '0x...' : null, // Get from wallet provider
});
} catch (error) {
setAuthState({
isAuthenticated: false,
isLoading: false,
userAddress: null,
});
}
};

checkAuth();
}, []);

return authState;
}
export { useAuth } from "./src/hooks/useAuth";
Loading