Scan manually added accounts
Build — Windows, Linux & macOS / GDPRScanner / linux (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / macos (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / windows (push) Has been cancelled
Build — Windows, Linux & macOS / Create GitHub Release (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / linux (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / macos (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / windows (push) Has been cancelled
Build — Windows, Linux & macOS / Create GitHub Release (push) Has been cancelled
Manual accounts were never scanned: - Sidebar "add user" entries had no platform, and getSelectedUsers() required platform === 'm365', so they were dropped from every scan. - Profile-editor manual accounts were stored as "manual:<email>", which is not a Graph user id, and applying the profile never put them in the sidebar list, so they could not be selected. Now the editor looks the email up via /api/users/lookup and stores the directory id plus name/email (falling back to manual:<email> only when the lookup fails). Applying a profile adds its manual accounts to the sidebar and resolves legacy manual:<email> entries, logging any that can't be found. The scan engine resolves remaining manual:<email> ids (e.g. from scheduled scans) via the new M365Connector.lookup_user(), skipping and reporting failures without widening to all users. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
1 parent
bbe9c62ff9
commit
1c79ac15b7
11 files changed
+234
-18
No files matched your search
+75
-12
@@ -215,6 +215,7 @@ function _applyProfile(profile) {
|
||||
}
|
||||
} else if (Array.isArray(profile.user_ids) && profile.user_ids.length) {
|
||||
window._pendingProfileUserIds = profile.user_ids.map(u => u.id || u);
|
||||
_addProfileManualUsers(profile.user_ids);
|
||||
_applyPendingProfileUsers();
|
||||
} else if (Array.isArray(profile.user_ids) && profile.user_ids.length === 0) {
|
||||
// Explicitly empty list — deselect everyone so previous sidebar state doesn't persist
|
||||
@@ -229,7 +230,7 @@ function _applyProfile(profile) {
|
||||
// un-awaited _mergeGoogleUsers(). Keep the pending IDs until every one is
|
||||
// matched or the user list is final, else those users stay unticked.
|
||||
function _userListFinal() {
|
||||
return S._allUsers.length > 0 && (!window._googleConnected || S._googleUsersMerged);
|
||||
return S._m365UsersLoaded && (!window._googleConnected || S._googleUsersMerged);
|
||||
}
|
||||
|
||||
function _applyPendingProfileUsers() {
|
||||
@@ -244,6 +245,54 @@ function _applyPendingProfileUsers() {
|
||||
if (found >= wanted.size || _userListFinal()) window._pendingProfileUserIds = null;
|
||||
}
|
||||
|
||||
function _isManualEntry(u) {
|
||||
return typeof u === 'object' ? !!u.manual : String(u).startsWith('manual:');
|
||||
}
|
||||
|
||||
// Manual accounts aren't in the /api/users directory list, so they must be added
|
||||
// to S._allUsers (as sidebar manual users) or they can never be selected/scanned.
|
||||
// Legacy "manual:<email>" entries are looked up to get their directory id.
|
||||
function _addProfileManualUsers(entries) {
|
||||
entries.filter(_isManualEntry).forEach(async function(u) {
|
||||
if (typeof u === 'object' && !String(u.id).startsWith('manual:')) {
|
||||
if (!S._allUsers.some(x => x.id === u.id)) {
|
||||
S._allUsers.push({id: u.id, displayName: u.displayName || u.email || u.id,
|
||||
email: u.email || '', userRole: u.userRole || 'other',
|
||||
platform: 'm365', manual: true, selected: false});
|
||||
}
|
||||
return; // selection is handled by _applyPendingProfileUsers()
|
||||
}
|
||||
const legacyId = u.id || u;
|
||||
try {
|
||||
const email = legacyId.slice('manual:'.length);
|
||||
const d = await _lookupM365User(email);
|
||||
if (!d) {
|
||||
// Not connected / not found — scheduled scans retry the lookup at scan time
|
||||
log(t('m365_profile_manual_not_found', 'Manual account not found in M365') + ': ' + email, 'err');
|
||||
return;
|
||||
}
|
||||
let user = S._allUsers.find(x => x.id === d.id);
|
||||
if (!user) {
|
||||
user = {...d, platform: 'm365', manual: true};
|
||||
S._allUsers.push(user);
|
||||
}
|
||||
user.selected = true;
|
||||
// Pending apply may re-run later — make it select the resolved id, not the legacy one
|
||||
const pend = window._pendingProfileUserIds;
|
||||
if (pend) window._pendingProfileUserIds = pend.map(x => x === legacyId ? d.id : x);
|
||||
renderAccountList();
|
||||
} catch(e) { /* leave for the scan-time fallback */ }
|
||||
});
|
||||
}
|
||||
|
||||
// Returns {id, displayName, email} or null when M365 isn't connected / the user doesn't exist.
|
||||
async function _lookupM365User(upn) {
|
||||
const r = await fetch('/api/users/lookup?upn=' + encodeURIComponent(upn));
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
return d && d.id ? d : null;
|
||||
}
|
||||
|
||||
async function saveCurrentAsProfile() {
|
||||
const name = prompt(t('m365_profile_save_prompt', 'Profile name:'),
|
||||
S._activeProfileId
|
||||
@@ -385,12 +434,14 @@ function _openEditorForProfile(profile) {
|
||||
const accountRows = S._allUsers.map(u => {
|
||||
// Only check if the user was explicitly saved — default to unchecked like the main window
|
||||
const checked = isSaved(u) ? 'checked' : '';
|
||||
const manualAttrs = u.manual
|
||||
? ` data-manual="1" data-name="${_esc(u.displayName || '')}" data-email="${_esc(u.email || '')}"` : '';
|
||||
const platBadge = u.platform === 'both' ? '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:linear-gradient(90deg,#E6F1FB 50%,#EAF3DE 50%);color:#1a4a1a;font-weight:500;border:0.5px solid #b5d4b5">M365+GWS</span>'
|
||||
: (u.platform || 'm365') === 'google' ? '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#EAF3DE;color:#3B6D11;font-weight:500">GWS</span>'
|
||||
: '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#E6F1FB;color:#185FA5;font-weight:500">M365</span>';
|
||||
const roleBadge = u.userRole === 'student' ? t('role_student','Elev') : u.userRole === 'staff' ? t('role_staff','Ansat') : t('role_other','Anden');
|
||||
const roleOverrideStyle = u.roleOverride ? 'color:var(--color-text-info);outline:1px solid var(--color-border-info);' : '';
|
||||
return `<label class="pmgmt-acct-row" data-uid="${_esc(u.id)}" data-role="${_esc(u.userRole || 'other')}"><input type="checkbox" ${checked} data-uid="${_esc(u.id)}"><span style="flex:1;color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(u.displayName)}</span>${platBadge}<button type="button" class="pmgmt-role-badge" data-uid="${_esc(u.id)}" onclick="_pmgmtCycleRole(this.getAttribute('data-uid'),event)" style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;border:none;cursor:pointer;${roleOverrideStyle}">${roleBadge}</button></label>`;
|
||||
return `<label class="pmgmt-acct-row" data-uid="${_esc(u.id)}" data-role="${_esc(u.userRole || 'other')}"><input type="checkbox" ${checked} data-uid="${_esc(u.id)}"${manualAttrs}><span style="flex:1;color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(u.displayName)}</span>${platBadge}<button type="button" class="pmgmt-role-badge" data-uid="${_esc(u.id)}" onclick="_pmgmtCycleRole(this.getAttribute('data-uid'),event)" style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;border:none;cursor:pointer;${roleOverrideStyle}">${roleBadge}</button></label>`;
|
||||
}).join('') + _unresolvedAccountRows(savedList);
|
||||
|
||||
body.innerHTML = `
|
||||
@@ -492,11 +543,13 @@ function _unresolvedAccountRows(savedList) {
|
||||
const id = u.id || u;
|
||||
if (!id || known.has(id) || seen.has(id)) return '';
|
||||
seen.add(id);
|
||||
const isManual = id.startsWith('manual:');
|
||||
const isManual = _isManualEntry(u);
|
||||
const label = (typeof u === 'object' && u.displayName) || id.replace(/^(manual|google):/, '');
|
||||
const badge = isManual ? t('m365_profile_acct_manual', 'Manual')
|
||||
: t('m365_profile_acct_unresolved', 'Not loaded');
|
||||
return `<label class="pmgmt-acct-row" data-uid="${_esc(id)}" data-role="${_esc((typeof u === 'object' && u.userRole) || 'other')}"><input type="checkbox" checked data-uid="${_esc(id)}"><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${_esc(id)}">${_esc(label)}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">${_esc(badge)}</span></label>`;
|
||||
const manualAttrs = _isManualEntry(u) && typeof u === 'object'
|
||||
? ` data-manual="1" data-name="${_esc(u.displayName || '')}" data-email="${_esc(u.email || '')}"` : '';
|
||||
return `<label class="pmgmt-acct-row" data-uid="${_esc(id)}" data-role="${_esc((typeof u === 'object' && u.userRole) || 'other')}"><input type="checkbox" checked data-uid="${_esc(id)}"${manualAttrs}><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${_esc(id)}">${_esc(label)}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">${_esc(badge)}</span></label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -628,18 +681,24 @@ function _pmgmtRoleFilter(role) {
|
||||
_pmgmtFilterAccounts(document.getElementById('pmgmtAcctSearch')?.value || '');
|
||||
}
|
||||
|
||||
function _pmgmtAddManual() {
|
||||
const email = prompt('E-mail adresse:');
|
||||
if (!email || !email.trim()) return;
|
||||
async function _pmgmtAddManual() {
|
||||
const email = (prompt('E-mail adresse:') || '').trim();
|
||||
if (!email) return;
|
||||
const list = document.getElementById('pmgmtAcctList');
|
||||
if (!list) return;
|
||||
const id = 'manual:' + email.trim().toLowerCase();
|
||||
if (list.querySelector(`input[data-uid="${id}"]`)) return; // already exists
|
||||
// Resolve to the directory id so the scan can use it. If M365 isn't reachable
|
||||
// right now, fall back to "manual:<email>" — the scan engine resolves that later.
|
||||
let d = null;
|
||||
try { d = await _lookupM365User(email); } catch(e) { /* offline — use fallback */ }
|
||||
const id = d ? d.id : 'manual:' + email.toLowerCase();
|
||||
const label = d ? (d.displayName || email) : email;
|
||||
const existing = [...list.querySelectorAll('input[data-uid]')].find(cb => cb.dataset.uid === id);
|
||||
if (existing) { existing.checked = true; return; } // already listed — just tick it
|
||||
const lbl = document.createElement('label');
|
||||
lbl.className = 'pmgmt-acct-row';
|
||||
lbl.dataset.uid = id;
|
||||
lbl.dataset.role = 'other';
|
||||
lbl.innerHTML = `<input type="checkbox" checked data-uid="${_esc(id)}"><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(email.trim())}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">${_esc(t('m365_profile_acct_manual', 'Manual'))}</span>`;
|
||||
lbl.innerHTML = `<input type="checkbox" checked data-uid="${_esc(id)}" data-manual="1" data-name="${_esc(label)}" data-email="${_esc(d ? (d.email || email) : email)}"><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${_esc(email)}">${_esc(label)}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">${_esc(t('m365_profile_acct_manual', 'Manual'))}</span>`;
|
||||
list.appendChild(lbl);
|
||||
}
|
||||
|
||||
@@ -674,8 +733,10 @@ async function _pmgmtSaveFullEdit() {
|
||||
const effectiveFileSources = fileRendered ? fileSources : (profile.file_sources || []);
|
||||
const allSources = m365Sources.concat(effectiveGoogleSources).concat(effectiveFileSources);
|
||||
let user_ids = Array.from(document.querySelectorAll('#pmgmtAcctList input[type=checkbox]:checked'))
|
||||
.map(cb => cb.dataset.uid)
|
||||
.filter(Boolean);
|
||||
.filter(cb => cb.dataset.uid)
|
||||
.map(cb => cb.dataset.manual
|
||||
? {id: cb.dataset.uid, displayName: cb.dataset.name || '', email: cb.dataset.email || '', manual: true}
|
||||
: cb.dataset.uid);
|
||||
// "all" profile opened before users loaded: there were no rows to tick, so keep "all"
|
||||
if (window._pmgmtSavedAll && !S._allUsers.length && !user_ids.length) user_ids = 'all';
|
||||
const updated = {
|
||||
@@ -810,6 +871,8 @@ window._pmgmtUse = _pmgmtUse;
|
||||
window._pmgmtOpenEditor = _pmgmtOpenEditor;
|
||||
window._openEditorForProfile = _openEditorForProfile;
|
||||
window._unresolvedAccountRows = _unresolvedAccountRows;
|
||||
window._addProfileManualUsers = _addProfileManualUsers;
|
||||
window._lookupM365User = _lookupM365User;
|
||||
window._userListFinal = _userListFinal;
|
||||
window._peSetDate = _peSetDate;
|
||||
window._peSetYear = _peSetYear;
|
||||
|
||||
Reference in new issue
Block a user