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

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>
This commit is contained in:
2026-10-12 11:31:45 +03:00
parent 1fe2421cc4
commit 9e3e59a804
14 changed files with 407 additions and 52 deletions
+6 -5
View File
@@ -71,14 +71,15 @@ export async function POST(req: NextRequest) {
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 400 });
}
// Unique within this business only (see the User model).
const lowerEmail = manager.email.toLowerCase();
const emailTaken = await User.findOne({ email: lowerEmail }).lean();
const emailTaken = await User.exists({ businessId, email: lowerEmail });
if (emailTaken) {
return NextResponse.json({ error: "An account with this email already exists." }, { status: 409 });
return NextResponse.json({ error: "Someone in your business already uses this email." }, { status: 409 });
}
const phoneTaken = await User.findOne({ phoneNumber: managerPhone }).lean();
const phoneTaken = await User.exists({ businessId, phoneNumber: managerPhone });
if (phoneTaken) {
return NextResponse.json({ error: "An account with this phone number already exists." }, { status: 409 });
return NextResponse.json({ error: "Someone in your business already uses this phone number." }, { status: 409 });
}
}
@@ -107,7 +108,7 @@ export async function POST(req: NextRequest) {
} catch (err: any) {
await Branch.deleteOne({ _id: branch._id }).catch(() => {});
if (err?.code === 11000) {
return NextResponse.json({ error: "A user with this phone number or email already exists." }, { status: 409 });
return NextResponse.json({ error: "Someone in your business already uses this phone number or email." }, { status: 409 });
}
return NextResponse.json({ error: "Could not create the branch manager. Please try again." }, { status: 500 });
}