diff --git a/dashcaddy-api/__tests__/network-people-store.test.js b/dashcaddy-api/__tests__/network-people-store.test.js index 8d1c715..56079b9 100644 --- a/dashcaddy-api/__tests__/network-people-store.test.js +++ b/dashcaddy-api/__tests__/network-people-store.test.js @@ -82,6 +82,26 @@ describe('network-people-store: CRUD', () => { expect(list).toHaveLength(1); // merged, not duplicated }); + test('upsertPerson merges by providerUserId when id is omitted (wizard after provider sync)', async () => { + const synced = await store.upsertPerson({ + displayName: 'SamiAhmed7777', + providerUserId: 'ughVnuLDu621CNTRL', + }); + expect(synced.groups).toEqual(['Admin']); // bootstrap identity + + const fromWizard = await store.upsertPerson({ + displayName: 'Sami Ahmed', + providerUserId: 'ughVnuLDu621CNTRL', + }); + + expect(fromWizard.id).toBe(synced.id); + expect(fromWizard.displayName).toBe('Sami Ahmed'); + expect(fromWizard.groups).toEqual(['Admin']); + expect(fromWizard.protectedAt).toBe(synced.protectedAt); + const list = await store.listPeople(); + expect(list).toHaveLength(1); + }); + test('updatePerson patches only provided fields', async () => { const p = await store.upsertPerson({ displayName: 'Bob', providerUserId: 'ts-2', groups: ['G'] }); const u = await store.updatePerson(p.id, { displayName: 'Bobby' }); diff --git a/dashcaddy-api/src/security/network-people-store.js b/dashcaddy-api/src/security/network-people-store.js index 71b9e6a..b9a028d 100644 --- a/dashcaddy-api/src/security/network-people-store.js +++ b/dashcaddy-api/src/security/network-people-store.js @@ -180,7 +180,17 @@ function createPeopleStore(opts = {}) { function upsertPerson(input = {}) { return _enqueue(() => { const data = _load(); - const existing = input.id ? data.people[input.id] : null; + // Resolve the existing record by id when given, else by providerUserId + // (dogfood finding 2026-10-04: the wizard upserts a person AFTER a + // provider sync already created one for the same Tailscale user with no + // id — without this lookup the same providerUserId was duplicated). + const existing = input.id + ? data.people[input.id] + : (input.providerUserId + ? Object.values(data.people).find( + (p) => p.providerUserId && p.providerUserId === input.providerUserId + ) || null + : null); const isFirst = Object.keys(data.people).length === 0; const id = existing ? existing.id : (input.id || crypto.randomUUID());