CognxSafeTrack commited on
Commit
87dcd87
·
1 Parent(s): d66ae60

fix(clients): resolve file import dialog error and SSE 401

Browse files

File import (Bug 1):
- CrmAIAssistant had its own <input type="file"> that called onFileUpload
onChange, which then called .click() on #crm-file-upload — a second
programmatic file dialog triggered from an onChange handler. Chrome 126+
blocks this as it lacks a direct "user activation".
- Fix: replace the embedded <input> with <label htmlFor="crm-file-upload">
so the click routes directly to FileImporter's input. Remove the
now-unused onFileUpload prop entirely.

SSE 401 (Bug 2):
- EventSource (browser API) cannot send custom headers — the JWT token
never reached the server, causing a 401 on every SSE connection.
- Fix: pass the token as ?token= query param in the EventSource URL
(MainLayout + CrmConversationalDashboard).
- Backend: verifyJwt now injects the query param token into the
Authorization header before calling jwtVerify(), so no route changes
are needed.

apps/admin/src/components/crm/CrmAIAssistant.tsx CHANGED
@@ -13,7 +13,6 @@ interface CrmAIAssistantProps {
13
  isUploading: boolean;
14
  isGenerating: boolean;
15
  isDragging: boolean;
16
- onFileUpload: () => void;
17
  onDragOver: (e: React.DragEvent) => void;
18
  onDragLeave: () => void;
19
  onDrop: (e: React.DragEvent) => void;
@@ -41,7 +40,6 @@ export default function CrmAIAssistant({
41
  isUploading,
42
  isGenerating,
43
  isDragging,
44
- onFileUpload,
45
  onDragOver,
46
  onDragLeave,
47
  onDrop,
@@ -107,14 +105,8 @@ export default function CrmAIAssistant({
107
  <h3 className="text-lg font-black text-slate-900">Glissez-déposez ici</h3>
108
  <p className="text-sm text-slate-400 mt-2 mb-8 font-medium">Fichiers .xlsx, .xls ou .csv</p>
109
 
110
- <label className="bg-slate-900 text-white px-8 py-4 rounded-2xl font-bold text-sm cursor-pointer hover:bg-slate-800 transition shadow-xl shadow-slate-200 active:scale-95">
111
  Parcourir mes fichiers
112
- <input
113
- type="file"
114
- className="hidden"
115
- accept=".xlsx,.xls,.csv"
116
- onChange={() => onFileUpload()}
117
- />
118
  </label>
119
  </div>
120
  )}
 
13
  isUploading: boolean;
14
  isGenerating: boolean;
15
  isDragging: boolean;
 
16
  onDragOver: (e: React.DragEvent) => void;
17
  onDragLeave: () => void;
18
  onDrop: (e: React.DragEvent) => void;
 
40
  isUploading,
41
  isGenerating,
42
  isDragging,
 
43
  onDragOver,
44
  onDragLeave,
45
  onDrop,
 
105
  <h3 className="text-lg font-black text-slate-900">Glissez-déposez ici</h3>
106
  <p className="text-sm text-slate-400 mt-2 mb-8 font-medium">Fichiers .xlsx, .xls ou .csv</p>
107
 
108
+ <label htmlFor="crm-file-upload" className="bg-slate-900 text-white px-8 py-4 rounded-2xl font-bold text-sm cursor-pointer hover:bg-slate-800 transition shadow-xl shadow-slate-200 active:scale-95">
109
  Parcourir mes fichiers
 
 
 
 
 
 
110
  </label>
111
  </div>
112
  )}
apps/admin/src/components/layouts/MainLayout.tsx CHANGED
@@ -30,7 +30,7 @@ function useAdminChatPage(): AdminChatPage | null {
30
 
31
  export default function MainLayout({ children, isSuperAdmin, orgs }: MainLayoutProps) {
32
  const { t } = useTranslation();
33
- const { logout, user } = useAuth();
34
  const { selectedOrgId, setSelectedOrgId, currentOrg } = useTenant();
35
  const [sidebarOpen, setSidebarOpen] = useState(false);
36
  const [unreadCount, setUnreadCount] = useState(0);
@@ -50,9 +50,9 @@ export default function MainLayout({ children, isSuperAdmin, orgs }: MainLayoutP
50
 
51
  // SSE — track new inbound messages for the notification badge
52
  useEffect(() => {
53
- if (!selectedOrgId) return;
54
  const apiBase = import.meta.env.VITE_API_URL || '';
55
- const es = new EventSource(`${apiBase}/v1/organizations/${selectedOrgId}/stream`);
56
  esRef.current = es;
57
  es.addEventListener('message', (event) => {
58
  try {
@@ -68,7 +68,7 @@ export default function MainLayout({ children, isSuperAdmin, orgs }: MainLayoutP
68
  });
69
  es.onerror = () => es.close();
70
  return () => { es.close(); esRef.current = null; };
71
- }, [selectedOrgId]);
72
 
73
  const navItems = [
74
  { to: '/', label: t('nav.home'), icon: <BarChart2 className="w-4 h-4" />, end: true },
 
30
 
31
  export default function MainLayout({ children, isSuperAdmin, orgs }: MainLayoutProps) {
32
  const { t } = useTranslation();
33
+ const { logout, user, token } = useAuth();
34
  const { selectedOrgId, setSelectedOrgId, currentOrg } = useTenant();
35
  const [sidebarOpen, setSidebarOpen] = useState(false);
36
  const [unreadCount, setUnreadCount] = useState(0);
 
50
 
51
  // SSE — track new inbound messages for the notification badge
52
  useEffect(() => {
53
+ if (!selectedOrgId || !token) return;
54
  const apiBase = import.meta.env.VITE_API_URL || '';
55
+ const es = new EventSource(`${apiBase}/v1/organizations/${selectedOrgId}/stream?token=${encodeURIComponent(token)}`);
56
  esRef.current = es;
57
  es.addEventListener('message', (event) => {
58
  try {
 
68
  });
69
  es.onerror = () => es.close();
70
  return () => { es.close(); esRef.current = null; };
71
+ }, [selectedOrgId, token]);
72
 
73
  const navItems = [
74
  { to: '/', label: t('nav.home'), icon: <BarChart2 className="w-4 h-4" />, end: true },
apps/admin/src/pages/CrmConversationalDashboard.tsx CHANGED
@@ -68,7 +68,7 @@ export default function CrmConversationalDashboard() {
68
  useEffect(() => {
69
  if (!selectedOrgId || !token) return;
70
  const apiBase = import.meta.env.VITE_API_URL || '';
71
- const url = `${apiBase}/v1/organizations/${selectedOrgId}/stream`;
72
  const es = new EventSource(url);
73
  es.addEventListener('message', (event) => {
74
  try {
@@ -198,7 +198,6 @@ export default function CrmConversationalDashboard() {
198
  isUploading={isUploading}
199
  isGenerating={isGenerating}
200
  isDragging={isDragging}
201
- onFileUpload={() => document.getElementById('crm-file-upload')?.click()}
202
  onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
203
  onDragLeave={() => setIsDragging(false)}
204
  onDrop={(e) => {
 
68
  useEffect(() => {
69
  if (!selectedOrgId || !token) return;
70
  const apiBase = import.meta.env.VITE_API_URL || '';
71
+ const url = `${apiBase}/v1/organizations/${selectedOrgId}/stream?token=${encodeURIComponent(token)}`;
72
  const es = new EventSource(url);
73
  es.addEventListener('message', (event) => {
74
  try {
 
198
  isUploading={isUploading}
199
  isGenerating={isGenerating}
200
  isDragging={isDragging}
 
201
  onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
202
  onDragLeave={() => setIsDragging(false)}
203
  onDrop={(e) => {
apps/api/src/middleware/verifyJwt.ts CHANGED
@@ -6,9 +6,14 @@ import { FastifyRequest, FastifyReply } from 'fastify';
6
  */
7
  export const verifyJwt = async (request: FastifyRequest, reply: FastifyReply) => {
8
  try {
 
 
 
 
 
9
  await request.jwtVerify();
10
  } catch (err) {
11
  reply.code(401).send({ error: 'Unauthorized', message: 'Invalid or missing token' });
12
- throw err; // Stop further execution in the hook chain
13
  }
14
  };
 
6
  */
7
  export const verifyJwt = async (request: FastifyRequest, reply: FastifyReply) => {
8
  try {
9
+ // EventSource (SSE) cannot send custom headers — accept JWT as ?token= query param fallback
10
+ const queryToken = (request.query as Record<string, string>)?.token;
11
+ if (queryToken && !request.headers.authorization) {
12
+ request.headers.authorization = `Bearer ${queryToken}`;
13
+ }
14
  await request.jwtVerify();
15
  } catch (err) {
16
  reply.code(401).send({ error: 'Unauthorized', message: 'Invalid or missing token' });
17
+ throw err;
18
  }
19
  };