Migração completa para Logto - Remoção de Clerk finalizada
This commit is contained in:
101
package-lock.json
generated
101
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.61.4",
|
||||
"@logto/node": "^2.4.0",
|
||||
"@logto/react": "^4.0.13",
|
||||
"@supabase/supabase-js": "^2.47.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/uuid": "^10.0.0",
|
||||
@@ -2276,6 +2277,93 @@
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser": {
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@logto/browser/-/browser-3.0.12.tgz",
|
||||
"integrity": "sha512-Ec45IExLYS64bF22wS7dZuWgOMmC2w3FZmWWnVCv2fX2vKQVs0wiI+FE/PlNhEvi8up4AW0zHO4NTGwF7ipFsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logto/client": "^3.1.7",
|
||||
"@silverhand/essentials": "^2.9.3",
|
||||
"js-base64": "^3.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser/node_modules/@logto/client": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@logto/client/-/client-3.1.7.tgz",
|
||||
"integrity": "sha512-t/5wXMhiXtmbmP6Cmcl4uMsYetq21vSZuYZztPHXv6QX0dx7lSKBvYi/65ERoS+fmNmtV2/i4Ojf1U41o0TLPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logto/js": "^6.1.1",
|
||||
"@silverhand/essentials": "^2.9.3",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"jose": "^5.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser/node_modules/@logto/js": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@logto/js/-/js-6.1.1.tgz",
|
||||
"integrity": "sha512-G0lRS7VyOXdB06WYajEh9Kq2E3m11JshiKIKLj6LRPI1qZ06JYQ+Jsej3K60/4OIZMSzUas4FVnY+ORrhDdktA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@silverhand/essentials": "^2.9.3",
|
||||
"camelcase-keys": "^9.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser/node_modules/camelcase": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
|
||||
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser/node_modules/camelcase-keys": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz",
|
||||
"integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelcase": "^8.0.0",
|
||||
"map-obj": "5.0.0",
|
||||
"quick-lru": "^6.1.1",
|
||||
"type-fest": "^4.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser/node_modules/map-obj": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz",
|
||||
"integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/browser/node_modules/quick-lru": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
|
||||
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/client": {
|
||||
"version": "2.8.1",
|
||||
"license": "MIT",
|
||||
@@ -2303,6 +2391,19 @@
|
||||
"js-base64": "^3.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@logto/react": {
|
||||
"version": "4.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@logto/react/-/react-4.0.13.tgz",
|
||||
"integrity": "sha512-CU4rjJmueY0CQoJZq7BDZt/9sQYpxKDwVBrGHR55ljl4zPFF2URJPixqCtEEfWq5/pFk7MEnIOePOYbj7BWKfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logto/browser": "^3.0.12",
|
||||
"@silverhand/essentials": "^2.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"start": "tsx src/server/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsx": "^4.21.0",
|
||||
"@clerk/clerk-react": "^5.61.4",
|
||||
"@logto/node": "^2.4.0",
|
||||
"@logto/react": "^4.0.13",
|
||||
"@supabase/supabase-js": "^2.47.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/uuid": "^10.0.0",
|
||||
@@ -47,6 +47,7 @@
|
||||
"serverless-http": "^4.0.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tesseract.js": "^7.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CalculatorDashboard } from './pages/CalculatorDashboard';
|
||||
import { StockDashboard } from './pages/StockDashboard';
|
||||
import { GuestDashboard } from './pages/GuestDashboard';
|
||||
import { Login } from './pages/Login';
|
||||
import { Callback } from './pages/Callback';
|
||||
import InstrumentList from './pages/InstrumentList';
|
||||
|
||||
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
@@ -38,6 +39,8 @@ const AppContent: React.FC = () => {
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<ProjectList />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
||||
<Route path="/projects" element={<ProjectList />} />
|
||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||
|
||||
@@ -2,13 +2,11 @@ import React, { useState } from 'react';
|
||||
import NotificationBell from './NotificationBell';
|
||||
import { TeamPresence } from './TeamPresence';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer } from 'lucide-react';
|
||||
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer, User } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useClerk, UserButton, useUser, OrganizationSwitcher, useOrganization } from '@clerk/clerk-react';
|
||||
import { useLogto } from '@logto/react';
|
||||
import { TechnicalManual } from './TechnicalManual';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||
import { setApiOrgData } from '../services/api';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -22,21 +20,8 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
});
|
||||
const location = useLocation();
|
||||
const { signOut } = useClerk();
|
||||
const { user } = useUser();
|
||||
const { organization } = useOrganization();
|
||||
const { isAdmin, isUser, isDeveloper, appUser } = useAuth();
|
||||
// const { settings } = useSystemSettings();
|
||||
|
||||
// Sync Organization ID with API client
|
||||
React.useEffect(() => {
|
||||
if (organization?.id) {
|
||||
|
||||
setApiOrgData(organization.id, organization.name);
|
||||
} else {
|
||||
setApiOrgData(null);
|
||||
}
|
||||
}, [organization]);
|
||||
const { signOut } = useLogto();
|
||||
const { isAdmin, isUser, isDeveloper, appUser, isSignedIn } = useAuth();
|
||||
|
||||
// Helper to get role display name
|
||||
const getRoleDisplay = () => {
|
||||
@@ -69,6 +54,14 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
}
|
||||
}, [appUser, location.pathname, navigate]);
|
||||
|
||||
// Redirect to login if not signed in (except for login and callback pages)
|
||||
React.useEffect(() => {
|
||||
const publicPaths = ['/login', '/callback'];
|
||||
if (!isSignedIn && !publicPaths.includes(location.pathname)) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [isSignedIn, location.pathname, navigate]);
|
||||
|
||||
interface NavItem {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
@@ -94,6 +87,14 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
signOut(window.location.origin);
|
||||
};
|
||||
|
||||
if (location.pathname === '/login' || location.pathname === '/callback') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-surface-soft flex font-sans selection:bg-primary/30">
|
||||
{/* Sidebar Desktop - Fixed */}
|
||||
@@ -117,47 +118,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 mb-2">
|
||||
{isAdmin() ? (
|
||||
<OrganizationSwitcher
|
||||
hidePersonal={true}
|
||||
afterSelectOrganizationUrl="/"
|
||||
afterCreateOrganizationUrl="/"
|
||||
afterLeaveOrganizationUrl="/"
|
||||
appearance={{
|
||||
elements: {
|
||||
rootBox: "w-full",
|
||||
organizationSwitcherTrigger: "w-full justify-between bg-surface-hover/50 hover:bg-surface-hover p-2 rounded-xl border border-border/50 text-text-main transition-all",
|
||||
organizationPreviewTextContainer: "text-text-main",
|
||||
organizationPreviewMainIdentifier: "text-text-main font-semibold",
|
||||
organizationSwitcherPopoverCard: "bg-surface border border-border/40 shadow-2xl",
|
||||
organizationSwitcherPopoverActions: "bg-surface-soft/50",
|
||||
organizationSwitcherPopoverActionButton: "text-text-main hover:bg-surface-hover transition-colors",
|
||||
organizationPreview: "hover:bg-surface-hover cursor-pointer transition-colors px-4 py-3",
|
||||
organizationPreviewSecondaryIdentifier: "text-text-muted",
|
||||
organizationSwitcherPopoverFooter: "hidden",
|
||||
userPreviewMainIdentifier: "text-text-main font-bold",
|
||||
userPreviewSecondaryIdentifier: "text-text-muted",
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full flex items-center gap-3 p-2 rounded-xl border border-border/50 bg-surface-hover/50 text-text-main opacity-80 cursor-default" title="Apenas visualização">
|
||||
{organization?.imageUrl ? (
|
||||
<img src={organization.imageUrl} alt={organization.name} className="w-8 h-8 rounded-lg object-cover bg-surface-soft" />
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs">
|
||||
{organization?.name?.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold truncate">{organization?.name || 'Carregando...'}</p>
|
||||
<p className="text-[10px] text-text-muted uppercase tracking-wider">Organização</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Team Presence - Shows all members with online/offline status */}
|
||||
<TeamPresence />
|
||||
|
||||
@@ -257,7 +217,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:text-error hover:bg-error/5 transition-all w-full"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
@@ -268,9 +228,11 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
<div className="w-px h-6 bg-border/50 mx-1"></div>
|
||||
<UserButton afterSignOutUrl="/" />
|
||||
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
||||
<User size={16} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{user?.firstName || 'Usuário'}</span>
|
||||
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{appUser?.name || 'Usuário'}</span>
|
||||
<span className="text-[8px] text-text-muted">v2.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,6 +348,13 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
{isDarkMode ? <Sun size={20} className="text-yellow-500" /> : <Moon size={20} className="text-primary" />}
|
||||
{isDarkMode ? 'Modo Claro' : 'Modo Escuro'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-4 w-full text-text-main font-bold"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -415,4 +384,3 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
requireEdit = false,
|
||||
redirectTo = '/',
|
||||
}) => {
|
||||
const { appUser, isLoading, canEdit } = useAuth();
|
||||
const { appUser, isLoading, canEdit, isSignedIn } = useAuth();
|
||||
|
||||
// Show loading state
|
||||
if (isLoading) {
|
||||
@@ -34,6 +34,11 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
if (!isSignedIn) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// Check role-based access
|
||||
if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) {
|
||||
return <Navigate to={redirectTo} replace />;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import { usePresence } from '../hooks/usePresence';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
import { useOrganization } from '@clerk/clerk-react';
|
||||
import { SendMessageModal } from './SendMessageModal';
|
||||
import api from '../services/api';
|
||||
|
||||
interface OrganizationMember {
|
||||
_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
clerkUserId: string;
|
||||
logto_id: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
@@ -25,69 +25,50 @@ interface PendingMessage {
|
||||
export const TeamPresence: React.FC = () => {
|
||||
const { activeUsers } = usePresence();
|
||||
const { appUser } = useAuth();
|
||||
const { organization } = useOrganization();
|
||||
const [allMembers, setAllMembers] = React.useState<OrganizationMember[]>([]);
|
||||
const [pendingMessages, setPendingMessages] = React.useState<PendingMessage[]>([]);
|
||||
const [selectedUser, setSelectedUser] = React.useState<{ id: string; name: string } | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
|
||||
console.log('TeamPresence rendered');
|
||||
console.log('appUser:', appUser);
|
||||
console.log('organization:', organization);
|
||||
console.log('activeUsers:', activeUsers);
|
||||
console.log('allMembers:', allMembers);
|
||||
|
||||
// Fetch all organization members
|
||||
React.useEffect(() => {
|
||||
const fetchMembers = async () => {
|
||||
console.log('Fetching members...');
|
||||
// Fetch all members
|
||||
const fetchMembers = useCallback(async () => {
|
||||
try {
|
||||
const response = await api.get<OrganizationMember[]>('/users');
|
||||
console.log('Members fetched:', response.data);
|
||||
setAllMembers(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching members:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (organization?.id) {
|
||||
console.log('Organization ID exists, fetching members');
|
||||
fetchMembers();
|
||||
// Refresh every minute
|
||||
const interval = setInterval(fetchMembers, 60000);
|
||||
return () => clearInterval(interval);
|
||||
} else {
|
||||
console.log('No organization ID, skipping fetch');
|
||||
}
|
||||
}, [organization?.id]);
|
||||
}, []);
|
||||
|
||||
// Fetch pending messages
|
||||
React.useEffect(() => {
|
||||
const fetchPendingMessages = async () => {
|
||||
const fetchPendingMessages = useCallback(async () => {
|
||||
try {
|
||||
const response = await api.get<PendingMessage[]>('/messages/pending');
|
||||
setPendingMessages(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pending messages:', error);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (organization?.id) {
|
||||
React.useEffect(() => {
|
||||
fetchMembers();
|
||||
fetchPendingMessages();
|
||||
const interval = setInterval(fetchPendingMessages, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [organization?.id]);
|
||||
|
||||
console.log('Rendering with allMembers.length:', allMembers.length);
|
||||
const memberInterval = setInterval(fetchMembers, 60000);
|
||||
const messageInterval = setInterval(fetchPendingMessages, 30000);
|
||||
|
||||
return () => {
|
||||
clearInterval(memberInterval);
|
||||
clearInterval(messageInterval);
|
||||
};
|
||||
}, [fetchMembers, fetchPendingMessages]);
|
||||
|
||||
if (allMembers.length === 0) {
|
||||
console.log('No members, returning null');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a Set of active user IDs for fast lookup
|
||||
const activeUserIds = new Set(activeUsers.map(u => u.clerkId));
|
||||
const activeUserLogtoIds = new Set(activeUsers.map(u => u.logtoId));
|
||||
|
||||
// Create a map of pending messages by recipient ID
|
||||
const pendingMessagesByRecipient = new Map(
|
||||
@@ -95,10 +76,10 @@ export const TeamPresence: React.FC = () => {
|
||||
);
|
||||
|
||||
const handleMemberClick = (member: OrganizationMember) => {
|
||||
if (member.clerkUserId === appUser?.clerkId) {
|
||||
if (member.logto_id === appUser?.logtoId) {
|
||||
return; // Don't allow messaging yourself
|
||||
}
|
||||
setSelectedUser({ id: member.clerkUserId, name: member.name });
|
||||
setSelectedUser({ id: member.logto_id, name: member.name });
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -108,13 +89,7 @@ export const TeamPresence: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMessageSent = async () => {
|
||||
// Refresh pending messages
|
||||
try {
|
||||
const response = await api.get<PendingMessage[]>('/messages/pending');
|
||||
setPendingMessages(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error refreshing pending messages:', error);
|
||||
}
|
||||
await fetchPendingMessages();
|
||||
};
|
||||
|
||||
const getExistingMessage = (member: OrganizationMember) => {
|
||||
@@ -132,8 +107,8 @@ export const TeamPresence: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allMembers.map((member) => {
|
||||
const isOnline = activeUserIds.has(member.clerkUserId);
|
||||
const isCurrentUser = member.clerkUserId === appUser?.clerkId;
|
||||
const isOnline = activeUserLogtoIds.has(member.logto_id);
|
||||
const isCurrentUser = member.logto_id === appUser?.logtoId;
|
||||
const hasPendingMessage = pendingMessagesByRecipient.has(member.email);
|
||||
|
||||
return (
|
||||
@@ -201,10 +176,13 @@ export const TeamPresence: React.FC = () => {
|
||||
onClose={handleModalClose}
|
||||
recipientId={selectedUser.id}
|
||||
recipientName={selectedUser.name}
|
||||
existingMessage={getExistingMessage(allMembers.find(m => m.clerkUserId === selectedUser.id)!)}
|
||||
existingMessage={getExistingMessage(allMembers.find(m => m.logto_id === selectedUser.id)!)}
|
||||
onMessageSent={handleMessageSent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// No the component body I used useCallback so I need to import it
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Download, Upload, AlertTriangle, CheckCircle, Database, FileJson, Info, RefreshCw } from 'lucide-react';
|
||||
import api from '../../services/api';
|
||||
import { useOrganization } from '@clerk/clerk-react';
|
||||
import { useAuth } from '../../context/useAuth';
|
||||
|
||||
interface BackupStats {
|
||||
projects: number;
|
||||
@@ -28,7 +28,7 @@ interface BackupValidation {
|
||||
}
|
||||
|
||||
export const BackupRestore: React.FC = () => {
|
||||
const { organization } = useOrganization();
|
||||
const { appUser } = useAuth();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<BackupValidation | null>(null);
|
||||
@@ -36,8 +36,6 @@ export const BackupRestore: React.FC = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!organization) return;
|
||||
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const response = await api.get('/backup/export', {
|
||||
@@ -52,7 +50,8 @@ export const BackupRestore: React.FC = () => {
|
||||
|
||||
// Nome do arquivo com timestamp
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||
link.download = `backup_${organization.name}_${timestamp}.json`;
|
||||
const orgName = appUser?.name || 'GPI';
|
||||
link.download = `backup_${orgName}_${timestamp}.json`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
@@ -248,18 +247,18 @@ export const BackupRestore: React.FC = () => {
|
||||
</label>
|
||||
|
||||
{validationResult && (
|
||||
<div className={`p-4 rounded-xl border ${validationResult.valid && validationResult.isValidOrganization
|
||||
<div className={`p-4 rounded-xl border ${validationResult.valid
|
||||
? 'bg-green-500/10 border-green-500/30'
|
||||
: 'bg-red-500/10 border-red-500/30'
|
||||
}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
{validationResult.valid && validationResult.isValidOrganization ? (
|
||||
{validationResult.valid ? (
|
||||
<CheckCircle size={20} className="text-green-400 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<AlertTriangle size={20} className="text-red-400 flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className={`text-sm font-bold ${validationResult.valid && validationResult.isValidOrganization
|
||||
<p className={`text-sm font-bold ${validationResult.valid
|
||||
? 'text-green-400'
|
||||
: 'text-red-400'
|
||||
}`}>
|
||||
@@ -289,7 +288,7 @@ export const BackupRestore: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!validationResult?.valid || !validationResult?.isValidOrganization || isImporting}
|
||||
disabled={!validationResult?.valid || isImporting}
|
||||
className="w-full flex items-center justify-center gap-2 px-6 py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-red-500/20"
|
||||
>
|
||||
{isImporting ? (
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useOrganization } from '@clerk/clerk-react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Plus, Pencil, Trash2, Box, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '../Button';
|
||||
import { Modal } from '../Modal';
|
||||
import { Input } from '../Input';
|
||||
import * as geometryService from '../../services/geometryTypeService';
|
||||
import type { GeometryType } from '../../types';
|
||||
import { useAuth } from '../../context/useAuth';
|
||||
|
||||
export const GeometrySettings: React.FC = () => {
|
||||
const { organization } = useOrganization();
|
||||
const { isSignedIn } = useAuth();
|
||||
const [types, setTypes] = useState<GeometryType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -20,13 +20,7 @@ export const GeometrySettings: React.FC = () => {
|
||||
efficiencyLoss: '20'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (organization?.id) {
|
||||
fetchTypes();
|
||||
}
|
||||
}, [organization?.id]);
|
||||
|
||||
const fetchTypes = async () => {
|
||||
const fetchTypes = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await geometryService.getAllTypes();
|
||||
@@ -36,7 +30,13 @@ export const GeometrySettings: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSignedIn) {
|
||||
fetchTypes();
|
||||
}
|
||||
}, [isSignedIn, fetchTypes]);
|
||||
|
||||
const handleOpenModal = (item?: GeometryType) => {
|
||||
if (item) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
import type { AppUser } from '../types';
|
||||
import { AuthContext } from './AuthContextType';
|
||||
import { getToken, getUser, setUser, login as logtoLogin } from '../main';
|
||||
import { setUser } from '../main';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
@@ -10,41 +11,53 @@ interface AuthProviderProps {
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const { isAuthenticated, getAccessToken, fetchUserInfo, isLoading: isLogtoLoading } = useLogto();
|
||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAppLoading, setIsAppLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSignedIn, setIsSignedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getToken();
|
||||
const user = getUser();
|
||||
|
||||
if (token && user) {
|
||||
setAppUser(user as AppUser);
|
||||
setIsSignedIn(true);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const syncUser = useCallback(async () => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
if (!isAuthenticated) {
|
||||
setAppUser(null);
|
||||
setIsSignedIn(false);
|
||||
setIsLoading(false);
|
||||
setIsAppLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setIsAppLoading(true);
|
||||
setError(null);
|
||||
|
||||
const token = await getAccessToken();
|
||||
if (!token) throw new Error('Token não disponível');
|
||||
|
||||
// Busca dados básicos do Logto se necessário
|
||||
const logtoUserInfo = await fetchUserInfo();
|
||||
|
||||
const response = await fetch(`${API_URL}/users/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404 && logtoUserInfo) {
|
||||
// Usuário não existe no banco (provavelmente redirecionamento pós-login)
|
||||
// Vamos tentar sincronizar/provisionar
|
||||
const syncResp = await fetch(`${API_URL}/users/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: logtoUserInfo.email,
|
||||
name: logtoUserInfo.name || logtoUserInfo.username || 'Usuário Logto',
|
||||
logto_id: logtoUserInfo.sub
|
||||
})
|
||||
});
|
||||
|
||||
if (!syncResp.ok) throw new Error('Falha ao sincronizar usuário');
|
||||
|
||||
// Tenta buscar novamente após o sync
|
||||
return syncUser();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao carregar usuário');
|
||||
}
|
||||
@@ -60,16 +73,20 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
|
||||
setUser(token, user);
|
||||
setAppUser(user);
|
||||
setIsSignedIn(true);
|
||||
} catch (err) {
|
||||
console.error('Error loading user:', err);
|
||||
setError('Erro ao carregar dados do usuário');
|
||||
setAppUser(null);
|
||||
setIsSignedIn(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsAppLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [isAuthenticated, getAccessToken, fetchUserInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLogtoLoading) {
|
||||
syncUser();
|
||||
}
|
||||
}, [isLogtoLoading, syncUser]);
|
||||
|
||||
const refetchUser = useCallback(async () => {
|
||||
await syncUser();
|
||||
@@ -84,12 +101,12 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
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={{
|
||||
const isLoading = isLogtoLoading || isAppLoading;
|
||||
|
||||
const value = useMemo(() => ({
|
||||
appUser,
|
||||
isLoading,
|
||||
isSignedIn,
|
||||
isSignedIn: isAuthenticated,
|
||||
error,
|
||||
isAdmin,
|
||||
isUser,
|
||||
@@ -97,8 +114,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
isDeveloper,
|
||||
canEdit,
|
||||
refetchUser,
|
||||
}}
|
||||
>
|
||||
}), [appUser, isLoading, isAuthenticated, error, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
import api from '../services/api';
|
||||
import type { INotification } from '../types';
|
||||
import { NotificationContext } from './NotificationContextState';
|
||||
|
||||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { orgId, isSignedIn } = useAuth();
|
||||
const { isSignedIn } = useAuth();
|
||||
const [notifications, setNotifications] = useState<INotification[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
if (!orgId || !isSignedIn) return;
|
||||
if (!isSignedIn) return;
|
||||
|
||||
try {
|
||||
if (notifications.length === 0) setLoading(true);
|
||||
@@ -21,7 +21,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [orgId, isSignedIn, notifications.length]);
|
||||
}, [isSignedIn, notifications.length]);
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
@@ -70,7 +70,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
// Polling effect
|
||||
useEffect(() => {
|
||||
if (isSignedIn && orgId) {
|
||||
if (isSignedIn) {
|
||||
fetchNotifications(); // Initial fetch
|
||||
|
||||
const interval = setInterval(() => {
|
||||
@@ -81,7 +81,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
} else {
|
||||
setNotifications([]);
|
||||
}
|
||||
}, [isSignedIn, orgId, fetchNotifications]);
|
||||
}, [isSignedIn, fetchNotifications]);
|
||||
|
||||
const unreadCount = notifications.filter(n => !n.isRead).length;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import api from '../services/api';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
|
||||
export interface ActiveUser {
|
||||
_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
clerkId: string;
|
||||
logtoId: string;
|
||||
lastSeenAt: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,18 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { LogtoProvider, type LogtoConfig } from '@logto/react';
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
const LOGTO_URL = import.meta.env.VITE_LOGTO_URL || 'https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io';
|
||||
const APP_ID = import.meta.env.VITE_LOGTO_APP_ID || 'gpi-app-001';
|
||||
|
||||
const redirectUrl = `${window.location.origin}/auth/callback`;
|
||||
|
||||
function generateRandomString(length: number) {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function storeState(state: string) {
|
||||
sessionStorage.setItem('logto_oauth_state', state);
|
||||
}
|
||||
|
||||
export function login() {
|
||||
const state = generateRandomString(21);
|
||||
storeState(state);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: APP_ID,
|
||||
redirect_uri: redirectUrl,
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email',
|
||||
state: state
|
||||
});
|
||||
|
||||
window.location.href = `${LOGTO_URL}/oidc/auth?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
sessionStorage.removeItem('logto_token');
|
||||
sessionStorage.removeItem('logto_user');
|
||||
window.location.href = '/';
|
||||
}
|
||||
const config: LogtoConfig = {
|
||||
endpoint: LOGTO_URL,
|
||||
appId: APP_ID,
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
};
|
||||
|
||||
// Mantenha estas para compatibilidade temporária se necessário
|
||||
export function getToken() {
|
||||
return sessionStorage.getItem('logto_token');
|
||||
}
|
||||
@@ -53,5 +28,7 @@ export function setUser(token: string, user: any) {
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<LogtoProvider config={config}>
|
||||
<App />
|
||||
</LogtoProvider>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useUser, useOrganization } from '@clerk/clerk-react';
|
||||
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react';
|
||||
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database, Terminal } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import type { AppUser, UserRole } from '../types';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
@@ -15,9 +14,7 @@ const roleLabels: Record<UserRole, { label: string; color: string; icon: React.R
|
||||
};
|
||||
|
||||
export const AdminDashboard: React.FC = () => {
|
||||
const { user } = useUser();
|
||||
const { organization } = useOrganization();
|
||||
const { isAdmin } = useAuth();
|
||||
const { isAdmin, appUser: currentUser } = useAuth();
|
||||
const [users, setUsers] = useState<AppUser[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -27,8 +24,6 @@ export const AdminDashboard: React.FC = () => {
|
||||
const [logoLoading, setLogoLoading] = useState(false);
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
if (!user || !organization?.id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await api.get('/users');
|
||||
@@ -38,111 +33,15 @@ export const AdminDashboard: React.FC = () => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user, organization?.id]);
|
||||
|
||||
const syncOrganizationMembers = useCallback(async () => {
|
||||
if (!organization) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Fetch ALL members from Clerk (handle pagination)
|
||||
console.log('Fetching members from Clerk organization:', organization.id);
|
||||
let allMembers: any[] = [];
|
||||
let hasMore = true;
|
||||
|
||||
// Fetch all pages
|
||||
while (hasMore) {
|
||||
const clerkMembers = await organization.getMemberships();
|
||||
console.log(`Fetched members:`, clerkMembers.data.length);
|
||||
|
||||
if (clerkMembers.data.length === 0) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
allMembers = clerkMembers.data;
|
||||
hasMore = false; // Clerk retorna todos de uma vez normalmente
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Total Clerk members fetched:', allMembers.length, allMembers);
|
||||
|
||||
// Get current users from database
|
||||
const currentUsersResponse = await api.get('/users');
|
||||
const currentUsers = currentUsersResponse.data;
|
||||
console.log('Current users in database:', currentUsers.length, currentUsers);
|
||||
|
||||
// Create a Set of Clerk user IDs for fast lookup
|
||||
const clerkUserIds = new Set(
|
||||
allMembers
|
||||
.map(m => m.publicUserData?.userId)
|
||||
.filter(id => id != null)
|
||||
);
|
||||
|
||||
console.log('Clerk user IDs:', Array.from(clerkUserIds));
|
||||
|
||||
// Step 1: Add/Update users from Clerk
|
||||
for (const membership of allMembers) {
|
||||
const clerkUser = membership.publicUserData;
|
||||
console.log('Processing membership:', membership);
|
||||
console.log('Public user data:', clerkUser);
|
||||
|
||||
if (clerkUser) {
|
||||
const syncData = {
|
||||
clerkId: clerkUser.userId,
|
||||
email: clerkUser.identifier || '',
|
||||
name: `${clerkUser.firstName || ''} ${clerkUser.lastName || ''}`.trim() || clerkUser.identifier || 'Usuário',
|
||||
organizationId: organization.id,
|
||||
clerkRole: membership.role
|
||||
};
|
||||
|
||||
console.log('Syncing user:', syncData);
|
||||
|
||||
try {
|
||||
const response = await api.post('/users/sync', syncData);
|
||||
console.log('Sync success for', clerkUser.userId, ':', response.data);
|
||||
} catch (syncError) {
|
||||
console.error('Error syncing member:', clerkUser.userId, syncError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Remove users from database that don't exist in Clerk anymore
|
||||
let removedCount = 0;
|
||||
for (const dbUser of currentUsers) {
|
||||
const clerkUserId = dbUser.clerkUserId || dbUser.clerkId;
|
||||
if (!clerkUserIds.has(clerkUserId)) {
|
||||
console.log(`User ${dbUser.name} (${clerkUserId}) is in DB but not in Clerk - removing...`);
|
||||
try {
|
||||
await api.delete(`/users/${dbUser._id}`);
|
||||
console.log(`Removed user ${dbUser.name} from database`);
|
||||
removedCount++;
|
||||
} catch (deleteError) {
|
||||
console.error(`Error removing user ${dbUser.name}:`, deleteError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload users after sync
|
||||
console.log('Reloading users from database...');
|
||||
await fetchUsers();
|
||||
|
||||
const message = `Sincronização concluída!\n✅ ${allMembers.length} membros atualizados\n${removedCount > 0 ? `🗑️ ${removedCount} membros removidos` : ''}`;
|
||||
alert(message);
|
||||
} catch (error) {
|
||||
console.error('Error syncing organization members:', error);
|
||||
alert('Erro ao sincronizar membros. Verifique o console para mais detalhes.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [organization, fetchUsers]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdmin()) {
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
}
|
||||
}, [isAdmin, fetchUsers]);
|
||||
|
||||
const handleRoleChange = async (userId: string, newRole: UserRole) => {
|
||||
if (!user) return;
|
||||
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await api.patch(`/users/${userId}/role`, { role: newRole });
|
||||
@@ -158,8 +57,6 @@ export const AdminDashboard: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleToggleBan = async (userId: string, isBanned: boolean) => {
|
||||
if (!user) return;
|
||||
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
const response = await api.patch(`/users/${userId}/ban`, { isBanned });
|
||||
@@ -182,31 +79,8 @@ export const AdminDashboard: React.FC = () => {
|
||||
});
|
||||
|
||||
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !organization) return;
|
||||
|
||||
// Validations
|
||||
const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
alert('Por favor, selecione uma imagem PNG, JPG ou SVG.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 500 * 1024) {
|
||||
alert('O arquivo deve ter no máximo 500KB.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLogoLoading(true);
|
||||
try {
|
||||
await organization.setLogo({ file });
|
||||
alert('Logo atualizado com sucesso!');
|
||||
} catch (error) {
|
||||
console.error('Error uploading logo:', error);
|
||||
alert('Erro ao atualizar o logo.');
|
||||
} finally {
|
||||
setLogoLoading(false);
|
||||
}
|
||||
// Implement Logo Upload via Backend API if needed
|
||||
alert('Funcionalidade de upload de logo em migração para o novo sistema.');
|
||||
};
|
||||
|
||||
if (!isAdmin()) {
|
||||
@@ -234,14 +108,6 @@ export const AdminDashboard: React.FC = () => {
|
||||
</div>
|
||||
{activeTab === 'users' && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={syncOrganizationMembers}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-primary hover:bg-primary-dark text-white border border-primary-dark rounded-xl font-semibold transition-all disabled:opacity-50"
|
||||
>
|
||||
<Users size={18} className={isLoading ? 'animate-spin' : ''} />
|
||||
Sincronizar Clerk
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchUsers}
|
||||
disabled={isLoading}
|
||||
@@ -407,7 +273,7 @@ export const AdminDashboard: React.FC = () => {
|
||||
<tbody className="divide-y divide-border/40">
|
||||
{filteredUsers.map((u) => {
|
||||
const roleInfo = roleLabels[u.role];
|
||||
const isCurrentUser = u.clerkId === user?.id;
|
||||
const isCurrentUser = u.email === currentUser?.email;
|
||||
const isActionDisabled = actionLoading === u.id;
|
||||
|
||||
return (
|
||||
@@ -483,130 +349,24 @@ export const AdminDashboard: React.FC = () => {
|
||||
</>
|
||||
) : activeTab === 'organization' ? (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300">
|
||||
{/* Organization Settings Panel */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6">
|
||||
<div className="flex items-center gap-3 pb-4 border-b border-border/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Upload size={20} className="text-primary" />
|
||||
<Info size={20} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-text-main">Identidade Visual</h2>
|
||||
<p className="text-xs text-text-muted">Gerencie o logo da sua organização</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-6 py-4">
|
||||
{organization?.imageUrl ? (
|
||||
<div className="relative group">
|
||||
<div className="w-32 h-32 rounded-2xl border-2 border-primary/20 p-2 bg-white overflow-hidden shadow-xl">
|
||||
<img
|
||||
src={organization.imageUrl}
|
||||
alt={organization.name}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-2 -right-2 bg-primary text-white p-2 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<ImageIcon size={14} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-32 h-32 rounded-2xl border-2 border-dashed border-border/40 flex flex-col items-center justify-center bg-surface-soft text-text-muted gap-2">
|
||||
<ImageIcon size={32} className="opacity-20" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest">Sem Logo</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full space-y-4">
|
||||
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-border/40 rounded-2xl cursor-pointer hover:bg-surface-hover hover:border-primary/50 transition-all group">
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<Upload className="w-8 h-8 text-text-muted group-hover:text-primary transition-colors mb-2" />
|
||||
<p className="text-sm text-text-main font-bold">Clique para alterar o logo</p>
|
||||
<p className="text-xs text-text-muted">ou arraste e solte o arquivo</p>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
|
||||
onChange={handleLogoUpload}
|
||||
disabled={logoLoading}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{logoLoading && (
|
||||
<div className="flex items-center justify-center gap-2 text-primary font-bold animate-pulse">
|
||||
<RefreshCw size={16} className="animate-spin" />
|
||||
<span>Enviando logo...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6">
|
||||
<div className="flex items-center gap-3 pb-4 border-b border-border/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center">
|
||||
<Info size={20} className="text-amber-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-text-main">Requisitos & Dicas</h2>
|
||||
<p className="text-xs text-text-muted">Regras para um visual impecável</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
|
||||
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary"></div>
|
||||
Formatos Suportados
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted leading-relaxed">
|
||||
Aceitamos arquivos nos formatos <strong>PNG, JPG ou SVG</strong>. O formato SVG é recomendado para máxima nitidez em qualquer tamanho.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
|
||||
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
|
||||
Dimensões Recomendadas
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted leading-relaxed">
|
||||
Recomendamos uma imagem quadrada de no mínimo <strong>512x512 pixels</strong>. Logos horizontais podem não aparecer corretamente em todas as áreas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
|
||||
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red-500"></div>
|
||||
Limite de Tamanho
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted leading-relaxed">
|
||||
O arquivo não deve ultrapassar <strong>500 KB</strong>. Arquivos maiores serão rejeitados automaticamente para garantir rapidez no carregamento.
|
||||
</p>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-text-main">Configurações da Organização</h2>
|
||||
<p className="text-xs text-text-muted">Migrando para o novo sistema Logto</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-muted">A gestão de identidade visual e dados da organização está sendo migrada para a API central.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab === 'settings' ? (
|
||||
<GeometrySettings />
|
||||
) : activeTab === 'backup' ? (
|
||||
<BackupRestore />
|
||||
) : (
|
||||
// Lazily load or direct render StockDashboard (Need to import it)
|
||||
<div className="bg-surface rounded-2xl border border-border/40 p-6">
|
||||
<div className="text-center py-10">
|
||||
<h2 className="text-xl font-bold text-text-main">Gestão de Estoque</h2>
|
||||
<p className="text-text-muted mt-2">Acesse a nova página dedicada ao controle de estoque.</p>
|
||||
<a
|
||||
href="/stock"
|
||||
className="mt-6 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
Ir para Estoque
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
23
src/client/pages/Callback.tsx
Normal file
23
src/client/pages/Callback.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useHandleSignInCallback } from '@logto/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const Callback = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isLoading } = useHandleSignInCallback(() => {
|
||||
// Redireciona para a home após o login bem-sucedido
|
||||
navigate('/');
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-text-muted font-medium">Finalizando autenticação...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Hammer } from "lucide-react";
|
||||
import { login as logtoLogin } from "../main";
|
||||
import { useLogto } from "@logto/react";
|
||||
|
||||
const CALLBACK_URL = import.meta.env.VITE_LOGTO_CALLBACK_URL || `${window.location.origin}/callback`;
|
||||
|
||||
export const Login = () => {
|
||||
const { signIn } = useLogto();
|
||||
|
||||
const handleLogin = () => {
|
||||
logtoLogin();
|
||||
signIn(CALLBACK_URL);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -32,7 +36,7 @@ export const Login = () => {
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Continuar com Google
|
||||
</button>
|
||||
|
||||
@@ -1,184 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useUser, useOrganizationList, useOrganization } from '@clerk/clerk-react';
|
||||
import { Building2, Users, RefreshCw, Mail } from 'lucide-react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* @deprecated Clerk legacy component. No longer used in Logto flow.
|
||||
*/
|
||||
export const OrganizationSelector: React.FC = () => {
|
||||
const { user } = useUser();
|
||||
const { setActive, userMemberships, userInvitations } = useOrganizationList({
|
||||
userMemberships: {
|
||||
infinite: true,
|
||||
},
|
||||
userInvitations: {
|
||||
infinite: true,
|
||||
}
|
||||
});
|
||||
const { organization } = useOrganization();
|
||||
const [isAcceptingInvites, setIsAcceptingInvites] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
console.log('OrganizationSelector rendered');
|
||||
console.log('Current organization:', organization);
|
||||
console.log('User memberships:', userMemberships);
|
||||
console.log('User memberships data:', userMemberships.data);
|
||||
console.log('User invitations:', userInvitations);
|
||||
console.log('User invitations data:', userInvitations.data);
|
||||
|
||||
// Auto-accept pending invitations
|
||||
useEffect(() => {
|
||||
const acceptPendingInvitations = async () => {
|
||||
if (userInvitations.data && userInvitations.data.length > 0 && !isAcceptingInvites) {
|
||||
console.log('Found pending invitations, auto-accepting...');
|
||||
setIsAcceptingInvites(true);
|
||||
navigate('/', { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
for (const invitation of userInvitations.data) {
|
||||
try {
|
||||
console.log('Accepting invitation:', invitation);
|
||||
await invitation.accept();
|
||||
console.log('Invitation accepted successfully');
|
||||
} catch (error) {
|
||||
console.error('Error accepting invitation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload memberships after accepting invitations
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
acceptPendingInvitations();
|
||||
}, [userInvitations.data, isAcceptingInvites]);
|
||||
|
||||
// Auto-select if user has only one organization
|
||||
useEffect(() => {
|
||||
console.log('Auto-select effect running...');
|
||||
if (!organization && userMemberships.data && userMemberships.data.length === 1) {
|
||||
console.log('Auto-selecting single organization...');
|
||||
const membership = userMemberships.data[0];
|
||||
if (setActive) {
|
||||
setActive({ organization: membership.organization });
|
||||
}
|
||||
}
|
||||
}, [organization, userMemberships.data, setActive]);
|
||||
|
||||
const handleSelectOrganization = async (orgId: string) => {
|
||||
console.log('Selecting organization:', orgId);
|
||||
if (setActive) {
|
||||
await setActive({ organization: orgId });
|
||||
}
|
||||
// The auth context will automatically sync after organization changes
|
||||
};
|
||||
|
||||
// Loading state - check if data exists or accepting invites
|
||||
if (!userMemberships.data || isAcceptingInvites) {
|
||||
console.log('Loading state - no data yet or accepting invites');
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
{isAcceptingInvites ? (
|
||||
<>
|
||||
<Mail className="w-12 h-12 text-primary animate-bounce mx-auto mb-4" />
|
||||
<p className="text-text-main font-bold mb-2">Aceitando convites pendentes...</p>
|
||||
<p className="text-text-muted text-sm">Por favor aguarde</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-12 h-12 text-primary animate-spin mx-auto mb-4" />
|
||||
<p className="text-text-muted">Carregando organizações...</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (userMemberships.data?.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-surface rounded-2xl border border-border/40 p-8 text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-amber-500/20 flex items-center justify-center mx-auto mb-4">
|
||||
<Building2 className="w-8 h-8 text-amber-500" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-text-main mb-2">
|
||||
Nenhuma Organização
|
||||
</h1>
|
||||
<p className="text-text-muted mb-6">
|
||||
Você ainda não faz parte de nenhuma organização. Entre em contato com o administrador para receber um convite.
|
||||
</p>
|
||||
<div className="text-sm text-text-muted bg-surface-soft rounded-lg p-4">
|
||||
<p className="font-semibold mb-1">Conectado como:</p>
|
||||
<p>{user?.primaryEmailAddress?.emailAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="max-w-2xl w-full">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-primary/20 flex items-center justify-center mx-auto mb-4">
|
||||
<Building2 className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-text-main mb-2">
|
||||
Selecione uma Organização
|
||||
</h1>
|
||||
<p className="text-text-muted">
|
||||
Escolha qual organização você deseja acessar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{userMemberships.data?.map((membership) => (
|
||||
<button
|
||||
key={membership.organization.id}
|
||||
onClick={() => handleSelectOrganization(membership.organization.id)}
|
||||
className="w-full bg-surface hover:bg-surface-hover border border-border/40 rounded-2xl p-6 text-left transition-all hover:border-primary/50 hover:shadow-lg hover:shadow-primary/10 group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-primary/20 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/30 transition-colors">
|
||||
{membership.organization.imageUrl ? (
|
||||
<img
|
||||
src={membership.organization.imageUrl}
|
||||
alt={membership.organization.name}
|
||||
className="w-12 h-12 rounded-lg object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Building2 className="w-7 h-7 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-text-main group-hover:text-primary transition-colors">
|
||||
{membership.organization.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-primary/20 text-primary text-xs font-semibold">
|
||||
{membership.role === 'org:admin' ? 'Administrador' :
|
||||
membership.role === 'org:member' ? 'Membro' : 'Convidado'}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs text-text-muted">
|
||||
<Users className="w-3 h-3" />
|
||||
{membership.organization.membersCount || 0} membros
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-primary opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-text-muted">
|
||||
Conectado como <span className="font-semibold">{user?.primaryEmailAddress?.emailAddress}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useAuth } from '../context/useAuth';
|
||||
import { useToast } from '../hooks/useToast';
|
||||
import { MobileList } from '../components/MobileList';
|
||||
import { CreateProjectModal } from '../components/modals/CreateProjectModal';
|
||||
import { useOrganization } from '@clerk/clerk-react';
|
||||
import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||
import { Modal } from '../components/Modal';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
@@ -50,14 +49,12 @@ export const ProjectList: React.FC = () => {
|
||||
const [isPrintingGeneral, setIsPrintingGeneral] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { appUser } = useAuth();
|
||||
const { appUser, isAdmin: checkIsAdmin } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const { organization } = useOrganization();
|
||||
const { settings } = useSystemSettings();
|
||||
|
||||
const logoUrl = settings?.appLogoUrl || organization?.imageUrl;
|
||||
|
||||
const isAdmin = appUser?.email === 'admtracksteel@gmail.com' || appUser?.role === 'admin';
|
||||
const logoUrl = settings?.appLogoUrl;
|
||||
const isAdmin = checkIsAdmin();
|
||||
|
||||
const fetchProjects = useCallback(async () => {
|
||||
try {
|
||||
|
||||
@@ -7,13 +7,10 @@ import { StockHistoryModal } from '../components/modals/StockHistoryModal';
|
||||
import { StockInventoryReport } from '../components/reports/StockInventoryReport';
|
||||
import { DiluentListModal } from '../components/modals/DiluentListModal';
|
||||
import { useAuth } from '../context/useAuth';
|
||||
import { useOrganization } from '@clerk/clerk-react';
|
||||
import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||
|
||||
export const StockDashboard: React.FC = () => {
|
||||
// ... rest of component
|
||||
const { isAdmin } = useAuth();
|
||||
const { organization } = useOrganization();
|
||||
const { settings } = useSystemSettings();
|
||||
|
||||
const [items, setItems] = useState<StockItem[]>([]);
|
||||
@@ -28,7 +25,7 @@ export const StockDashboard: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'PAINT' | 'THINNER'>('PAINT');
|
||||
const [showDiluentModal, setShowDiluentModal] = useState(false);
|
||||
|
||||
const logoUrl = settings?.appLogoUrl || organization?.imageUrl;
|
||||
const logoUrl = settings?.appLogoUrl;
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
@@ -109,15 +106,15 @@ export const StockDashboard: React.FC = () => {
|
||||
const filteredItems = items.filter(item => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
// Handle type checking carefully. If type is missing, assume PAINT.
|
||||
const type = (typeof item.dataSheetId === 'object' ? item.dataSheetId.type : '') || 'PAINT';
|
||||
const type = (typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).type : '') || 'PAINT';
|
||||
const isThinner = type === 'THINNER' || type === 'DILUENTE';
|
||||
|
||||
// Tab Filter
|
||||
if (activeTab === 'THINNER' && !isThinner) return false;
|
||||
if (activeTab === 'PAINT' && isThinner) return false;
|
||||
|
||||
const productName = typeof item.dataSheetId === 'object' ? item.dataSheetId.name : '';
|
||||
const manufacturer = typeof item.dataSheetId === 'object' ? item.dataSheetId.manufacturer : '';
|
||||
const productName = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).name : '';
|
||||
const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : '';
|
||||
|
||||
return (
|
||||
item.rrNumber.toLowerCase().includes(searchLower) ||
|
||||
@@ -131,9 +128,9 @@ export const StockDashboard: React.FC = () => {
|
||||
const groups = new Map<string, { items: StockItem[], totalQty: number, minStock: number, unit: string, productName: string, color: string, manufacturer: string }>();
|
||||
|
||||
filteredItems.forEach(item => {
|
||||
const productName = typeof item.dataSheetId === 'object' ? item.dataSheetId.name : 'Unknown';
|
||||
const manufacturer = typeof item.dataSheetId === 'object' ? item.dataSheetId.manufacturer : '';
|
||||
const key = `${item.dataSheetId._id || item.dataSheetId}-${item.color}`;
|
||||
const productName = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).name : 'Unknown';
|
||||
const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : '';
|
||||
const key = `${(item.dataSheetId as any)._id || item.dataSheetId}-${item.color}`;
|
||||
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
|
||||
@@ -190,7 +190,8 @@ export type UserRole = 'guest' | 'user' | 'admin';
|
||||
export interface AppUser {
|
||||
id: string;
|
||||
_id?: string;
|
||||
clerkId: string;
|
||||
clerkId?: string;
|
||||
logtoId?: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
|
||||
@@ -7,7 +7,7 @@ interface AuthRequest extends Request {
|
||||
|
||||
export const syncUser = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, name } = req.body;
|
||||
const { email, name, logto_id } = req.body;
|
||||
|
||||
if (!email || !name) {
|
||||
return res.status(400).json({ error: 'email e name são obrigatórios.' });
|
||||
@@ -21,6 +21,7 @@ export const syncUser = async (req: Request, res: Response) => {
|
||||
.insert({
|
||||
email,
|
||||
name,
|
||||
logto_id,
|
||||
role: 'guest'
|
||||
})
|
||||
.select()
|
||||
|
||||
@@ -30,10 +30,28 @@ export async function authenticateRequest(req: any): Promise<AppUser | null> {
|
||||
|
||||
const logtoId = payload.sub as string;
|
||||
|
||||
const user = await findOneGpi('users', { logto_id: logtoId });
|
||||
// Primeiro tenta pelo Logto ID
|
||||
let user = await findOneGpi('users', { logto_id: logtoId });
|
||||
|
||||
// Se não encontrar, tenta pelo email (se houver no payload do token)
|
||||
if (!user && payload.email) {
|
||||
const email = payload.email as string;
|
||||
user = await findOneGpi('users', { email });
|
||||
|
||||
if (user) {
|
||||
// Vincula o Logto ID ao usuário existente
|
||||
await supabase
|
||||
.from('users')
|
||||
.update({ logto_id: logtoId })
|
||||
.eq('id', user.id);
|
||||
|
||||
user.logto_id = logtoId;
|
||||
console.log(`[Auth] Usuário ${email} vinculado ao Logto ID ${logtoId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
console.log(`[Auth] Usuário Logto ${logtoId} não encontrado no GPI`);
|
||||
console.log(`[Auth] Usuário Logto ${logtoId} sem registro no GPI`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user