profit-planet-frontend/src/app/store/authStore.ts
seaznCode 25fff9b1c3 feat: Implement user status management with custom hook
- Added `useUserStatus` hook to manage user status fetching and state.
- Integrated user status in Quick Action Dashboard and related pages.
- Enhanced error handling and loading states for user status.
- Updated profile completion and document upload flows to refresh user status after actions.
- Created a centralized API utility for handling requests and responses.
- Refactored authentication token management to use session storage.
2025-10-11 19:47:07 +02:00

237 lines
7.7 KiB
TypeScript

import { create } from "zustand";
import { log } from "../utils/logger";
// Helper to decode JWT and get expiry
function getTokenExpiry(token: string | null): Date | null {
if (!token) return null;
try {
const [, payload] = token.split(".");
const { exp } = JSON.parse(atob(payload));
return exp ? new Date(exp * 1000) : null;
} catch {
return null;
}
}
// Helper functions for sessionStorage with SSR safety
const getStoredUser = () => {
if (typeof window === 'undefined') return null; // SSR check
try {
const userData = sessionStorage.getItem('user');
const parsed = userData ? JSON.parse(userData) : null;
log("👤 Retrieved user from sessionStorage:", parsed ? `${parsed.email || parsed.companyName}` : null);
return parsed;
} catch (error) {
log("❌ Error retrieving user from sessionStorage:", error);
return null;
}
};
const getStoredToken = () => {
if (typeof window === 'undefined') return null; // SSR check
try {
const token = sessionStorage.getItem('accessToken');
if (token) {
const expiry = getTokenExpiry(token);
if (expiry && expiry.getTime() > Date.now()) {
log("🔑 Retrieved valid token from sessionStorage");
return token;
} else {
log("⏰ Stored token expired, removing");
sessionStorage.removeItem('accessToken');
return null;
}
}
return null;
} catch (error) {
log("❌ Error retrieving token from sessionStorage:", error);
return null;
}
};
interface User {
email?: string;
companyName?: string;
[key: string]: any;
}
interface AuthStore {
accessToken: string | null;
user: User | null;
isAuthReady: boolean;
isRefreshing: boolean;
refreshPromise: Promise<boolean> | null;
setAuthReady: (ready: boolean) => void;
setAccessToken: (token: string | null) => void;
setUser: (userData: User | null) => void;
clearAuth: () => void;
logout: () => Promise<void>;
refreshAuthToken: () => Promise<boolean | null>;
getAuthState: () => AuthStore;
}
const useAuthStore = create<AuthStore>((set, get) => ({
// Initialize with SSR-safe defaults
accessToken: typeof window !== 'undefined' ? getStoredToken() : null,
user: typeof window !== 'undefined' ? getStoredUser() : null,
isAuthReady: false,
isRefreshing: false,
refreshPromise: null,
setAuthReady: (ready) => {
log("🔔 Zustand: setAuthReady ->", ready);
set({ isAuthReady: !!ready });
},
setAccessToken: (token) => {
log("🔑 Zustand: Setting access token in memory:", token ? `${token.substring(0, 20)}...` : null);
if (typeof window !== 'undefined') {
try {
if (token) {
const expiry = getTokenExpiry(token);
log("⏳ Zustand: Token expiry:", expiry ? expiry.toLocaleString() : "Unknown");
sessionStorage.setItem('accessToken', token);
log("✅ Token stored in sessionStorage");
} else {
sessionStorage.removeItem('accessToken');
log("🗑️ Token removed from sessionStorage");
}
} catch (error) {
log("❌ Error storing token in sessionStorage:", error);
}
}
set({ accessToken: token });
},
setUser: (userData) => {
log("👤 Zustand: Setting user data:", userData ? `${userData.email || userData.companyName}` : null);
if (typeof window !== 'undefined') {
try {
if (userData) {
sessionStorage.setItem('user', JSON.stringify(userData));
log("✅ User data stored in sessionStorage successfully");
} else {
sessionStorage.removeItem('user');
log("🗑️ User data removed from sessionStorage");
}
} catch (error) {
log("❌ Error storing user in sessionStorage:", error);
}
}
set({ user: userData });
},
clearAuth: () => {
log("🧹 Zustand: Clearing all auth data from memory and removing persisted user");
if (typeof window !== 'undefined') {
try {
sessionStorage.removeItem('user');
log("✅ User cleared from sessionStorage");
sessionStorage.removeItem('accessToken');
log("✅ accessToken cleared from sessionStorage");
} catch (error) {
log("❌ Error clearing user/accessToken from sessionStorage:", error);
}
}
set({ accessToken: null, user: null });
},
logout: async () => {
log("🚪 Zustand: Logging out — revoking refresh token on server");
try {
const logoutUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/logout`;
log("🌐 Zustand: Calling logout endpoint:", logoutUrl);
const res = await fetch(logoutUrl, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
});
log("📡 Logout response status:", res.status);
try {
const body = await res.json().catch(() => null);
log("📦 Logout response body:", body);
} catch {}
// Attempt to clear refreshToken cookie client-side (will only work if not httpOnly)
if (typeof window !== 'undefined') {
document.cookie = "refreshToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=Strict";
}
} catch (error) {
log("❌ Error calling logout endpoint:", error);
} finally {
get().clearAuth();
get().setAuthReady(true);
}
},
refreshAuthToken: async () => {
// If there's already a refresh in flight, return that promise
if (get().refreshPromise) {
log("🔁 Zustand: refreshAuthToken - returning existing refresh promise");
return get().refreshPromise;
}
// SHORT-CIRCUIT: if we already have a valid accessToken that's not about to expire, skip refresh
const currentToken = get().accessToken;
if (currentToken) {
const expiry = getTokenExpiry(currentToken);
if (expiry && expiry.getTime() - Date.now() > 60 * 1000) { // more than 60s left
log("⏸️ Zustand: accessToken present and valid, skipping refresh");
return Promise.resolve(true);
}
}
log("🔄 Zustand: refreshAuthToken - starting new refresh");
// create promise so concurrent callers can await it
const p = (async () => {
set({ isRefreshing: true });
try {
const refreshUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/refresh`;
log("🌐 Zustand: Calling refresh endpoint:", refreshUrl);
const res = await fetch(refreshUrl, {
method: "POST",
credentials: "include"
});
log("📡 Zustand: Refresh response status:", res.status);
const body = await res.json().catch(() => null);
log("📦 Zustand: Refresh response body:", body);
if (res.ok && body && body.accessToken) {
log("✅ Zustand: Refresh succeeded, setting in-memory token and user");
get().setAccessToken(body.accessToken);
if (body.user) get().setUser(body.user);
return true;
} else {
log("❌ Zustand: Refresh failed (no accessToken or non-ok). Clearing auth state");
get().clearAuth();
return false;
}
} catch (error) {
log("❌ Zustand: Refresh error:", error);
get().clearAuth();
return false;
} finally {
set({ isRefreshing: false, refreshPromise: null });
log("🔔 Zustand: refreshAuthToken - finished (flags cleared)");
}
})();
set({ refreshPromise: p });
return p;
},
getAuthState: () => {
const state = get();
log("📊 Current auth state:", {
hasToken: !!state.accessToken,
tokenPrefix: state.accessToken ? `${state.accessToken.substring(0, 20)}...` : null,
user: state.user ? `${state.user.email || state.user.companyName}` : null
});
return state;
}
}));
export default useAuthStore;