larxius commited on
Commit
02cbfe4
·
verified ·
1 Parent(s): cbe4dc8

Update frontend/src/pages/Profile.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/Profile.jsx +622 -608
frontend/src/pages/Profile.jsx CHANGED
@@ -1,608 +1,622 @@
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);
10
- const [reportLogoUrl, setReportLogoUrl] = useState('');
11
-
12
- useEffect(() => {
13
- const fetchProfile = async () => {
14
- try {
15
- const res = await fetch('/api/auth/profile', {
16
- headers: { 'Authorization': `Bearer ${token}` }
17
- });
18
-
19
- if (res.ok) {
20
- const data = await res.json();
21
- setProfile(data.user);
22
- } else {
23
- setError("Failed to fetch profile data. Please try again.");
24
- }
25
- } catch (err) {
26
- setError("Network error while fetching profile data.");
27
- console.error(err);
28
- } finally {
29
- setLoading(false);
30
- }
31
- };
32
-
33
- const fetchBranding = async () => {
34
- try {
35
- const res = await fetch('/api/auth/organizations/webhook', {
36
- headers: { 'Authorization': `Bearer ${token}` }
37
- });
38
- if (res.ok) {
39
- const data = await res.json();
40
- setReportLogoUrl(data.report_logo_url || '');
41
- }
42
- } catch (err) {
43
- console.error("Error loading branding info", err);
44
- }
45
- };
46
-
47
- fetchProfile();
48
- fetchBranding();
49
- }, [token]);
50
-
51
- const fetchBrandingManual = async () => {
52
- try {
53
- const res = await fetch('/api/auth/organizations/webhook', {
54
- headers: { 'Authorization': `Bearer ${token}` }
55
- });
56
- if (res.ok) {
57
- const data = await res.json();
58
- setReportLogoUrl(data.report_logo_url || '');
59
- }
60
- } catch (err) {
61
- console.error("Error loading branding info", err);
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 });
68
-
69
- const [showEditModal, setShowEditModal] = useState(false);
70
- const [editData, setEditData] = useState({ first_name: '', last_name: '', email: '', contact_no: '', org_name: '' });
71
- const [editStatus, setEditStatus] = useState({ loading: false, error: null });
72
-
73
- const handleEditOpen = () => {
74
- setEditData({
75
- first_name: profile.first_name || '',
76
- last_name: profile.last_name || '',
77
- email: profile.email || '',
78
- contact_no: profile.contact_no || '',
79
- org_name: profile.org_name || 'LarShield Organization'
80
- });
81
- setShowEditModal(true);
82
- };
83
-
84
- const handleEditSubmit = async (e) => {
85
- e.preventDefault();
86
- setEditStatus({ loading: true, error: null });
87
- try {
88
- // Update User Profile
89
- const userRes = await fetch(`/api/auth/users/${profile.id}`, {
90
- method: 'PUT',
91
- headers: {
92
- 'Content-Type': 'application/json',
93
- 'Authorization': `Bearer ${token}`
94
- },
95
- body: JSON.stringify({
96
- ...profile,
97
- first_name: editData.first_name,
98
- last_name: editData.last_name,
99
- email: editData.email,
100
- contact_no: editData.contact_no
101
- })
102
- });
103
- const userData = await userRes.json();
104
-
105
- if (!userRes.ok) {
106
- setEditStatus({ loading: false, error: userData.message || "Failed to update profile" });
107
- toast.error(userData.message || "Failed to update profile");
108
- return;
109
- }
110
-
111
- // Update Organization Name if changed and user has permission
112
- if (editData.org_name !== profile.org_name && (profile.role === 'org_admin' || profile.role === 'super_admin')) {
113
- const orgRes = await fetch(`/api/auth/organizations/${profile.org_id}`, {
114
- method: 'PUT',
115
- headers: {
116
- 'Content-Type': 'application/json',
117
- 'Authorization': `Bearer ${token}`
118
- },
119
- body: JSON.stringify({ name: editData.org_name })
120
- });
121
-
122
- if (!orgRes.ok) {
123
- const orgData = await orgRes.json();
124
- toast.error(orgData.message || "Failed to update organization name");
125
- }
126
- }
127
-
128
- toast.success("Profile updated successfully!");
129
- setProfile({
130
- ...profile,
131
- first_name: editData.first_name,
132
- last_name: editData.last_name,
133
- email: editData.email,
134
- contact_no: editData.contact_no,
135
- org_name: editData.org_name
136
- });
137
- setShowEditModal(false);
138
- setEditStatus({ loading: false, error: null });
139
-
140
- } catch (err) {
141
- setEditStatus({ loading: false, error: "Network error" });
142
- toast.error("Network error");
143
- }
144
- };
145
-
146
- const handlePasswordChange = async (e) => {
147
- e.preventDefault();
148
- setPasswordStatus({ loading: true, error: null, success: false });
149
-
150
- if (passwordData.newPassword !== passwordData.confirmPassword) {
151
- setPasswordStatus({ loading: false, error: "New passwords do not match", success: false });
152
- return;
153
- }
154
-
155
- if (passwordData.newPassword.length < 6) {
156
- setPasswordStatus({ loading: false, error: "New password must be at least 6 characters", success: false });
157
- return;
158
- }
159
-
160
- try {
161
- const res = await fetch('/api/auth/password', {
162
- method: 'PUT',
163
- headers: {
164
- 'Content-Type': 'application/json',
165
- 'Authorization': `Bearer ${token}`
166
- },
167
- body: JSON.stringify({
168
- currentPassword: passwordData.currentPassword,
169
- newPassword: passwordData.newPassword
170
- })
171
- });
172
-
173
- let data = {};
174
- try {
175
- data = await res.json();
176
- } catch (e) {
177
- if (res.status === 429) {
178
- data = { message: "Too many attempts. Please try again later." };
179
- } else {
180
- data = { message: "Unexpected server error occurred." };
181
- }
182
- }
183
-
184
- if (res.ok) {
185
- setPasswordStatus({ loading: false, error: null, success: true });
186
- setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
187
- toast.success("Password updated successfully!");
188
- setTimeout(() => setPasswordStatus(prev => ({ ...prev, success: false })), 3000);
189
- } else {
190
- const errorMsg = data.message || "Failed to update password";
191
- setPasswordStatus({ loading: false, error: errorMsg, success: false });
192
- toast.error(errorMsg);
193
- }
194
- } catch (err) {
195
- setPasswordStatus({ loading: false, error: "Network error occurred", success: false });
196
- toast.error("Network error occurred");
197
- }
198
- };
199
-
200
- if (loading) {
201
- return (
202
- <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
203
- <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
204
- Loading Profile Data...
205
- </div>
206
- );
207
- }
208
-
209
- if (error || !profile) {
210
- return (
211
- <div className="text-center py-2xl bg-surface-container-lowest border border-outline-variant rounded-xl max-w-lg mx-auto p-xl flex flex-col items-center gap-md">
212
- <span className="material-symbols-outlined text-[48px] text-error">error</span>
213
- <h2 className="font-headline-md text-on-surface">Unable to load profile</h2>
214
- <p className="font-body-md text-on-surface-variant">{error || "Profile data not found."}</p>
215
- </div>
216
- );
217
- }
218
-
219
-
220
-
221
- return (
222
- <div className="flex flex-col gap-6 text-left w-full max-w-7xl mx-auto pb-8">
223
- {/* Main Grid Layout - 3 Equal Columns */}
224
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
225
-
226
- {/* Card 1: Identity Card */}
227
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full">
228
- <div className="flex items-center justify-between border-b border-outline-variant pb-4 mb-4">
229
- <h3 className="font-semibold text-on-surface m-0 flex items-center gap-2 text-lg">
230
- <span className="material-symbols-outlined text-primary">person</span>
231
- Organization Profile
232
- </h3>
233
- <button onClick={handleEditOpen} className="text-primary hover:text-on-primary-container bg-primary-container/20 hover:bg-primary-container px-3 py-1.5 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors flex items-center gap-1 cursor-pointer border-none">
234
- <span className="material-symbols-outlined text-[14px]">edit</span>
235
- Edit Profile
236
- </button>
237
- </div>
238
-
239
- <div className="flex flex-col items-center text-center mt-2 mb-6">
240
- <div className="w-20 h-20 rounded-full bg-primary-container/50 flex items-center justify-center mb-4">
241
- <span className="text-primary text-3xl font-bold uppercase">
242
- {profile.email ? profile.email.charAt(0) : '?'}
243
- </span>
244
- </div>
245
- <h2 className="font-bold text-on-surface m-0 text-xl">
246
- {profile.first_name || profile.last_name ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() : profile.email.split('@')[0]}
247
- </h2>
248
- <div className="mt-2 inline-flex items-center px-3 py-1 rounded-full bg-primary-container/30 text-primary font-semibold uppercase tracking-wider text-xs border border-primary/20">
249
- {(profile.role || 'user').replace(/_/g, ' ')}
250
- </div>
251
- </div>
252
-
253
- <div className="flex flex-col w-full mb-6">
254
- <div className="flex items-center justify-between py-3 border-b border-outline-variant">
255
- <div className="flex items-center gap-2 text-on-surface-variant">
256
- <span className="material-symbols-outlined text-[18px]">mail</span>
257
- <span className="font-medium text-sm">Email</span>
258
- </div>
259
- <span className="text-on-surface font-medium text-sm truncate max-w-[150px]" title={profile.email}>{profile.email}</span>
260
- </div>
261
-
262
- <div className="flex items-center justify-between py-3 border-b border-outline-variant">
263
- <div className="flex items-center gap-2 text-on-surface-variant">
264
- <span className="material-symbols-outlined text-[18px]">calendar_today</span>
265
- <span className="font-medium text-sm">Joined</span>
266
- </div>
267
- <span className="text-on-surface font-medium text-sm">
268
- {profile.created_at ? new Date(profile.created_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-') : 'N/A'}
269
- </span>
270
- </div>
271
- </div>
272
-
273
- <div className="mt-auto">
274
- <button
275
- onClick={logout}
276
- className="w-full bg-transparent text-error border border-error/50 hover:bg-error/10 font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 text-sm cursor-pointer"
277
- >
278
- <span className="material-symbols-outlined text-[18px]">logout</span>
279
- Sign Out Securely
280
- </button>
281
- </div>
282
- </div>
283
-
284
- {/* Card 2: Security Configuration */}
285
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full">
286
- <h3 className="font-semibold text-on-surface m-0 mb-4 flex items-center gap-2 border-b border-outline-variant pb-4 text-lg">
287
- <span className="material-symbols-outlined text-primary">security</span>
288
- Security Config
289
- </h3>
290
-
291
- <div className="flex flex-col flex-1">
292
- <div className="mb-5">
293
- <h4 className="font-semibold text-on-surface m-0 text-sm">Password Management</h4>
294
- <p className="text-on-surface-variant m-0 mt-1 text-xs">Update your account password securely.</p>
295
- </div>
296
-
297
- {(profile.role === 'soc_analyst' || profile.role === 'executive_user') ? (
298
- <div className="bg-error/10 rounded-lg p-4 flex flex-col items-center text-center gap-3 text-error border border-error/20 mt-auto mb-auto">
299
- <span className="material-symbols-outlined text-[24px]">lock</span>
300
- <p className="text-sm m-0 font-medium">
301
- Your account type is not permitted to change its own password. Please contact your administrator.
302
- </p>
303
- </div>
304
- ) : (
305
- <form onSubmit={handlePasswordChange} className="flex flex-col gap-4 flex-1">
306
- {passwordStatus.error && (
307
- <div className="bg-error/10 text-error px-md py-sm rounded-lg text-sm border border-error/20 flex items-center gap-2">
308
- <span className="material-symbols-outlined text-[16px]">error</span>
309
- {passwordStatus.error}
310
- </div>
311
- )}
312
- {passwordStatus.success && (
313
- <div className="bg-primary-container/20 text-primary px-md py-sm rounded-lg text-sm border border-primary/20 flex items-center gap-2">
314
- <span className="material-symbols-outlined text-[16px]">check_circle</span>
315
- Password updated successfully!
316
- </div>
317
- )}
318
-
319
- <div className="flex flex-col">
320
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Current Password</label>
321
- <div className="relative">
322
- <input
323
- type={showPassword.current ? "text" : "password"}
324
- required
325
- value={passwordData.currentPassword}
326
- onChange={(e) => setPasswordData({ ...passwordData, currentPassword: e.target.value })}
327
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10"
328
- />
329
- <button type="button" onClick={() => setShowPassword({ ...showPassword, current: !showPassword.current })} className="absolute inset-y-0 right-0 pr-3 flex items-center text-on-surface-variant hover:text-on-surface bg-transparent border-none cursor-pointer">
330
- <span className="material-symbols-outlined text-[18px]">{showPassword.current ? 'visibility_off' : 'visibility'}</span>
331
- </button>
332
- </div>
333
- </div>
334
-
335
- <div className="flex flex-col">
336
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">New Password</label>
337
- <div className="relative">
338
- <input
339
- type={showPassword.new ? "text" : "password"}
340
- required
341
- minLength={6}
342
- value={passwordData.newPassword}
343
- onChange={(e) => setPasswordData({ ...passwordData, newPassword: e.target.value })}
344
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10"
345
- />
346
- <button type="button" onClick={() => setShowPassword({ ...showPassword, new: !showPassword.new })} className="absolute inset-y-0 right-0 pr-3 flex items-center text-on-surface-variant hover:text-on-surface bg-transparent border-none cursor-pointer">
347
- <span className="material-symbols-outlined text-[18px]">{showPassword.new ? 'visibility_off' : 'visibility'}</span>
348
- </button>
349
- </div>
350
- </div>
351
-
352
- <div className="flex flex-col">
353
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Confirm New Password</label>
354
- <div className="relative">
355
- <input
356
- type={showPassword.confirm ? "text" : "password"}
357
- required
358
- minLength={6}
359
- value={passwordData.confirmPassword}
360
- onChange={(e) => setPasswordData({ ...passwordData, confirmPassword: e.target.value })}
361
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10"
362
- />
363
- <button type="button" onClick={() => setShowPassword({ ...showPassword, confirm: !showPassword.confirm })} className="absolute inset-y-0 right-0 pr-3 flex items-center text-on-surface-variant hover:text-on-surface bg-transparent border-none cursor-pointer">
364
- <span className="material-symbols-outlined text-[18px]">{showPassword.confirm ? 'visibility_off' : 'visibility'}</span>
365
- </button>
366
- </div>
367
- </div>
368
-
369
- <div className="mt-auto pt-2">
370
- <button
371
- type="submit"
372
- disabled={passwordStatus.loading}
373
- className="w-full bg-primary hover:brightness-110 border-none cursor-pointer text-on-primary font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50 text-sm"
374
- >
375
- {passwordStatus.loading ? (
376
- <><span className="material-symbols-outlined animate-spin text-[18px]">sync</span> Updating...</>
377
- ) : (
378
- <><span className="material-symbols-outlined text-[18px]">key</span> Update Password</>
379
- )}
380
- </button>
381
- </div>
382
- </form>
383
- )}
384
- </div>
385
- </div>
386
-
387
- {/* Card 3: Subscription Card */}
388
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full">
389
- <h3 className="font-semibold text-on-surface m-0 mb-6 flex items-center gap-2 border-b border-outline-variant pb-4 text-lg">
390
- <span className="material-symbols-outlined text-primary">workspace_premium</span>
391
- Subscription Status
392
- </h3>
393
-
394
- <div className="flex flex-col w-full mb-6 flex-1">
395
- <div className="flex items-center justify-between py-4 border-b border-outline-variant">
396
- <div className="flex items-center gap-2 text-on-surface-variant">
397
- <span className="material-symbols-outlined text-[20px]">inventory_2</span>
398
- <span className="font-medium text-sm">Current Plan</span>
399
- </div>
400
- <span className="text-primary font-bold text-sm uppercase tracking-widest bg-primary-container/30 px-3 py-1 rounded-full border border-primary/20">
401
- {profile.role === 'super_admin' ? 'Enterprise' : profile.subscription_tier || 'Free'}
402
- </span>
403
- </div>
404
-
405
- <div className="flex items-center justify-between py-4 border-b border-outline-variant">
406
- <div className="flex items-center gap-2 text-on-surface-variant">
407
- <span className="material-symbols-outlined text-[20px]">speed</span>
408
- <span className="font-medium text-sm">Status</span>
409
- </div>
410
- <div className="flex items-center gap-2">
411
- <div className={`w-2 h-2 rounded-full ${profile.role === 'super_admin' || profile.subscription_status === 'active' ? 'bg-green-500' : 'bg-yellow-500'}`}></div>
412
- <span className={`font-bold uppercase tracking-wide text-sm ${profile.role === 'super_admin' || profile.subscription_status === 'active' ? 'text-green-600' : 'text-yellow-600'}`}>
413
- {profile.role === 'super_admin' ? 'Active' : profile.subscription_status || 'Inactive'}
414
- </span>
415
- </div>
416
- </div>
417
- </div>
418
-
419
- <div className="mt-auto">
420
- <div className="bg-surface-variant/30 border border-outline-variant/50 rounded-lg p-4 flex items-start gap-3">
421
- <span className="material-symbols-outlined text-primary text-[18px] shrink-0 mt-0.5">info</span>
422
- <p className="text-on-surface-variant m-0 text-xs leading-relaxed">
423
- Need higher API limits, custom proxy integrations, or VIP enterprise support? Contact your organization admin or LarShield sales to upgrade your tier.
424
- </p>
425
- </div>
426
- </div>
427
- </div>
428
-
429
- </div>
430
-
431
- {/* Second Row for Report Branding */}
432
- {(profile.role === 'super_admin' || profile.role === 'org_admin') && (
433
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch mt-6">
434
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full lg:col-span-1">
435
- <h3 className="font-semibold text-on-surface m-0 mb-4 flex items-center gap-2 border-b border-outline-variant pb-4 text-lg">
436
- <span className="material-symbols-outlined text-primary">palette</span>
437
- Report Branding
438
- </h3>
439
- <p className="text-on-surface-variant text-sm mb-6">
440
- Upload your organization's logo to customize PDF reports.
441
- </p>
442
- <div className="flex flex-col flex-1 items-center justify-center border-2 border-dashed border-outline-variant rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer p-6"
443
- onClick={() => {
444
- const fileInput = document.createElement('input');
445
- fileInput.type = 'file';
446
- fileInput.accept = 'image/*';
447
- fileInput.onchange = async (e) => {
448
- const file = e.target.files[0];
449
- if (!file) return;
450
- const formData = new FormData();
451
- formData.append('logo', file);
452
- try {
453
- const res = await fetch('/api/auth/organizations/logo', {
454
- method: 'POST',
455
- headers: { 'Authorization': `Bearer ${token}` },
456
- body: formData
457
- });
458
- if (res.ok) {
459
- toast.success("Report branding updated! Future PDF reports will include your logo.");
460
- fetchBrandingManual();
461
- } else {
462
- const data = await res.json();
463
- toast.error(data.message || "Failed to update report branding.");
464
- }
465
- } catch (err) {
466
- toast.error("Error uploading logo.");
467
- }
468
- };
469
- fileInput.click();
470
- }}>
471
- {reportLogoUrl ? (
472
- <div className="flex flex-col items-center">
473
- <img src={reportLogoUrl} alt="Organization Logo" className="max-h-[100px] max-w-full object-contain rounded mb-4" />
474
- <div className="flex items-center gap-1 text-primary font-bold text-sm">
475
- <span className="material-symbols-outlined text-[16px]">change_circle</span>
476
- <span>Click to change logo</span>
477
- </div>
478
- </div>
479
- ) : (
480
- <div className="flex flex-col items-center text-center">
481
- <span className="material-symbols-outlined text-[40px] text-primary/60 mb-2">cloud_upload</span>
482
- <span className="font-bold text-on-surface text-sm">Click to upload logo</span>
483
- <span className="text-[12px] text-on-surface-variant mt-1">PNG, JPG up to 5MB</span>
484
- </div>
485
- )}
486
- </div>
487
- </div>
488
- </div>
489
- )}
490
-
491
- {/* Edit Profile Modal */}
492
- {showEditModal && (
493
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-fade-in">
494
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg overflow-hidden shadow-xl max-w-md w-full">
495
- <div className="p-6 border-b border-outline-variant bg-surface-container/50 flex justify-between items-center">
496
- <h3 className="text-xl font-bold flex items-center text-on-surface m-0">
497
- <span className="material-symbols-outlined text-primary mr-2">edit</span>
498
- Edit Profile
499
- </h3>
500
- <button
501
- onClick={() => setShowEditModal(false)}
502
- className="text-on-surface-variant hover:text-on-surface rounded-full p-1 transition-colors cursor-pointer border-0 bg-transparent flex items-center justify-center"
503
- >
504
- <span className="material-symbols-outlined text-[20px]">close</span>
505
- </button>
506
- </div>
507
-
508
- <form onSubmit={handleEditSubmit} className="p-6 flex flex-col gap-4">
509
- {editStatus.error && (
510
- <div className="bg-error/10 text-error px-md py-sm rounded-lg text-sm border border-error/20 flex items-center gap-2">
511
- <span className="material-symbols-outlined text-[16px]">error</span>
512
- {editStatus.error}
513
- </div>
514
- )}
515
-
516
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
517
- <div className="flex flex-col">
518
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">First Name</label>
519
- <input
520
- type="text"
521
- value={editData.first_name}
522
- onChange={(e) => setEditData({ ...editData, first_name: e.target.value })}
523
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
524
- placeholder="Enter first name"
525
- />
526
- </div>
527
-
528
- <div className="flex flex-col">
529
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Last Name</label>
530
- <input
531
- type="text"
532
- value={editData.last_name}
533
- onChange={(e) => setEditData({ ...editData, last_name: e.target.value })}
534
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
535
- placeholder="Enter last name"
536
- />
537
- </div>
538
- </div>
539
-
540
- <div className="flex flex-col">
541
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Email Address</label>
542
- <input
543
- type="email"
544
- value={editData.email}
545
- onChange={(e) => setEditData({ ...editData, email: e.target.value })}
546
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
547
- placeholder="Enter email address"
548
- required
549
- />
550
- </div>
551
-
552
- <div className="flex flex-col">
553
- <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Contact No</label>
554
- <input
555
- type="tel"
556
- value={editData.contact_no}
557
- onChange={(e) => setEditData({ ...editData, contact_no: e.target.value })}
558
- className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
559
- placeholder="+1 (555) 000-0000"
560
- />
561
- </div>
562
-
563
- <div className="flex flex-col">
564
- <div className="flex items-center justify-between mb-xs">
565
- <label className="block text-label-sm font-label-sm text-on-surface-variant">Organization Name</label>
566
- {profile.role !== 'org_admin' && profile.role !== 'super_admin' && (
567
- <span className="text-[10px] bg-surface-variant text-on-surface-variant px-2 py-0.5 rounded uppercase tracking-wider font-bold">Admin Only</span>
568
- )}
569
- </div>
570
- <input
571
- type="text"
572
- value={editData.org_name}
573
- onChange={(e) => setEditData({ ...editData, org_name: e.target.value })}
574
- disabled={profile.role !== 'org_admin' && profile.role !== 'super_admin'}
575
- className={`w-full rounded-lg px-md py-sm outline-none transition-colors ${profile.role === 'org_admin' || profile.role === 'super_admin' ? 'bg-surface-container border border-outline-variant text-on-surface focus:border-primary focus:ring-1 focus:ring-primary' : 'bg-surface-variant/50 border border-outline-variant text-on-surface-variant cursor-not-allowed'}`}
576
- />
577
- </div>
578
-
579
- <div className="mt-2 flex gap-sm justify-end">
580
- <button
581
- type="button"
582
- onClick={() => setShowEditModal(false)}
583
- className="px-xl py-sm bg-transparent border border-outline-variant rounded-lg font-label-md text-on-surface hover:bg-surface-variant transition-colors cursor-pointer"
584
- >
585
- Cancel
586
- </button>
587
- <button
588
- type="submit"
589
- disabled={editStatus.loading}
590
- className="px-xl py-sm bg-primary text-on-primary rounded-lg font-label-md hover:brightness-110 transition-all border-none cursor-pointer flex items-center justify-center gap-2 disabled:opacity-50"
591
- >
592
- {editStatus.loading ? (
593
- <><span className="material-symbols-outlined animate-spin text-[16px]">sync</span> Saving...</>
594
- ) : (
595
- "Save Changes"
596
- )}
597
- </button>
598
- </div>
599
- </form>
600
- </div>
601
- </div>
602
- )}
603
- </div>
604
- );
605
- };
606
-
607
- export default Profile;
608
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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);
10
+ const [reportLogoUrl, setReportLogoUrl] = useState('');
11
+
12
+ useEffect(() => {
13
+ const fetchProfile = async () => {
14
+ try {
15
+ const res = await fetch('/api/auth/profile', {
16
+ headers: { 'Authorization': `Bearer ${token}` }
17
+ });
18
+
19
+ if (res.ok) {
20
+ const data = await res.json();
21
+ setProfile(data.user);
22
+ } else {
23
+ setError("Failed to fetch profile data. Please try again.");
24
+ }
25
+ } catch (err) {
26
+ setError("Network error while fetching profile data.");
27
+ console.error(err);
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ const fetchBranding = async () => {
34
+ try {
35
+ const res = await fetch('/api/auth/organizations/webhook', {
36
+ headers: { 'Authorization': `Bearer ${token}` }
37
+ });
38
+ if (res.ok) {
39
+ const data = await res.json();
40
+ setReportLogoUrl(data.report_logo_url || '');
41
+ }
42
+ } catch (err) {
43
+ console.error("Error loading branding info", err);
44
+ }
45
+ };
46
+
47
+ fetchProfile();
48
+ fetchBranding();
49
+ }, [token]);
50
+
51
+ const fetchBrandingManual = async () => {
52
+ try {
53
+ const res = await fetch('/api/auth/organizations/webhook', {
54
+ headers: { 'Authorization': `Bearer ${token}` }
55
+ });
56
+ if (res.ok) {
57
+ const data = await res.json();
58
+ setReportLogoUrl(data.report_logo_url || '');
59
+ }
60
+ } catch (err) {
61
+ console.error("Error loading branding info", err);
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 });
68
+
69
+ const [showEditModal, setShowEditModal] = useState(false);
70
+ const [editData, setEditData] = useState({ first_name: '', last_name: '', email: '', contact_no: '', org_name: '' });
71
+ const [editStatus, setEditStatus] = useState({ loading: false, error: null });
72
+
73
+ const handleEditOpen = () => {
74
+ setEditData({
75
+ first_name: profile.first_name || '',
76
+ last_name: profile.last_name || '',
77
+ email: profile.email || '',
78
+ contact_no: profile.contact_no || '',
79
+ org_name: profile.org_name || 'LarShield Organization'
80
+ });
81
+ setShowEditModal(true);
82
+ };
83
+
84
+ const handleEditSubmit = async (e) => {
85
+ e.preventDefault();
86
+ setEditStatus({ loading: true, error: null });
87
+ try {
88
+ // Update User Profile
89
+ const userRes = await fetch(`/api/auth/users/${profile.id}`, {
90
+ method: 'PUT',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ 'Authorization': `Bearer ${token}`
94
+ },
95
+ body: JSON.stringify({
96
+ ...profile,
97
+ first_name: editData.first_name,
98
+ last_name: editData.last_name,
99
+ email: editData.email,
100
+ contact_no: editData.contact_no
101
+ })
102
+ });
103
+ const userData = await userRes.json();
104
+
105
+ if (!userRes.ok) {
106
+ setEditStatus({ loading: false, error: userData.message || "Failed to update profile" });
107
+ toast.error(userData.message || "Failed to update profile");
108
+ return;
109
+ }
110
+
111
+ // Update Organization Name if changed and user has permission
112
+ if (editData.org_name !== profile.org_name && (profile.role === 'org_admin' || profile.role === 'super_admin')) {
113
+ const orgRes = await fetch(`/api/auth/organizations/${profile.org_id}`, {
114
+ method: 'PUT',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ 'Authorization': `Bearer ${token}`
118
+ },
119
+ body: JSON.stringify({ name: editData.org_name })
120
+ });
121
+
122
+ if (!orgRes.ok) {
123
+ const orgData = await orgRes.json();
124
+ toast.error(orgData.message || "Failed to update organization name");
125
+ }
126
+ }
127
+
128
+ toast.success("Profile updated successfully!");
129
+ setProfile({
130
+ ...profile,
131
+ first_name: editData.first_name,
132
+ last_name: editData.last_name,
133
+ email: editData.email,
134
+ contact_no: editData.contact_no,
135
+ org_name: editData.org_name
136
+ });
137
+ setShowEditModal(false);
138
+ setEditStatus({ loading: false, error: null });
139
+
140
+ } catch (err) {
141
+ setEditStatus({ loading: false, error: "Network error" });
142
+ toast.error("Network error");
143
+ }
144
+ };
145
+
146
+ const handlePasswordChange = async (e) => {
147
+ e.preventDefault();
148
+ setPasswordStatus({ loading: true, error: null, success: false });
149
+
150
+ if (passwordData.newPassword !== passwordData.confirmPassword) {
151
+ setPasswordStatus({ loading: false, error: "New passwords do not match", success: false });
152
+ return;
153
+ }
154
+
155
+ if (passwordData.newPassword.length < 6) {
156
+ setPasswordStatus({ loading: false, error: "New password must be at least 6 characters", success: false });
157
+ return;
158
+ }
159
+
160
+ try {
161
+ const res = await fetch('/api/auth/password', {
162
+ method: 'PUT',
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ 'Authorization': `Bearer ${token}`
166
+ },
167
+ body: JSON.stringify({
168
+ currentPassword: passwordData.currentPassword,
169
+ newPassword: passwordData.newPassword
170
+ })
171
+ });
172
+
173
+ let data = {};
174
+ try {
175
+ data = await res.json();
176
+ } catch (e) {
177
+ if (res.status === 429) {
178
+ data = { message: "Too many attempts. Please try again later." };
179
+ } else {
180
+ data = { message: "Unexpected server error occurred." };
181
+ }
182
+ }
183
+
184
+ if (res.ok) {
185
+ setPasswordStatus({ loading: false, error: null, success: true });
186
+ setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
187
+ toast.success("Password updated successfully!");
188
+ setTimeout(() => setPasswordStatus(prev => ({ ...prev, success: false })), 3000);
189
+ } else {
190
+ const errorMsg = data.message || "Failed to update password";
191
+ setPasswordStatus({ loading: false, error: errorMsg, success: false });
192
+ toast.error(errorMsg);
193
+ }
194
+ } catch (err) {
195
+ setPasswordStatus({ loading: false, error: "Network error occurred", success: false });
196
+ toast.error("Network error occurred");
197
+ }
198
+ };
199
+
200
+ if (loading) {
201
+ return (
202
+ <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
203
+ <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
204
+ Loading Profile Data...
205
+ </div>
206
+ );
207
+ }
208
+
209
+ if (error || !profile) {
210
+ return (
211
+ <div className="text-center py-2xl bg-surface-container-lowest border border-outline-variant rounded-xl max-w-lg mx-auto p-xl flex flex-col items-center gap-md">
212
+ <span className="material-symbols-outlined text-[48px] text-error">error</span>
213
+ <h2 className="font-headline-md text-on-surface">Unable to load profile</h2>
214
+ <p className="font-body-md text-on-surface-variant">{error || "Profile data not found."}</p>
215
+ </div>
216
+ );
217
+ }
218
+
219
+
220
+
221
+ return (
222
+ <div className="flex flex-col gap-6 text-left w-full max-w-7xl mx-auto pb-8">
223
+ {/* Main Grid Layout - 3 Equal Columns */}
224
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
225
+
226
+ {/* Card 1: Identity Card */}
227
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full">
228
+ <div className="flex items-center justify-between border-b border-outline-variant pb-4 mb-4">
229
+ <h3 className="font-semibold text-on-surface m-0 flex items-center gap-2 text-lg">
230
+ <span className="material-symbols-outlined text-primary">person</span>
231
+ Organization Profile
232
+ </h3>
233
+ <button onClick={handleEditOpen} className="text-primary hover:text-on-primary-container bg-primary-container/20 hover:bg-primary-container px-3 py-1.5 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors flex items-center gap-1 cursor-pointer border-none">
234
+ <span className="material-symbols-outlined text-[14px]">edit</span>
235
+ Edit Profile
236
+ </button>
237
+ </div>
238
+
239
+ <div className="flex flex-col items-center text-center mt-2 mb-6">
240
+ <div className="w-20 h-20 rounded-full bg-primary-container/50 flex items-center justify-center mb-4">
241
+ <span className="text-primary text-3xl font-bold uppercase">
242
+ {profile.email ? profile.email.charAt(0) : '?'}
243
+ </span>
244
+ </div>
245
+ <h2 className="font-bold text-on-surface m-0 text-xl">
246
+ {profile.first_name || profile.last_name ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() : profile.email.split('@')[0]}
247
+ </h2>
248
+ <div className="mt-2 inline-flex items-center px-3 py-1 rounded-full bg-primary-container/30 text-primary font-semibold uppercase tracking-wider text-xs border border-primary/20">
249
+ {(profile.role || 'user').replace(/_/g, ' ')}
250
+ </div>
251
+ </div>
252
+
253
+ <div className="flex flex-col w-full mb-6">
254
+ <div className="flex items-center justify-between py-3 border-b border-outline-variant">
255
+ <div className="flex items-center gap-2 text-on-surface-variant">
256
+ <span className="material-symbols-outlined text-[18px]">mail</span>
257
+ <span className="font-medium text-sm">Email</span>
258
+ </div>
259
+ <span className="text-on-surface font-medium text-sm truncate max-w-[150px]" title={profile.email}>{profile.email}</span>
260
+ </div>
261
+
262
+ <div className="flex items-center justify-between py-3 border-b border-outline-variant">
263
+ <div className="flex items-center gap-2 text-on-surface-variant">
264
+ <span className="material-symbols-outlined text-[18px]">calendar_today</span>
265
+ <span className="font-medium text-sm">Joined</span>
266
+ </div>
267
+ <span className="text-on-surface font-medium text-sm">
268
+ {profile.created_at ? new Date(profile.created_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, '-') : 'N/A'}
269
+ </span>
270
+ </div>
271
+ </div>
272
+
273
+ <div className="mt-auto">
274
+ <button
275
+ onClick={logout}
276
+ className="w-full bg-transparent text-error border border-error/50 hover:bg-error/10 font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 text-sm cursor-pointer"
277
+ >
278
+ <span className="material-symbols-outlined text-[18px]">logout</span>
279
+ Sign Out Securely
280
+ </button>
281
+ </div>
282
+ </div>
283
+
284
+ {/* Card 2: Security Configuration */}
285
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full">
286
+ <h3 className="font-semibold text-on-surface m-0 mb-4 flex items-center gap-2 border-b border-outline-variant pb-4 text-lg">
287
+ <span className="material-symbols-outlined text-primary">security</span>
288
+ Security Config
289
+ </h3>
290
+
291
+ <div className="flex flex-col flex-1">
292
+ <div className="mb-5">
293
+ <h4 className="font-semibold text-on-surface m-0 text-sm">Password Management</h4>
294
+ <p className="text-on-surface-variant m-0 mt-1 text-xs">Update your account password securely.</p>
295
+ </div>
296
+
297
+ {(profile.role === 'soc_analyst' || profile.role === 'executive_user') ? (
298
+ <div className="bg-error/10 rounded-lg p-4 flex flex-col items-center text-center gap-3 text-error border border-error/20 mt-auto mb-auto">
299
+ <span className="material-symbols-outlined text-[24px]">lock</span>
300
+ <p className="text-sm m-0 font-medium">
301
+ Your account type is not permitted to change its own password. Please contact your administrator.
302
+ </p>
303
+ </div>
304
+ ) : (
305
+ <form onSubmit={handlePasswordChange} className="flex flex-col gap-4 flex-1">
306
+ {passwordStatus.error && (
307
+ <div className="bg-error/10 text-error px-md py-sm rounded-lg text-sm border border-error/20 flex items-center gap-2">
308
+ <span className="material-symbols-outlined text-[16px]">error</span>
309
+ {passwordStatus.error}
310
+ </div>
311
+ )}
312
+ {passwordStatus.success && (
313
+ <div className="bg-primary-container/20 text-primary px-md py-sm rounded-lg text-sm border border-primary/20 flex items-center gap-2">
314
+ <span className="material-symbols-outlined text-[16px]">check_circle</span>
315
+ Password updated successfully!
316
+ </div>
317
+ )}
318
+
319
+ <div className="flex flex-col">
320
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Current Password</label>
321
+ <div className="relative">
322
+ <input
323
+ type={showPassword.current ? "text" : "password"}
324
+ required
325
+ value={passwordData.currentPassword}
326
+ onChange={(e) => setPasswordData({ ...passwordData, currentPassword: e.target.value })}
327
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10"
328
+ />
329
+ <button type="button" onClick={() => setShowPassword({ ...showPassword, current: !showPassword.current })} className="absolute inset-y-0 right-0 pr-3 flex items-center text-on-surface-variant hover:text-on-surface bg-transparent border-none cursor-pointer">
330
+ <span className="material-symbols-outlined text-[18px]">{showPassword.current ? 'visibility_off' : 'visibility'}</span>
331
+ </button>
332
+ </div>
333
+ </div>
334
+
335
+ <div className="flex flex-col">
336
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">New Password</label>
337
+ <div className="relative">
338
+ <input
339
+ type={showPassword.new ? "text" : "password"}
340
+ required
341
+ minLength={6}
342
+ value={passwordData.newPassword}
343
+ onChange={(e) => setPasswordData({ ...passwordData, newPassword: e.target.value })}
344
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10"
345
+ />
346
+ <button type="button" onClick={() => setShowPassword({ ...showPassword, new: !showPassword.new })} className="absolute inset-y-0 right-0 pr-3 flex items-center text-on-surface-variant hover:text-on-surface bg-transparent border-none cursor-pointer">
347
+ <span className="material-symbols-outlined text-[18px]">{showPassword.new ? 'visibility_off' : 'visibility'}</span>
348
+ </button>
349
+ </div>
350
+ </div>
351
+
352
+ <div className="flex flex-col">
353
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Confirm New Password</label>
354
+ <div className="relative">
355
+ <input
356
+ type={showPassword.confirm ? "text" : "password"}
357
+ required
358
+ minLength={6}
359
+ value={passwordData.confirmPassword}
360
+ onChange={(e) => setPasswordData({ ...passwordData, confirmPassword: e.target.value })}
361
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors pr-10"
362
+ />
363
+ <button type="button" onClick={() => setShowPassword({ ...showPassword, confirm: !showPassword.confirm })} className="absolute inset-y-0 right-0 pr-3 flex items-center text-on-surface-variant hover:text-on-surface bg-transparent border-none cursor-pointer">
364
+ <span className="material-symbols-outlined text-[18px]">{showPassword.confirm ? 'visibility_off' : 'visibility'}</span>
365
+ </button>
366
+ </div>
367
+ </div>
368
+
369
+ <div className="mt-auto pt-2">
370
+ <button
371
+ type="submit"
372
+ disabled={passwordStatus.loading}
373
+ className="w-full bg-primary hover:brightness-110 border-none cursor-pointer text-on-primary font-medium px-4 py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50 text-sm"
374
+ >
375
+ {passwordStatus.loading ? (
376
+ <><span className="material-symbols-outlined animate-spin text-[18px]">sync</span> Updating...</>
377
+ ) : (
378
+ <><span className="material-symbols-outlined text-[18px]">key</span> Update Password</>
379
+ )}
380
+ </button>
381
+ </div>
382
+ </form>
383
+ )}
384
+ </div>
385
+ </div>
386
+
387
+ {/* Card 3: Subscription Card */}
388
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm p-6 flex flex-col h-full">
389
+ <h3 className="font-semibold text-on-surface m-0 mb-6 flex items-center gap-2 border-b border-outline-variant pb-4 text-lg">
390
+ <span className="material-symbols-outlined text-primary">workspace_premium</span>
391
+ Subscription Status
392
+ </h3>
393
+
394
+ <div className="flex flex-col w-full mb-6 flex-1">
395
+ <div className="flex items-center justify-between py-4 border-b border-outline-variant">
396
+ <div className="flex items-center gap-2 text-on-surface-variant">
397
+ <span className="material-symbols-outlined text-[20px]">inventory_2</span>
398
+ <span className="font-medium text-sm">Current Plan</span>
399
+ </div>
400
+ <span className="text-primary font-bold text-sm uppercase tracking-widest bg-primary-container/30 px-3 py-1 rounded-full border border-primary/20">
401
+ {profile.role === 'super_admin' ? 'Enterprise' : profile.subscription_tier || 'Free'}
402
+ </span>
403
+ </div>
404
+
405
+ <div className="flex items-center justify-between py-4 border-b border-outline-variant">
406
+ <div className="flex items-center gap-2 text-on-surface-variant">
407
+ <span className="material-symbols-outlined text-[20px]">speed</span>
408
+ <span className="font-medium text-sm">Status</span>
409
+ </div>
410
+ <div className="flex items-center gap-2">
411
+ <div className={`w-2 h-2 rounded-full ${profile.role === 'super_admin' || profile.subscription_status === 'active' ? 'bg-green-500' : 'bg-yellow-500'}`}></div>
412
+ <span className={`font-bold uppercase tracking-wide text-sm ${profile.role === 'super_admin' || profile.subscription_status === 'active' ? 'text-green-600' : 'text-yellow-600'}`}>
413
+ {profile.role === 'super_admin' ? 'Active' : profile.subscription_status || 'Inactive'}
414
+ </span>
415
+ </div>
416
+ </div>
417
+ </div>
418
+
419
+ <div className="mt-auto">
420
+ <div className="bg-surface-variant/30 border border-outline-variant/50 rounded-lg p-4 flex items-start gap-3">
421
+ <span className="material-symbols-outlined text-primary text-[18px] shrink-0 mt-0.5">info</span>
422
+ <p className="text-on-surface-variant m-0 text-xs leading-relaxed">
423
+ Need higher API limits, custom proxy integrations, or VIP enterprise support? Contact your organization admin or LarShield sales to upgrade your tier.
424
+ </p>
425
+ </div>
426
+ </div>
427
+ </div>
428
+
429
+ </div>
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>
501
+ </div>
502
+ </div>
503
+ )}
504
+
505
+ {/* Edit Profile Modal */}
506
+ {showEditModal && (
507
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-fade-in">
508
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-lg overflow-hidden shadow-xl max-w-md w-full">
509
+ <div className="p-6 border-b border-outline-variant bg-surface-container/50 flex justify-between items-center">
510
+ <h3 className="text-xl font-bold flex items-center text-on-surface m-0">
511
+ <span className="material-symbols-outlined text-primary mr-2">edit</span>
512
+ Edit Profile
513
+ </h3>
514
+ <button
515
+ onClick={() => setShowEditModal(false)}
516
+ className="text-on-surface-variant hover:text-on-surface rounded-full p-1 transition-colors cursor-pointer border-0 bg-transparent flex items-center justify-center"
517
+ >
518
+ <span className="material-symbols-outlined text-[20px]">close</span>
519
+ </button>
520
+ </div>
521
+
522
+ <form onSubmit={handleEditSubmit} className="p-6 flex flex-col gap-4">
523
+ {editStatus.error && (
524
+ <div className="bg-error/10 text-error px-md py-sm rounded-lg text-sm border border-error/20 flex items-center gap-2">
525
+ <span className="material-symbols-outlined text-[16px]">error</span>
526
+ {editStatus.error}
527
+ </div>
528
+ )}
529
+
530
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
531
+ <div className="flex flex-col">
532
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">First Name</label>
533
+ <input
534
+ type="text"
535
+ value={editData.first_name}
536
+ onChange={(e) => setEditData({ ...editData, first_name: e.target.value })}
537
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
538
+ placeholder="Enter first name"
539
+ />
540
+ </div>
541
+
542
+ <div className="flex flex-col">
543
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Last Name</label>
544
+ <input
545
+ type="text"
546
+ value={editData.last_name}
547
+ onChange={(e) => setEditData({ ...editData, last_name: e.target.value })}
548
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
549
+ placeholder="Enter last name"
550
+ />
551
+ </div>
552
+ </div>
553
+
554
+ <div className="flex flex-col">
555
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Email Address</label>
556
+ <input
557
+ type="email"
558
+ value={editData.email}
559
+ onChange={(e) => setEditData({ ...editData, email: e.target.value })}
560
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
561
+ placeholder="Enter email address"
562
+ required
563
+ />
564
+ </div>
565
+
566
+ <div className="flex flex-col">
567
+ <label className="block text-label-sm font-label-sm text-on-surface-variant mb-xs">Contact No</label>
568
+ <input
569
+ type="tel"
570
+ value={editData.contact_no}
571
+ onChange={(e) => setEditData({ ...editData, contact_no: e.target.value })}
572
+ className="w-full bg-surface-container border border-outline-variant text-on-surface rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-colors"
573
+ placeholder="+1 (555) 000-0000"
574
+ />
575
+ </div>
576
+
577
+ <div className="flex flex-col">
578
+ <div className="flex items-center justify-between mb-xs">
579
+ <label className="block text-label-sm font-label-sm text-on-surface-variant">Organization Name</label>
580
+ {profile.role !== 'org_admin' && profile.role !== 'super_admin' && (
581
+ <span className="text-[10px] bg-surface-variant text-on-surface-variant px-2 py-0.5 rounded uppercase tracking-wider font-bold">Admin Only</span>
582
+ )}
583
+ </div>
584
+ <input
585
+ type="text"
586
+ value={editData.org_name}
587
+ onChange={(e) => setEditData({ ...editData, org_name: e.target.value })}
588
+ disabled={profile.role !== 'org_admin' && profile.role !== 'super_admin'}
589
+ className={`w-full rounded-lg px-md py-sm outline-none transition-colors ${profile.role === 'org_admin' || profile.role === 'super_admin' ? 'bg-surface-container border border-outline-variant text-on-surface focus:border-primary focus:ring-1 focus:ring-primary' : 'bg-surface-variant/50 border border-outline-variant text-on-surface-variant cursor-not-allowed'}`}
590
+ />
591
+ </div>
592
+
593
+ <div className="mt-2 flex gap-sm justify-end">
594
+ <button
595
+ type="button"
596
+ onClick={() => setShowEditModal(false)}
597
+ className="px-xl py-sm bg-transparent border border-outline-variant rounded-lg font-label-md text-on-surface hover:bg-surface-variant transition-colors cursor-pointer"
598
+ >
599
+ Cancel
600
+ </button>
601
+ <button
602
+ type="submit"
603
+ disabled={editStatus.loading}
604
+ className="px-xl py-sm bg-primary text-on-primary rounded-lg font-label-md hover:brightness-110 transition-all border-none cursor-pointer flex items-center justify-center gap-2 disabled:opacity-50"
605
+ >
606
+ {editStatus.loading ? (
607
+ <><span className="material-symbols-outlined animate-spin text-[16px]">sync</span> Saving...</>
608
+ ) : (
609
+ "Save Changes"
610
+ )}
611
+ </button>
612
+ </div>
613
+ </form>
614
+ </div>
615
+ </div>
616
+ )}
617
+ </div>
618
+ );
619
+ };
620
+
621
+ export default Profile;
622
+