9e3e59a804
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>
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
import dotenv from "dotenv";
|
|
import path from "path";
|
|
dotenv.config({ path: path.resolve(__dirname, "../.env.local") });
|
|
|
|
import mongoose from "mongoose";
|
|
import { connectDb } from "../src/lib/server/db";
|
|
import { User } from "../src/lib/server/models/User";
|
|
import { platformAdminEmails } from "../src/lib/server/platform";
|
|
|
|
/**
|
|
* One-off, safe to re-run. Moves user identity from platform-wide to
|
|
* per-business uniqueness:
|
|
*
|
|
* 1. Marks today's platform operators. Until now an operator was anyone
|
|
* whose email was in PLATFORM_ADMIN_EMAILS; emails were unique, so
|
|
* that was one account each. From now on emails are only unique per
|
|
* business, so access also needs the platformAdmin flag on the account.
|
|
* Run this BEFORE dropping the old index, while each listed email
|
|
* still matches exactly one account.
|
|
* 2. Syncs the User indexes: drops the old unique `email_1` and
|
|
* `phoneNumber_1`, creates unique (businessId, email) and
|
|
* (businessId, phoneNumber), and a plain email lookup index.
|
|
*
|
|
* npm run migrate:identity
|
|
*/
|
|
async function main() {
|
|
await connectDb();
|
|
|
|
for (const email of platformAdminEmails()) {
|
|
const accounts = await User.find({ email }).select("_id").lean();
|
|
if (accounts.length === 1) {
|
|
await User.updateOne({ _id: accounts[0]._id }, { $set: { platformAdmin: true } });
|
|
console.log(`Platform operator: ${email}`);
|
|
} else if (accounts.length === 0) {
|
|
console.warn(`No account for ${email} - create it with npm run seed.`);
|
|
} else {
|
|
console.warn(`${email} has ${accounts.length} accounts - set platformAdmin on the right one by hand.`);
|
|
}
|
|
}
|
|
|
|
const dropped = await User.syncIndexes();
|
|
console.log(`User indexes synced. Dropped: ${dropped.length ? dropped.join(", ") : "none"}.`);
|
|
await mongoose.disconnect();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("migration failed:", err.message);
|
|
process.exit(1);
|
|
});
|