221 lines
5.5 KiB
TypeScript
221 lines
5.5 KiB
TypeScript
/**
|
|
* 🔐 Auth Store (Zustand)
|
|
* Gerenciador de estado de autenticação
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import apiClient, { handleApiError } from '../api';
|
|
import {
|
|
AuthState,
|
|
AuthUser,
|
|
SignupPayload,
|
|
LoginPayload,
|
|
AuthResponse,
|
|
} from '../types';
|
|
|
|
interface AuthStore extends AuthState {
|
|
// ações
|
|
signup: (payload: SignupPayload) => Promise<void>;
|
|
login: (payload: LoginPayload) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
getProfile: () => Promise<AuthUser | null>;
|
|
setError: (error: string | null) => void;
|
|
setLoading: (loading: boolean) => void;
|
|
}
|
|
|
|
/**
|
|
* Decodificar JWT para pegar o userId
|
|
*/
|
|
const decodeToken = (token: string): { userId: string } | null => {
|
|
try {
|
|
const base64Url = token.split('.')[1];
|
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
const jsonPayload = decodeURIComponent(
|
|
atob(base64)
|
|
.split('')
|
|
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
.join(''),
|
|
);
|
|
return JSON.parse(jsonPayload);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Criar store de autenticação
|
|
*/
|
|
export const useAuthStore = create<AuthStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// Estado inicial
|
|
user: null,
|
|
token: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
// ============================================================================
|
|
// AÇÕES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Registrar novo usuário
|
|
*/
|
|
signup: async (payload: SignupPayload) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
|
|
const response = await apiClient.post<AuthResponse>(
|
|
'/auth/signup',
|
|
payload,
|
|
);
|
|
const { access_token, user } = response.data;
|
|
|
|
set({
|
|
token: access_token,
|
|
user,
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
});
|
|
|
|
// Salvar token no localStorage
|
|
localStorage.setItem('auth_token', access_token);
|
|
localStorage.setItem('auth_user', JSON.stringify(user));
|
|
} catch (error) {
|
|
const message = handleApiError(error);
|
|
set({ error: message, isLoading: false });
|
|
throw new Error(message);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fazer login
|
|
*/
|
|
login: async (payload: LoginPayload) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
|
|
const response = await apiClient.post<AuthResponse>(
|
|
'/auth/login',
|
|
payload,
|
|
);
|
|
const { access_token, user } = response.data;
|
|
|
|
set({
|
|
token: access_token,
|
|
user,
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
});
|
|
|
|
// Salvar no localStorage
|
|
localStorage.setItem('auth_token', access_token);
|
|
localStorage.setItem('auth_user', JSON.stringify(user));
|
|
} catch (error) {
|
|
const message = handleApiError(error);
|
|
set({ error: message, isLoading: false });
|
|
throw new Error(message);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fazer logout
|
|
*/
|
|
logout: async () => {
|
|
try {
|
|
set({ isLoading: true });
|
|
// Chamar endpoint de logout (opcional)
|
|
await apiClient.post('/auth/logout');
|
|
} catch (error) {
|
|
console.error('Erro ao fazer logout:', error);
|
|
} finally {
|
|
// Limpar estado e localStorage
|
|
set({
|
|
user: null,
|
|
token: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
localStorage.removeItem('auth_token');
|
|
localStorage.removeItem('auth_user');
|
|
localStorage.removeItem('auth_store');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Obter perfil do usuário
|
|
*/
|
|
getProfile: async () => {
|
|
try {
|
|
const response = await apiClient.get<AuthUser>('/auth/me');
|
|
const user = response.data;
|
|
|
|
set({ user, isAuthenticated: true });
|
|
localStorage.setItem('auth_user', JSON.stringify(user));
|
|
|
|
return user;
|
|
} catch (error) {
|
|
const message = handleApiError(error);
|
|
set({ error: message, isAuthenticated: false });
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Setar erro
|
|
*/
|
|
setError: (error: string | null) => {
|
|
set({ error });
|
|
},
|
|
|
|
/**
|
|
* Setar loading
|
|
*/
|
|
setLoading: (isLoading: boolean) => {
|
|
set({ isLoading });
|
|
},
|
|
}),
|
|
|
|
// Configurar persistência
|
|
{
|
|
name: 'auth_store',
|
|
partialize: (state) => ({
|
|
token: state.token,
|
|
user: state.user,
|
|
isAuthenticated: state.isAuthenticated,
|
|
}),
|
|
|
|
// Validar token ao recuperar do storage
|
|
onRehydrateStorage: () => (state) => {
|
|
if (state?.token) {
|
|
const decoded = decodeToken(state.token);
|
|
if (!decoded) {
|
|
state.token = null;
|
|
state.isAuthenticated = false;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
// Exportar hook separado para debugging
|
|
export const useAuth = () => {
|
|
const store = useAuthStore();
|
|
return {
|
|
user: store.user,
|
|
token: store.token,
|
|
isAuthenticated: store.isAuthenticated,
|
|
isLoading: store.isLoading,
|
|
error: store.error,
|
|
login: store.login,
|
|
signup: store.signup,
|
|
logout: store.logout,
|
|
getProfile: store.getProfile,
|
|
};
|
|
};
|