larxius commited on
Commit
b0b39cf
·
verified ·
1 Parent(s): 16a9fff

Update frontend/src/pages/Profile.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/Profile.jsx +238 -51
frontend/src/pages/Profile.jsx CHANGED
@@ -1,9 +1,10 @@
1
- import React, { useState, useEffect } from 'react';
2
  import { useAuth } from '../components/AuthContext';
3
  import { toast } from 'react-hot-toast';
4
 
5
  export const Profile = () => {
6
  const { token, logout } = useAuth();
 
7
  const [profile, setProfile] = useState(null);
8
  const [loading, setLoading] = useState(true);
9
  const [error, setError] = useState(null);
@@ -62,6 +63,186 @@ export const Profile = () => {
62
  }
63
  };
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  const [passwordData, setPasswordData] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
66
  const [passwordStatus, setPasswordStatus] = useState({ loading: false, error: null, success: false });
67
  const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false });
@@ -430,71 +611,77 @@ export const Profile = () => {
430
 
431
  {/* Second Row for Report Branding */}
432
  {(profile.role === 'super_admin' || profile.role === 'org_admin') && (
433
- <div className="w-full bg-surface-container-lowest border border-outline-variant/70 rounded-2xl shadow-2xs p-6 mt-6">
434
  <div className="flex items-center gap-2 mb-1">
435
- <span className="material-symbols-outlined text-[#2563eb] text-[24px]">palette</span>
436
- <h3 className="font-bold text-on-surface text-[18px] m-0">
437
  Report Branding
438
  </h3>
439
  </div>
440
- <p className="text-on-surface-variant text-sm mb-6 m-0">
441
  Customize generated PDF security reports with your organization's logo.
442
  </p>
443
 
444
- <div className="border-t border-outline-variant/40 pt-6">
445
- <div className="border-2 border-dashed border-outline-variant/60 rounded-xl bg-surface-container-low/30 p-10 flex flex-col items-center justify-center text-center">
446
- <span className="material-symbols-outlined text-[#2563eb] text-[44px] mb-3">cloud_upload</span>
447
-
448
- <h4 className="font-bold text-on-surface text-[16px] mb-1">
449
- Upload Organization Logo
450
- </h4>
451
- <p className="text-on-surface-variant text-sm mb-5">
452
- Upload your custom logo to brand all PDF security reports
453
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
- {reportLogoUrl && (
456
- <div className="mb-4 p-2 bg-surface-container rounded-lg border border-outline-variant/40">
457
- <img src={reportLogoUrl} alt="Organization Logo" className="max-h-20 max-w-full object-contain rounded" />
458
- </div>
459
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
  <button
462
  type="button"
463
- onClick={() => {
464
- const fileInput = document.createElement('input');
465
- fileInput.type = 'file';
466
- fileInput.accept = 'image/*';
467
- fileInput.onchange = async (e) => {
468
- const file = e.target.files[0];
469
- if (!file) return;
470
- const formData = new FormData();
471
- formData.append('logo', file);
472
- try {
473
- const res = await fetch('/api/auth/organizations/logo', {
474
- method: 'POST',
475
- headers: { 'Authorization': `Bearer ${token}` },
476
- body: formData
477
- });
478
- if (res.ok) {
479
- toast.success("Report branding updated! Future PDF reports will include your logo.");
480
- fetchBrandingManual();
481
- } else {
482
- const data = await res.json();
483
- toast.error(data.message || "Failed to update report branding.");
484
- }
485
- } catch (err) {
486
- toast.error("Error uploading logo.");
487
- }
488
- };
489
- fileInput.click();
490
  }}
491
- className="bg-[#2563eb] hover:bg-[#1d4ed8] text-white font-bold px-6 py-2.5 rounded-lg flex items-center gap-2 text-sm transition-all cursor-pointer shadow-2xs mb-4"
492
  >
493
- <span className="material-symbols-outlined text-[18px]">upload</span>
494
- <span>{reportLogoUrl ? 'Change Logo' : 'Upload Here'}</span>
 
 
495
  </button>
496
 
497
- <p className="text-on-surface-variant text-[12px] m-0 font-medium">
498
  Supported formats: PNG, JPG, WebP, SVG (Max 5MB)
499
  </p>
500
  </div>
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
  import { useAuth } from '../components/AuthContext';
3
  import { toast } from 'react-hot-toast';
4
 
5
  export const Profile = () => {
6
  const { token, logout } = useAuth();
7
+ const fileInputRef = useRef(null);
8
  const [profile, setProfile] = useState(null);
9
  const [loading, setLoading] = useState(true);
10
  const [error, setError] = useState(null);
 
63
  }
64
  };
65
 
66
+ useEffect(() => {
67
+ const preventWindowDrop = (e) => {
68
+ e.preventDefault();
69
+ };
70
+ window.addEventListener('dragover', preventWindowDrop);
71
+ window.addEventListener('drop', preventWindowDrop);
72
+ return () => {
73
+ window.removeEventListener('dragover', preventWindowDrop);
74
+ window.removeEventListener('drop', preventWindowDrop);
75
+ };
76
+ }, []);
77
+
78
+ const [isDragging, setIsDragging] = useState(false);
79
+ const [uploadingLogo, setUploadingLogo] = useState(false);
80
+
81
+ const handleUploadLogoUrl = async (imageUrl) => {
82
+ setUploadingLogo(true);
83
+ try {
84
+ const res = await fetch('/api/auth/organizations/logo', {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Authorization': `Bearer ${token}`,
88
+ 'Content-Type': 'application/json'
89
+ },
90
+ body: JSON.stringify({ logo_url: imageUrl })
91
+ });
92
+ if (res.ok) {
93
+ toast.success("Report branding updated! Future PDF reports will include your logo.");
94
+ fetchBrandingManual();
95
+ } else {
96
+ const data = await res.json();
97
+ toast.error(data.message || "Failed to download and process web image.");
98
+ }
99
+ } catch (err) {
100
+ toast.error("Error uploading logo URL.");
101
+ } finally {
102
+ setUploadingLogo(false);
103
+ }
104
+ };
105
+
106
+ const handleUploadLogoFile = async (file) => {
107
+ if (!file) return;
108
+ if (file.size > 5 * 1024 * 1024) {
109
+ toast.error("File size exceeds 5MB limit.");
110
+ return;
111
+ }
112
+ setUploadingLogo(true);
113
+ const formData = new FormData();
114
+ formData.append('logo', file);
115
+ try {
116
+ const res = await fetch('/api/auth/organizations/logo', {
117
+ method: 'POST',
118
+ headers: { 'Authorization': `Bearer ${token}` },
119
+ body: formData
120
+ });
121
+ if (res.ok) {
122
+ toast.success("Report branding updated! Future PDF reports will include your logo.");
123
+ fetchBrandingManual();
124
+ } else {
125
+ const data = await res.json();
126
+ toast.error(data.message || "Failed to update report branding.");
127
+ }
128
+ } catch (err) {
129
+ toast.error("Error uploading logo.");
130
+ } finally {
131
+ setUploadingLogo(false);
132
+ }
133
+ };
134
+
135
+ const handleDragOver = (e) => {
136
+ e.preventDefault();
137
+ e.stopPropagation();
138
+ if (e.dataTransfer) {
139
+ e.dataTransfer.dropEffect = 'copy';
140
+ }
141
+ if (!isDragging) setIsDragging(true);
142
+ };
143
+
144
+ const handleDragLeave = (e) => {
145
+ e.preventDefault();
146
+ e.stopPropagation();
147
+ // Only set false if leaving the main drop container
148
+ if (e.currentTarget && e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) {
149
+ return;
150
+ }
151
+ setIsDragging(false);
152
+ };
153
+
154
+ const handleDrop = async (e) => {
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ setIsDragging(false);
158
+
159
+ // 1. Direct File Drop (from File Explorer or Desktop)
160
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
161
+ const file = e.dataTransfer.files[0];
162
+ if (file && (file.type.startsWith('image/') || file.type === '' || file.name.match(/\.(png|jpe?g|webp|svg|gif|bmp)$/i))) {
163
+ await handleUploadLogoFile(file);
164
+ return;
165
+ }
166
+ }
167
+
168
+ // 2. DataTransfer Items (Dragging image element or file item from browser window)
169
+ if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
170
+ for (let i = 0; i < e.dataTransfer.items.length; i++) {
171
+ const item = e.dataTransfer.items[i];
172
+ if (item.kind === 'file') {
173
+ const file = item.getAsFile();
174
+ if (file && (file.type.startsWith('image/') || file.name.match(/\.(png|jpe?g|webp|svg|gif|bmp)$/i))) {
175
+ await handleUploadLogoFile(file);
176
+ return;
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ // 3. Chrome / Web Image / HTML / URL Drag
183
+ const htmlData = e.dataTransfer.getData('text/html');
184
+ const uriData = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('URL') || e.dataTransfer.getData('text/plain');
185
+
186
+ let imageUrl = '';
187
+ if (htmlData) {
188
+ try {
189
+ const parser = new DOMParser();
190
+ const doc = parser.parseFromString(htmlData, 'text/html');
191
+ const img = doc.querySelector('img');
192
+ if (img && img.src) {
193
+ imageUrl = img.src;
194
+ }
195
+ } catch (err) {
196
+ console.warn("Could not parse dragged HTML", err);
197
+ }
198
+ }
199
+
200
+ if (!imageUrl && uriData && uriData.trim().match(/^https?:\/\/.+/i)) {
201
+ imageUrl = uriData.trim();
202
+ }
203
+
204
+ if (imageUrl) {
205
+ // Base64 Data URL handling
206
+ if (imageUrl.startsWith('data:image/')) {
207
+ try {
208
+ const arr = imageUrl.split(',');
209
+ const mime = arr[0].match(/:(.*?);/)[1];
210
+ const bstr = atob(arr[1]);
211
+ let n = bstr.length;
212
+ const u8arr = new Uint8Array(n);
213
+ while (n--) {
214
+ u8arr[n] = bstr.charCodeAt(n);
215
+ }
216
+ const file = new File([u8arr], 'dragged_logo.png', { type: mime });
217
+ await handleUploadLogoFile(file);
218
+ } catch (err) {
219
+ toast.error("Invalid base64 image data.");
220
+ }
221
+ return;
222
+ }
223
+
224
+ // Web HTTP/HTTPS URL handling - First try frontend fetch, fallback to backend fetch
225
+ setUploadingLogo(true);
226
+ try {
227
+ const res = await fetch(imageUrl, { mode: 'cors' });
228
+ if (!res.ok) throw new Error("CORS or HTTP error");
229
+ const blob = await res.blob();
230
+ const contentType = blob.type || 'image/png';
231
+ const fileExt = contentType.split('/')[1] || 'png';
232
+ const file = new File([blob], `dragged_logo.${fileExt}`, { type: contentType });
233
+ await handleUploadLogoFile(file);
234
+ } catch (frontendErr) {
235
+ // Fallback: send web image URL to backend to download server-side (bypasses CORS!)
236
+ await handleUploadLogoUrl(imageUrl);
237
+ } finally {
238
+ setUploadingLogo(false);
239
+ }
240
+ return;
241
+ }
242
+
243
+ toast.error("Please drop a valid image file (PNG, JPG, WebP, SVG).");
244
+ };
245
+
246
  const [passwordData, setPasswordData] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
247
  const [passwordStatus, setPasswordStatus] = useState({ loading: false, error: null, success: false });
248
  const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false });
 
611
 
612
  {/* Second Row for Report Branding */}
613
  {(profile.role === 'super_admin' || profile.role === 'org_admin') && (
614
+ <div className="w-full bg-white border border-[#e5e7eb] rounded-xl shadow-xs p-6 md:p-8 mt-6">
615
  <div className="flex items-center gap-2 mb-1">
616
+ <span className="material-symbols-outlined text-[#2563eb] text-[22px]">palette</span>
617
+ <h3 className="font-bold text-[#111827] text-[18px] m-0">
618
  Report Branding
619
  </h3>
620
  </div>
621
+ <p className="text-[#4b5563] text-sm mt-1 mb-5 m-0">
622
  Customize generated PDF security reports with your organization's logo.
623
  </p>
624
 
625
+ <div className="border-t border-[#f3f4f6] pt-6">
626
+ <div
627
+ onDragOver={handleDragOver}
628
+ onDragEnter={handleDragOver}
629
+ onDragLeave={handleDragLeave}
630
+ onDrop={handleDrop}
631
+ onClick={() => fileInputRef.current?.click()}
632
+ className={`relative border-2 border-dashed rounded-xl py-12 px-6 flex flex-col items-center justify-center text-center transition-all duration-200 cursor-pointer ${
633
+ isDragging
634
+ ? 'border-[#2563eb] bg-[#eff6ff] scale-[1.005]'
635
+ : 'border-[#d1d5db] bg-[#f9fafb] hover:border-[#9ca3af] hover:bg-[#f3f4f6]'
636
+ }`}
637
+ >
638
+ <input
639
+ ref={fileInputRef}
640
+ type="file"
641
+ accept="image/*"
642
+ onChange={(e) => {
643
+ if (e.target.files && e.target.files[0]) {
644
+ handleUploadLogoFile(e.target.files[0]);
645
+ }
646
+ }}
647
+ className="hidden"
648
+ />
649
 
650
+ <div className="pointer-events-none flex flex-col items-center justify-center">
651
+ <span className={`material-symbols-outlined text-[36px] text-[#2563eb] mb-3 transition-transform ${isDragging ? 'scale-110' : ''} ${uploadingLogo ? 'animate-spin' : ''}`}>
652
+ {uploadingLogo ? 'sync' : 'cloud'}
653
+ </span>
654
+
655
+ <h4 className="font-bold text-[#111827] text-[16px] mb-1 m-0">
656
+ {isDragging ? 'Drop Image Here to Upload' : 'Upload Organization Logo'}
657
+ </h4>
658
+ <p className="text-[#6b7280] text-sm mt-1 mb-5 m-0">
659
+ {isDragging ? 'Release to upload your custom logo immediately' : 'Upload your custom logo to brand all PDF security reports'}
660
+ </p>
661
+
662
+ {reportLogoUrl && (
663
+ <div className="mb-5 p-3 bg-white rounded-lg border border-[#e5e7eb] shadow-2xs flex items-center justify-center pointer-events-auto">
664
+ <img src={reportLogoUrl} alt="Organization Logo" className="max-h-16 max-w-xs object-contain" />
665
+ </div>
666
+ )}
667
+ </div>
668
 
669
  <button
670
  type="button"
671
+ disabled={uploadingLogo}
672
+ onClick={(e) => {
673
+ e.stopPropagation();
674
+ fileInputRef.current?.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  }}
676
+ className="relative z-20 bg-[#2563eb] hover:bg-[#1d4ed8] text-white font-semibold px-5 py-2.5 rounded-lg flex items-center gap-2 text-sm shadow-xs transition-colors cursor-pointer mb-4 disabled:opacity-50"
677
  >
678
+ <span className={`material-symbols-outlined text-[18px] ${uploadingLogo ? 'animate-spin' : ''}`}>
679
+ {uploadingLogo ? 'sync' : 'upload'}
680
+ </span>
681
+ <span>{uploadingLogo ? 'Uploading...' : (reportLogoUrl ? 'Change Logo' : 'Upload Here')}</span>
682
  </button>
683
 
684
+ <p className="text-[#6b7280] text-[12px] m-0 font-normal pointer-events-none">
685
  Supported formats: PNG, JPG, WebP, SVG (Max 5MB)
686
  </p>
687
  </div>