Skip to content

Commit 328fd02

Browse files
authored
rework weights for same name signups (#1298)
- **update submodule** - **Enhance sign-up risk assessment by adding sameEmailCount and sameEmailLimit to recent stats request. Update loadRecentSignUpStats function to include email normalization checks. Adjust tests to reflect new return structure.** <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Risk scoring now tracks and reports counts of recent signups that share a normalized email (with configurable limit), exposing this as part of signup-risk statistics. * **Performance** * Added a database index and migration to speed up recent-signup queries, improving risk assessment responsiveness. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent fd158bb commit 328fd02

4 files changed

Lines changed: 25 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- SPLIT_STATEMENT_SENTINEL
2+
-- SINGLE_STATEMENT_SENTINEL
3+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
4+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "ProjectUser_signUpEmailNormalized_recent_idx"
5+
ON "ProjectUser"("tenancyId", "isAnonymous", "signUpEmailNormalized", "signedUpAt");

apps/backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ model ProjectUser {
332332
@@index([tenancyId, createdAt(sort: Desc)], name: "ProjectUser_createdAt_desc")
333333
@@index([tenancyId, isAnonymous, signedUpAt(sort: Asc)], name: "ProjectUser_signedUpAt_asc")
334334
@@index([tenancyId, isAnonymous, signUpIp, signedUpAt], name: "ProjectUser_signUpIp_recent_idx")
335+
@@index([tenancyId, isAnonymous, signUpEmailNormalized, signedUpAt], name: "ProjectUser_signUpEmailNormalized_recent_idx")
335336
@@index([tenancyId, isAnonymous, signUpEmailBase, signedUpAt], name: "ProjectUser_signUpEmailBase_recent_idx")
336337
@@index([tenancyId, sequenceId], name: "ProjectUser_tenancyId_sequenceId_idx")
337338
@@index([shouldUpdateSequenceId, tenancyId], name: "ProjectUser_shouldUpdateSequenceId_idx")

apps/backend/src/lib/risk-scores.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,17 @@ export type SignUpRiskAssessment = {
3030
export type SignUpRiskRecentStatsRequest = {
3131
signedUpAt: Date,
3232
signUpIp: string | null,
33+
signUpEmailNormalized: string | null,
3334
signUpEmailBase: string | null,
3435
recentWindowHours: number,
3536
sameIpLimit: number,
37+
sameEmailLimit: number,
3638
similarEmailLimit: number,
3739
};
3840

3941
export type SignUpRiskRecentStats = {
4042
sameIpCount: number,
43+
sameEmailCount: number,
4144
similarEmailCount: number,
4245
};
4346

@@ -64,7 +67,7 @@ async function loadRecentSignUpStats(
6467
const schema = await getPrismaSchemaForTenancy(tenancy);
6568
const windowStart = new Date(request.signedUpAt.getTime() - request.recentWindowHours * 60 * 60 * 1000);
6669

67-
const [sameIpRows, similarEmailRows] = await Promise.all([
70+
const [sameIpRows, sameEmailRows, similarEmailRows] = await Promise.all([
6871
request.signUpIp == null || request.sameIpLimit === 0
6972
? []
7073
: prisma.$replica().$queryRaw<{ matched: number }[]>`
@@ -77,6 +80,18 @@ async function loadRecentSignUpStats(
7780
LIMIT ${request.sameIpLimit}
7881
`,
7982

83+
request.signUpEmailNormalized == null || request.sameEmailLimit === 0
84+
? []
85+
: prisma.$replica().$queryRaw<{ matched: number }[]>`
86+
SELECT 1 AS "matched"
87+
FROM ${sqlQuoteIdent(schema)}."ProjectUser"
88+
WHERE "tenancyId" = ${tenancy.id}::UUID
89+
AND "isAnonymous" = false
90+
AND "signedUpAt" >= ${windowStart}
91+
AND "signUpEmailNormalized" = ${request.signUpEmailNormalized}
92+
LIMIT ${request.sameEmailLimit}
93+
`,
94+
8095
request.signUpEmailBase == null || request.similarEmailLimit === 0
8196
? []
8297
: prisma.$replica().$queryRaw<{ matched: number }[]>`
@@ -92,6 +107,7 @@ async function loadRecentSignUpStats(
92107

93108
return {
94109
sameIpCount: sameIpRows.length,
110+
sameEmailCount: sameEmailRows.length,
95111
similarEmailCount: similarEmailRows.length,
96112
};
97113
}
@@ -144,7 +160,7 @@ import.meta.vitest?.test("loaded private sign-up risk engine can calculate score
144160
turnstileAssessment: { status: "ok" },
145161
}, {
146162
checkPrimaryEmailRisk: async () => ({ emailableScore: null }),
147-
loadRecentSignUpStats: async () => ({ sameIpCount: 0, similarEmailCount: 0 }),
163+
loadRecentSignUpStats: async () => ({ sameIpCount: 0, sameEmailCount: 0, similarEmailCount: 0 }),
148164
});
149165

150166
expect(assessment).toMatchInlineSnapshot(`

0 commit comments

Comments
 (0)