feat: add multi-BU tenancy with per-user team scoping (Option B)
- Add bu_teams column to users table (migration + fresh schema) - Create shared KNOWN_TEAMS constant and validateTeams helper - Expose user teams in auth middleware, login, and /me responses - Add bu_teams CRUD to user management routes with audit logging - Make Ivanti FINDINGS_FILTERS configurable via IVANTI_BU_FILTER env var - Add query-time team filtering to GET /findings and /findings/counts - Update AuthContext with teams helpers and admin scope toggle - Create AdminScopeToggle component (My Teams / All BUs) - Scope ReportingPage findings fetch by user teams - Scope CompliancePage team selector by user teams - Scope ExportsPage findings exports by user teams - Add BU teams multi-select to UserManagement create/edit forms - Display team badges in user list table
This commit is contained in:
@@ -180,7 +180,8 @@ export default function UserManagement({ onClose }) {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
group: 'Read_Only'
|
||||
group: 'Read_Only',
|
||||
bu_teams: ''
|
||||
});
|
||||
const [formError, setFormError] = useState('');
|
||||
const [formSuccess, setFormSuccess] = useState('');
|
||||
@@ -240,7 +241,7 @@ export default function UserManagement({ onClose }) {
|
||||
setTimeout(() => {
|
||||
setShowAddUser(false);
|
||||
setEditingUser(null);
|
||||
setFormData({ username: '', email: '', password: '', group: 'Read_Only' });
|
||||
setFormData({ username: '', email: '', password: '', group: 'Read_Only', bu_teams: '' });
|
||||
setFormSuccess('');
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
@@ -278,7 +279,8 @@ export default function UserManagement({ onClose }) {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: '',
|
||||
group: user.group
|
||||
group: user.group,
|
||||
bu_teams: user.bu_teams || ''
|
||||
});
|
||||
setShowAddUser(true);
|
||||
setFormError('');
|
||||
@@ -361,7 +363,7 @@ export default function UserManagement({ onClose }) {
|
||||
onClick={() => {
|
||||
setShowAddUser(true);
|
||||
setEditingUser(null);
|
||||
setFormData({ username: '', email: '', password: '', group: 'Read_Only' });
|
||||
setFormData({ username: '', email: '', password: '', group: 'Read_Only', bu_teams: '' });
|
||||
setFormError('');
|
||||
setFormSuccess('');
|
||||
}}
|
||||
@@ -482,6 +484,50 @@ export default function UserManagement({ onClose }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BU Teams assignment */}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<label style={styles.label}>BU Teams</label>
|
||||
<div style={{
|
||||
display: 'flex', flexWrap: 'wrap', gap: '0.5rem',
|
||||
padding: '0.75rem',
|
||||
background: 'rgba(30,41,59,0.6)',
|
||||
border: '1px solid rgba(14,165,233,0.25)',
|
||||
borderRadius: '0.5rem',
|
||||
}}>
|
||||
{['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'].map(team => {
|
||||
const currentTeams = formData.bu_teams ? formData.bu_teams.split(',').filter(Boolean) : [];
|
||||
const isChecked = currentTeams.includes(team);
|
||||
return (
|
||||
<label key={team} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: '0.375rem',
|
||||
cursor: 'pointer', fontSize: '0.8rem', fontFamily: 'monospace',
|
||||
color: isChecked ? '#38BDF8' : '#94A3B8',
|
||||
padding: '0.25rem 0.5rem', borderRadius: '0.25rem',
|
||||
background: isChecked ? 'rgba(14,165,233,0.1)' : 'transparent',
|
||||
border: isChecked ? '1px solid rgba(14,165,233,0.3)' : '1px solid transparent',
|
||||
transition: 'all 0.15s',
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => {
|
||||
const updated = isChecked
|
||||
? currentTeams.filter(t => t !== team)
|
||||
: [...currentTeams, team];
|
||||
setFormData({ ...formData, bu_teams: updated.join(',') });
|
||||
}}
|
||||
style={{ accentColor: '#0EA5E9' }}
|
||||
/>
|
||||
{team}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p style={{ fontSize: '0.65rem', color: '#64748B', marginTop: '0.375rem' }}>
|
||||
Determines which BU data the user sees on Reporting and Compliance pages.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.75rem', paddingTop: '1rem' }}>
|
||||
<button type="submit" style={styles.primaryBtn}
|
||||
onMouseEnter={e => {
|
||||
@@ -523,6 +569,7 @@ export default function UserManagement({ onClose }) {
|
||||
<tr>
|
||||
<th style={styles.th}>User</th>
|
||||
<th style={styles.th}>Group</th>
|
||||
<th style={styles.th}>Teams</th>
|
||||
<th style={styles.th}>Status</th>
|
||||
<th style={styles.th}>Last Login</th>
|
||||
<th style={styles.thRight}>Actions</th>
|
||||
@@ -547,6 +594,25 @@ export default function UserManagement({ onClose }) {
|
||||
{user.group ? user.group.replace('_', ' ') : 'Read Only'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={styles.td}>
|
||||
{(user.teams && user.teams.length > 0) ? (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||
{user.teams.map(t => (
|
||||
<span key={t} style={{
|
||||
fontSize: '0.65rem', fontFamily: 'monospace',
|
||||
padding: '0.1rem 0.35rem', borderRadius: '0.2rem',
|
||||
background: 'rgba(14,165,233,0.1)',
|
||||
border: '1px solid rgba(14,165,233,0.25)',
|
||||
color: '#7DD3FC',
|
||||
}}>{t}</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.7rem', color: '#F59E0B', fontStyle: 'italic' }}>
|
||||
⚠ No teams
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={styles.td}>
|
||||
<button
|
||||
onClick={() => handleToggleActive(user)}
|
||||
|
||||
Reference in New Issue
Block a user