Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
/**
* Authentication Store (Zustand)
* Manages authentication state, user, and account information
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface User {
id: number;
email: string;
username: string;
role: string;
account?: {
id: number;
name: string;
slug: string;
credits: number;
status: string;
};
}
interface AuthState {
user: User | null;
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
loading: boolean;
// Actions
login: (email: string, password: string) => Promise<void>;
logout: () => void;
register: (data: any) => Promise<void>;
setUser: (user: User | null) => void;
setToken: (token: string | null) => void;
refreshToken: () => Promise<void>;
refreshUser: () => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist<AuthState>(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
loading: false, // Always start with loading false - will be set true only during login/register
login: async (email, password) => {
set({ loading: true });
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/login/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || 'Login failed');
}
// Store user and JWT tokens
set({
user: data.user,
token: data.tokens?.access || null,
refreshToken: data.tokens?.refresh || null,
isAuthenticated: true,
loading: false
});
} catch (error: any) {
// ALWAYS reset loading on error - critical to prevent stuck state
set({ loading: false });
throw new Error(error.message || 'Login failed');
} finally {
// Extra safety: ensure loading is ALWAYS false after login attempt completes
// This handles edge cases like network timeouts, browser crashes, etc.
const current = get();
if (current.loading) {
set({ loading: false });
}
}
},
logout: () => {
set({ user: null, token: null, refreshToken: null, isAuthenticated: false, loading: false });
},
register: async (registerData) => {
set({ loading: true });
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/register/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...registerData,
password_confirm: registerData.password, // Add password_confirm
}),
});
const data = await response.json();
if (!response.ok || !data.success) {
// Handle validation errors
const errorMessage = data.message ||
(data.errors && typeof data.errors === 'object'
? JSON.stringify(data.errors)
: data.errors) ||
'Registration failed';
throw new Error(errorMessage);
}
// Store user and JWT tokens
set({
user: data.user,
token: data.tokens?.access || null,
refreshToken: data.tokens?.refresh || null,
isAuthenticated: true,
loading: false
});
} catch (error: any) {
// ALWAYS reset loading on error - critical to prevent stuck state
set({ loading: false });
throw new Error(error.message || 'Registration failed');
} finally {
// Extra safety: ensure loading is ALWAYS false after register attempt completes
// This handles edge cases like network timeouts, browser crashes, etc.
const current = get();
if (current.loading) {
set({ loading: false });
}
}
},
setUser: (user) => {
set({ user, isAuthenticated: !!user });
},
setToken: (token) => {
set({ token });
},
refreshToken: async () => {
const state = get();
if (!state.refreshToken) {
throw new Error('No refresh token available');
}
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/refresh/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh: state.refreshToken }),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || 'Token refresh failed');
}
// Update access token
set({ token: data.access });
// Also refresh user data to get latest account/plan information
// This ensures account/plan changes are reflected immediately
try {
await get().refreshUser();
} catch (userRefreshError) {
// If user refresh fails, don't fail the token refresh
console.warn('Failed to refresh user data during token refresh:', userRefreshError);
}
} catch (error: any) {
// If refresh fails, logout user
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
throw new Error(error.message || 'Token refresh failed');
}
},
refreshUser: async () => {
const state = get();
if (!state.token && !state.isAuthenticated) {
throw new Error('Not authenticated');
}
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const token = state.token || getAuthToken();
const response = await fetch(`${API_BASE_URL}/v1/auth/me/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
},
credentials: 'include',
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || 'Failed to refresh user data');
}
// Update user data with latest from server
// This ensures account/plan changes are reflected immediately
set({ user: data.user });
} catch (error: any) {
// If refresh fails, don't logout - just log the error
// User might still be authenticated, just couldn't refresh data
console.warn('Failed to refresh user data:', error);
throw new Error(error.message || 'Failed to refresh user data');
}
},
}),
{
name: 'auth-storage',
// NEVER persist loading - it should always be false on app start
partialize: (state) => ({
user: state.user,
token: state.token,
refreshToken: state.refreshToken,
isAuthenticated: state.isAuthenticated,
// Explicitly exclude loading from persistence
}),
// On rehydration, ensure loading is always false
onRehydrateStorage: () => {
return (state, error) => {
// Always reset loading to false on rehydration - critical fix for stuck loading state
if (state) {
state.loading = false;
}
if (error) {
console.error('Auth store rehydration error:', error);
// If rehydration fails, ensure loading is false
if (state) {
state.loading = false;
}
}
};
},
}
)
);