Upload source code
This commit is contained in:
151
src/client/context/AuthContext.tsx
Normal file
151
src/client/context/AuthContext.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useUser, useOrganization } from '@clerk/clerk-react';
|
||||
import type { AppUser } from '../types';
|
||||
import { AuthContext } from './AuthContextType';
|
||||
import { setApiClerkUserId, setApiOrganizationId, getBaseUrl } from '../services/api';
|
||||
|
||||
const API_URL = getBaseUrl();
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const { user, isLoaded } = useUser();
|
||||
const { organization, membership } = useOrganization();
|
||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const lastContextRef = useRef<{ clerkId?: string, orgId?: string | null }>({});
|
||||
|
||||
// Set the clerk user ID and organization ID for the API interceptor
|
||||
useEffect(() => {
|
||||
setApiClerkUserId(user?.id || null);
|
||||
setApiOrganizationId(organization?.id || null);
|
||||
}, [user?.id, organization?.id]);
|
||||
|
||||
const syncUser = useCallback(async () => {
|
||||
if (!user) {
|
||||
setAppUser(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Only set loading if the context has changed (new user or new organization)
|
||||
// This prevents unmounting/remounting components on window focus revalidations
|
||||
const isSameContext =
|
||||
lastContextRef.current.clerkId === user.id &&
|
||||
lastContextRef.current.orgId === (organization?.id || null);
|
||||
|
||||
if (!isSameContext) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
// Sync user with backend, including organization context
|
||||
const response = await fetch(`${API_URL}/users/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-clerk-user-id': user.id,
|
||||
...(organization?.id && { 'x-organization-id': organization.id }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clerkId: user.id,
|
||||
email: user.primaryEmailAddress?.emailAddress || '',
|
||||
name: user.fullName || user.firstName || 'Usuário',
|
||||
organizationId: organization?.id || null,
|
||||
clerkRole: membership?.role || null, // org:admin, org:member, etc.
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
if (response.status === 403 && data.error?.includes('bloqueada')) {
|
||||
setError('Sua conta foi bloqueada. Entre em contato com o administrador.');
|
||||
setAppUser(null);
|
||||
return;
|
||||
}
|
||||
throw new Error('Falha ao sincronizar usuário');
|
||||
}
|
||||
|
||||
const syncedUser = await response.json();
|
||||
// Use organizationRole if available (per-org role), otherwise fall back to global role
|
||||
const effectiveRole = syncedUser.organizationRole || syncedUser.role || 'guest';
|
||||
setAppUser({
|
||||
...syncedUser,
|
||||
id: syncedUser._id || syncedUser.id,
|
||||
role: effectiveRole, // Override with organization-specific role
|
||||
});
|
||||
|
||||
// Update last context ref
|
||||
lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null };
|
||||
} catch (err) {
|
||||
console.error('Error syncing user:', err);
|
||||
setError('Erro ao carregar dados do usuário');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user, organization?.id, membership?.role]);
|
||||
|
||||
const refetchUser = useCallback(async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/users/me`, {
|
||||
headers: {
|
||||
'x-clerk-user-id': user.id,
|
||||
...(organization?.id && { 'x-organization-id': organization.id }),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
const effectiveRole = userData.organizationRole || userData.role || 'guest';
|
||||
setAppUser({
|
||||
...userData,
|
||||
id: userData._id || userData.id,
|
||||
role: effectiveRole,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refetching user:', err);
|
||||
}
|
||||
}, [user, organization?.id]);
|
||||
|
||||
// Re-sync when organization changes
|
||||
useEffect(() => {
|
||||
if (isLoaded && user) {
|
||||
syncUser();
|
||||
}
|
||||
}, [isLoaded, user, organization?.id, syncUser]);
|
||||
|
||||
const isDeveloper = useCallback(() => {
|
||||
return user?.primaryEmailAddress?.emailAddress === 'admtracksteel@gmail.com';
|
||||
}, [user]);
|
||||
|
||||
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
|
||||
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
|
||||
const isGuest = useCallback(() => appUser?.role === 'guest' && !isDeveloper(), [appUser, isDeveloper]);
|
||||
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser?.role !== undefined) || isDeveloper(), [appUser, isDeveloper]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
appUser,
|
||||
isLoading,
|
||||
isSignedIn: !!user,
|
||||
error,
|
||||
isAdmin,
|
||||
isUser,
|
||||
isGuest,
|
||||
isDeveloper,
|
||||
canEdit,
|
||||
refetchUser,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
17
src/client/context/AuthContextType.ts
Normal file
17
src/client/context/AuthContextType.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createContext } from 'react';
|
||||
import type { AppUser } from '../types';
|
||||
|
||||
export interface AuthContextType {
|
||||
appUser: AppUser | null;
|
||||
isLoading: boolean;
|
||||
isSignedIn: boolean;
|
||||
error: string | null;
|
||||
isAdmin: () => boolean;
|
||||
isUser: () => boolean;
|
||||
isGuest: () => boolean;
|
||||
isDeveloper: () => boolean;
|
||||
canEdit: () => boolean;
|
||||
refetchUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
56
src/client/context/SystemSettingsContext.tsx
Normal file
56
src/client/context/SystemSettingsContext.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { systemSettingsService } from '../services/systemSettingsService';
|
||||
import type { SystemSettings } from '../services/systemSettingsService';
|
||||
|
||||
interface SystemSettingsContextType {
|
||||
settings: SystemSettings | null;
|
||||
isLoading: boolean;
|
||||
updateSettings: (newSettings: Partial<SystemSettings>) => Promise<void>;
|
||||
}
|
||||
|
||||
const SystemSettingsContext = createContext<SystemSettingsContextType | undefined>(undefined);
|
||||
|
||||
export const SystemSettingsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [settings, setSettings] = useState<SystemSettings | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const fetchSettings = useCallback(async () => {
|
||||
try {
|
||||
const data = await systemSettingsService.getSettings();
|
||||
setSettings(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load system settings:', error);
|
||||
// Set defaults if fetch fails
|
||||
setSettings({
|
||||
settingsId: 'global',
|
||||
appName: 'GPI',
|
||||
appSubtitle: 'Gestão de Pintura Industrial'
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateSettingsValue = async (newSettings: Partial<SystemSettings>) => {
|
||||
const updated = await systemSettingsService.updateSettings(newSettings);
|
||||
setSettings(updated);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, [fetchSettings]);
|
||||
|
||||
return (
|
||||
<SystemSettingsContext.Provider value={{ settings, isLoading, updateSettings: updateSettingsValue }}>
|
||||
{children}
|
||||
</SystemSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSystemSettings = () => {
|
||||
const context = useContext(SystemSettingsContext);
|
||||
if (!context) {
|
||||
throw new Error('useSystemSettings must be used within a SystemSettingsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
22
src/client/context/ToastContext.tsx
Normal file
22
src/client/context/ToastContext.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'warning' | 'error' | 'success' | 'info';
|
||||
}
|
||||
|
||||
export interface ToastContextType {
|
||||
showToast: (message: string, type?: Toast['type']) => void;
|
||||
showGuestWarning: () => void;
|
||||
}
|
||||
|
||||
export const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
10
src/client/context/useAuth.ts
Normal file
10
src/client/context/useAuth.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { AuthContext } from './AuthContextType';
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user