fix: phone retry limit messagin (#3358)

This commit is contained in:
Daniel Salazar
2026-07-08 00:05:41 -04:00
committed by GitHub
parent d039dcfee5
commit 722a6cf50f
3 changed files with 59 additions and 0 deletions
+34
View File
@@ -54,4 +54,38 @@ describe('UserStore', () => {
expect(typeof cachedUser?.email_confirmed).toBe('boolean');
expect(typeof cachedUser?.requires_email_confirmation).toBe('boolean');
});
it('counts other accounts holding the same phone number', async () => {
const phone = `+1415555${Math.floor(1000 + Math.random() * 9000)}`;
const makeUser = async () => {
const username = `up-${Math.random().toString(36).slice(2, 10)}`;
const user = await server.stores.user.create({
username,
uuid: uuidv4(),
password: null,
email: `${username}@test.local`,
});
await server.stores.user.update(user.id, { phone });
return user;
};
const first = await makeUser();
expect(
await server.stores.user.countOthersByPhone(phone, first.id),
).toBe(0);
const second = await makeUser();
expect(
await server.stores.user.countOthersByPhone(phone, first.id),
).toBe(1);
expect(
await server.stores.user.countOthersByPhone(phone, second.id),
).toBe(1);
// A caller that isn't among the holders sees both.
expect(await server.stores.user.countOthersByPhone(phone, -1)).toBe(2);
// Unknown number counts zero.
expect(
await server.stores.user.countOthersByPhone('+19995550000', -1),
).toBe(0);
});
});
+23
View File
@@ -257,6 +257,29 @@ export class UserStore extends PuterStore {
return this.getById(row.id as number);
}
/**
* Count accounts other than `excludeUserId` whose confirmed `phone`
* equals this number. Backs the cap on live accounts per phone number
* (the row is deleted with the account, so this only counts accounts
* that still exist — recycling velocity is rate-limited separately by
* the abuse policy's KV log).
*
* Not cached — like `getByCleanEmail`, callers use this for duplicate
* detection at write time, which needs fresh reads.
*/
async countOthersByPhone(
phone: string,
excludeUserId: number,
): Promise<number> {
if (!phone) return 0;
if (!isStorableAsLatin1(phone)) return 0;
const rows = (await this.clients.db.tryHardRead(
'SELECT COUNT(*) AS n FROM `user` WHERE `phone` = ? AND `id` != ?',
[phone, excludeUserId],
)) as Array<{ n: number | string }>;
return Number(rows[0]?.n) || 0;
}
/**
* Generic property lookup. Fast-path reads redis first (cache is
* multi-key — every identifying property points at the same serialized
@@ -44,6 +44,8 @@ import {
const SEND_REASON_MESSAGES = {
phone_already_used:
'This phone number has already been used to verify the maximum number of accounts. Please use a different number, or email support@puter.com for help.',
phone_verify_limit:
'This phone number cannot be used to verify this account. Please use a different number, or email support@puter.com for help.',
phone_send_limit:
'This phone number has received too many verification codes recently. Please try again later, or use a different number.',
phone_verify_attempts_exhausted: