fix(clients): resolve file import dialog error and SSE 401
Browse filesFile 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.
|
@@ -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 |
)}
|
|
@@ -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 },
|
|
@@ -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) => {
|
|
@@ -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;
|
| 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 |
};
|