Make email and phone unique per business, not platform-wide #5

Merged
denis_ireri merged 1 commits from fix/per-business-identity into main 2026-10-12 08:41:08 +00:00
Contributor

Email and phone were unique across the whole platform, so one person could not work for two businesses, and signup and staff creation told anyone whether an email or phone was registered anywhere.

⚠️ Deploy step

Run this straight after deploying:

npm run migrate:identity

It flags the existing platform-operator account(s) and swaps the old platform-wide unique indexes (email_1, phoneNumber_1) for per-business ones. Until it runs, operators cannot open the Platform console and a second business cannot reuse an email. Safe to re-run.

What changes

  • Per-business uniqueness: unique indexes on (businessId, email) and (businessId, phoneNumber). Duplicate checks in signup and branch-manager creation are scoped to the business, so they no longer reveal other businesses' users.
  • Login: the email is looked up in every business and the password checked against each account.
    • One match signs straight in.
    • Several matches return 300 with the businesses to choose from; the login page shows a picker and signs in to the one chosen.
    • A wrong password returns the same 401 as before and reveals nothing. Failures count against each candidate account; a correct password only resets the accounts it opens.
  • Platform-operator access now needs a platformAdmin flag on the account as well as the email in PLATFORM_ADMIN_EMAILS. With per-business emails, an email match alone would have let any owner create a staff account with the operator's email in their own business and open the cross-tenant console. The flag is only set by npm run seed and the migration, never through the app; removing the email from the env list still revokes access.

Testing

  • npm test: 65 tests pass, 9 new in tests/identity.test.ts:
    • same email and phone in two businesses accepted; duplicates within one business refused;
    • login with one matching password signs straight in; the same password in two businesses returns the choice, then signs in to the chosen one;
    • a wrong password reveals nothing; a wrong guess never resets another business's failed-login count;
    • a staff account with the operator's email in another business is refused the Platform console (403); the flagged operator gets in; removing the email revokes access even with the flag.
  • Migration run against a test database in the old shape: first run flagged the operator and dropped email_1 and phoneNumber_1; second run changed nothing; afterwards the same email in a second business is accepted and a duplicate within one business refused.
  • Browser: one email and password in two businesses showed the picker ("Demo Water Co - Cashier", "Carol's Eggs - Owner"); choosing one opened that business's dashboard.
  • Production build passes. Merges cleanly with #4.

Details: docs/PILOT_READINESS.md, "People in more than one business".

🤖 Generated with Claude Code

Email and phone were unique across the whole platform, so one person could not work for two businesses, and signup and staff creation told anyone whether an email or phone was registered anywhere. ## ⚠️ Deploy step Run this straight after deploying: ``` npm run migrate:identity ``` It flags the existing platform-operator account(s) and swaps the old platform-wide unique indexes (`email_1`, `phoneNumber_1`) for per-business ones. Until it runs, operators cannot open the Platform console and a second business cannot reuse an email. Safe to re-run. ## What changes - **Per-business uniqueness**: unique indexes on (businessId, email) and (businessId, phoneNumber). Duplicate checks in signup and branch-manager creation are scoped to the business, so they no longer reveal other businesses' users. - **Login**: the email is looked up in every business and the password checked against each account. - One match signs straight in. - Several matches return 300 with the businesses to choose from; the login page shows a picker and signs in to the one chosen. - A wrong password returns the same 401 as before and reveals nothing. Failures count against each candidate account; a correct password only resets the accounts it opens. - **Platform-operator access now needs a `platformAdmin` flag on the account** as well as the email in `PLATFORM_ADMIN_EMAILS`. With per-business emails, an email match alone would have let any owner create a staff account with the operator's email in their own business and open the cross-tenant console. The flag is only set by `npm run seed` and the migration, never through the app; removing the email from the env list still revokes access. ## Testing - `npm test`: 65 tests pass, 9 new in `tests/identity.test.ts`: - same email and phone in two businesses accepted; duplicates within one business refused; - login with one matching password signs straight in; the same password in two businesses returns the choice, then signs in to the chosen one; - a wrong password reveals nothing; a wrong guess never resets another business's failed-login count; - a staff account with the operator's email in another business is refused the Platform console (403); the flagged operator gets in; removing the email revokes access even with the flag. - Migration run against a test database in the old shape: first run flagged the operator and dropped `email_1` and `phoneNumber_1`; second run changed nothing; afterwards the same email in a second business is accepted and a duplicate within one business refused. - Browser: one email and password in two businesses showed the picker ("Demo Water Co - Cashier", "Carol's Eggs - Owner"); choosing one opened that business's dashboard. - Production build passes. Merges cleanly with #4. Details: `docs/PILOT_READINESS.md`, "People in more than one business". 🤖 Generated with [Claude Code](https://claude.com/claude-code)
denis_ireri added 1 commit 2026-10-12 08:38:21 +00:00
One person could not work for two businesses, and signup and staff
creation told anyone whether an email or phone was registered anywhere
on the platform.

- User email and phone are now unique per business: indexes on
  (businessId, email) and (businessId, phoneNumber). Duplicate checks in
  signup and branch-manager creation are scoped to the business, so they
  no longer reveal other businesses' users.
- Login finds the email in every business and checks the password
  against each account. One match signs straight in; several return 300
  with the businesses to choose from, and the login page shows a picker.
  A wrong password returns the same 401 as before and reveals nothing;
  failures count against each candidate account, and a correct password
  only resets the accounts it opens.
- Platform-operator access now also needs a platformAdmin flag on the
  account. With per-business emails, an email match alone would have let
  any owner create a staff account with the operator's email and open the
  cross-tenant console. The flag is only set by the seed and the
  migration; removing the email from PLATFORM_ADMIN_EMAILS still revokes.
- npm run migrate:identity flags today's operators (while each listed
  email still matches one account) and syncs the User indexes, dropping
  the old unique email_1 and phoneNumber_1. Checked against a database in
  the old shape; safe to re-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
denis_ireri merged commit bae21ee62c into main 2026-10-12 08:41:08 +00:00
denis_ireri deleted branch fix/per-business-identity 2026-10-12 08:59:33 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Ezekiel_Nyangweso/ERP#5