From 49538cfbd4bacf806543df95ad7747178f6b60cb Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Tue, 31 Mar 2026 10:32:42 +0000 Subject: [PATCH] =?UTF-8?q?Migra=C3=A7=C3=A3o=20completa=20para=20Logto=20?= =?UTF-8?q?-=20Remo=C3=A7=C3=A3o=20de=20Clerk=20finalizada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 101 +++++++ package.json | 7 +- src/client/App.tsx | 3 + src/client/components/Layout.tsx | 96 ++---- src/client/components/ProtectedRoute.tsx | 7 +- src/client/components/TeamPresence.tsx | 100 +++---- src/client/components/admin/BackupRestore.tsx | 17 +- .../components/admin/GeometrySettings.tsx | 22 +- src/client/context/AuthContext.tsx | 95 +++--- src/client/contexts/NotificationContext.tsx | 12 +- src/client/hooks/usePresence.ts | 4 +- src/client/main.tsx | 43 +-- src/client/pages/AdminDashboard.tsx | 278 ++---------------- src/client/pages/Callback.tsx | 23 ++ src/client/pages/Login.tsx | 10 +- src/client/pages/OrganizationSelector.tsx | 187 +----------- src/client/pages/ProjectList.tsx | 9 +- src/client/pages/StockDashboard.tsx | 17 +- src/client/types.ts | 3 +- src/server/controllers/userController.ts | 3 +- src/server/middleware/logtoAuth.ts | 22 +- 21 files changed, 371 insertions(+), 688 deletions(-) create mode 100644 src/client/pages/Callback.tsx diff --git a/package-lock.json b/package-lock.json index 42bc20c..35a0145 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/package.json b/package.json index ab5bf94..dc07c75 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,16 @@ "dev": "concurrently \"vite\" \"tsx watch src/server/index.ts\"", "build:client": "vite build", "build:server": "tsc -p tsconfig.server.json", -"prebuild": "npm install", - "build": "npm run build:client", + "prebuild": "npm install", + "build": "npm run build:client", "lint": "eslint .", "preview": "vite preview", "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": { diff --git a/src/client/App.tsx b/src/client/App.tsx index 9b04eba..1d4a147 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -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 = () => { } /> + } /> + } /> } /> } /> } /> diff --git a/src/client/components/Layout.tsx b/src/client/components/Layout.tsx index 1d6c29b..4dd0794 100644 --- a/src/client/components/Layout.tsx +++ b/src/client/components/Layout.tsx @@ -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 = ({ 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 = ({ 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 = ({ children }) => { return false; }; + const handleLogout = () => { + signOut(window.location.origin); + }; + + if (location.pathname === '/login' || location.pathname === '/callback') { + return <>{children}; + } + return (
{/* Sidebar Desktop - Fixed */} @@ -117,47 +118,6 @@ export const Layout: React.FC = ({ children }) => {
-
- {isAdmin() ? ( - - ) : ( -
- {organization?.imageUrl ? ( - {organization.name} - ) : ( -
- {organization?.name?.substring(0, 2).toUpperCase()} -
- )} -
-

{organization?.name || 'Carregando...'}

-

Organização

-
-
- )} -
- {/* Team Presence - Shows all members with online/offline status */} @@ -257,7 +217,7 @@ export const Layout: React.FC = ({ children }) => { + @@ -415,4 +384,3 @@ export const Layout: React.FC = ({ children }) => { ); }; - diff --git a/src/client/components/ProtectedRoute.tsx b/src/client/components/ProtectedRoute.tsx index 302ec86..abb04c0 100644 --- a/src/client/components/ProtectedRoute.tsx +++ b/src/client/components/ProtectedRoute.tsx @@ -23,7 +23,7 @@ export const ProtectedRoute: React.FC = ({ 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 = ({ ); } + // Check authentication + if (!isSignedIn) { + return ; + } + // Check role-based access if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) { return ; diff --git a/src/client/components/TeamPresence.tsx b/src/client/components/TeamPresence.tsx index 77388c9..0044a5c 100644 --- a/src/client/components/TeamPresence.tsx +++ b/src/client/components/TeamPresence.tsx @@ -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([]); const [pendingMessages, setPendingMessages] = React.useState([]); 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...'); - try { - const response = await api.get('/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'); + // Fetch all members + const fetchMembers = useCallback(async () => { + try { + const response = await api.get('/users'); + setAllMembers(response.data); + } catch (error) { + console.error('Error fetching members:', error); } - }, [organization?.id]); + }, []); // Fetch pending messages - React.useEffect(() => { - const fetchPendingMessages = async () => { - try { - const response = await api.get('/messages/pending'); - setPendingMessages(response.data); - } catch (error) { - console.error('Error fetching pending messages:', error); - } - }; - - if (organization?.id) { - fetchPendingMessages(); - const interval = setInterval(fetchPendingMessages, 30000); - return () => clearInterval(interval); + const fetchPendingMessages = useCallback(async () => { + try { + const response = await api.get('/messages/pending'); + setPendingMessages(response.data); + } catch (error) { + console.error('Error fetching pending messages:', error); } - }, [organization?.id]); + }, []); - console.log('Rendering with allMembers.length:', allMembers.length); + React.useEffect(() => { + fetchMembers(); + fetchPendingMessages(); + + 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('/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 = () => {
{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'; diff --git a/src/client/components/admin/BackupRestore.tsx b/src/client/components/admin/BackupRestore.tsx index 76d7d84..fd871a3 100644 --- a/src/client/components/admin/BackupRestore.tsx +++ b/src/client/components/admin/BackupRestore.tsx @@ -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(null); @@ -36,8 +36,6 @@ export const BackupRestore: React.FC = () => { const fileInputRef = useRef(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 = () => { {validationResult && ( -
- {validationResult.valid && validationResult.isValidOrganization ? ( + {validationResult.valid ? ( ) : ( )}
-

@@ -289,7 +288,7 @@ export const BackupRestore: React.FC = () => {

{activeTab === 'users' && (
- diff --git a/src/client/pages/OrganizationSelector.tsx b/src/client/pages/OrganizationSelector.tsx index 730ccf8..d3f8747 100644 --- a/src/client/pages/OrganizationSelector.tsx +++ b/src/client/pages/OrganizationSelector.tsx @@ -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 ( -
-
- {isAcceptingInvites ? ( - <> - -

Aceitando convites pendentes...

-

Por favor aguarde

- - ) : ( - <> - -

Carregando organizações...

- - )} -
-
- ); - } - - if (userMemberships.data?.length === 0) { - return ( -
-
-
- -
-

- Nenhuma Organização -

-

- Você ainda não faz parte de nenhuma organização. Entre em contato com o administrador para receber um convite. -

-
-

Conectado como:

-

{user?.primaryEmailAddress?.emailAddress}

-
-
-
- ); - } - - return ( -
-
-
-
- -
-

- Selecione uma Organização -

-

- Escolha qual organização você deseja acessar -

-
- -
- {userMemberships.data?.map((membership) => ( - - ))} -
- -
-

- Conectado como {user?.primaryEmailAddress?.emailAddress} -

-
-
-
- ); + return null; }; diff --git a/src/client/pages/ProjectList.tsx b/src/client/pages/ProjectList.tsx index 626fa83..fb51415 100644 --- a/src/client/pages/ProjectList.tsx +++ b/src/client/pages/ProjectList.tsx @@ -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 { diff --git a/src/client/pages/StockDashboard.tsx b/src/client/pages/StockDashboard.tsx index 6063a0f..0acd2be 100644 --- a/src/client/pages/StockDashboard.tsx +++ b/src/client/pages/StockDashboard.tsx @@ -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([]); @@ -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(); 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, { diff --git a/src/client/types.ts b/src/client/types.ts index d45ef5d..9a314ff 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -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; diff --git a/src/server/controllers/userController.ts b/src/server/controllers/userController.ts index 95faa22..92e6f18 100644 --- a/src/server/controllers/userController.ts +++ b/src/server/controllers/userController.ts @@ -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() diff --git a/src/server/middleware/logtoAuth.ts b/src/server/middleware/logtoAuth.ts index 3367f0d..4fd0f9c 100644 --- a/src/server/middleware/logtoAuth.ts +++ b/src/server/middleware/logtoAuth.ts @@ -30,10 +30,28 @@ export async function authenticateRequest(req: any): Promise { 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; }