Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- SPLIT_STATEMENT_SENTINEL
-- SINGLE_STATEMENT_SENTINEL
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
CREATE INDEX CONCURRENTLY IF NOT EXISTS "ProjectUser_signUpEmailNormalized_recent_idx"
ON "ProjectUser"("tenancyId", "isAnonymous", "signUpEmailNormalized", "signedUpAt");
1 change: 1 addition & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ model ProjectUser {
@@index([tenancyId, createdAt(sort: Desc)], name: "ProjectUser_createdAt_desc")
@@index([tenancyId, isAnonymous, signedUpAt(sort: Asc)], name: "ProjectUser_signedUpAt_asc")
@@index([tenancyId, isAnonymous, signUpIp, signedUpAt], name: "ProjectUser_signUpIp_recent_idx")
@@index([tenancyId, isAnonymous, signUpEmailNormalized, signedUpAt], name: "ProjectUser_signUpEmailNormalized_recent_idx")
@@index([tenancyId, isAnonymous, signUpEmailBase, signedUpAt], name: "ProjectUser_signUpEmailBase_recent_idx")
@@index([tenancyId, sequenceId], name: "ProjectUser_tenancyId_sequenceId_idx")
@@index([shouldUpdateSequenceId, tenancyId], name: "ProjectUser_shouldUpdateSequenceId_idx")
Expand Down
20 changes: 18 additions & 2 deletions apps/backend/src/lib/risk-scores.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ export type SignUpRiskAssessment = {
export type SignUpRiskRecentStatsRequest = {
signedUpAt: Date,
signUpIp: string | null,
signUpEmailNormalized: string | null,
signUpEmailBase: string | null,
recentWindowHours: number,
sameIpLimit: number,
sameEmailLimit: number,
similarEmailLimit: number,
};

export type SignUpRiskRecentStats = {
sameIpCount: number,
sameEmailCount: number,
similarEmailCount: number,
};

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

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

request.signUpEmailNormalized == null || request.sameEmailLimit === 0
? []
: prisma.$replica().$queryRaw<{ matched: number }[]>`
SELECT 1 AS "matched"
FROM ${sqlQuoteIdent(schema)}."ProjectUser"
WHERE "tenancyId" = ${tenancy.id}::UUID
AND "isAnonymous" = false
AND "signedUpAt" >= ${windowStart}
AND "signUpEmailNormalized" = ${request.signUpEmailNormalized}
LIMIT ${request.sameEmailLimit}
Comment thread
mantrakp04 marked this conversation as resolved.
`,
Comment thread
mantrakp04 marked this conversation as resolved.

request.signUpEmailBase == null || request.similarEmailLimit === 0
? []
: prisma.$replica().$queryRaw<{ matched: number }[]>`
Expand All @@ -92,6 +107,7 @@ async function loadRecentSignUpStats(

return {
sameIpCount: sameIpRows.length,
sameEmailCount: sameEmailRows.length,
similarEmailCount: similarEmailRows.length,
};
}
Expand Down Expand Up @@ -144,7 +160,7 @@ import.meta.vitest?.test("loaded private sign-up risk engine can calculate score
turnstileAssessment: { status: "ok" },
}, {
checkPrimaryEmailRisk: async () => ({ emailableScore: null }),
loadRecentSignUpStats: async () => ({ sameIpCount: 0, similarEmailCount: 0 }),
loadRecentSignUpStats: async () => ({ sameIpCount: 0, sameEmailCount: 0, similarEmailCount: 0 }),
});

expect(assessment).toMatchInlineSnapshot(`
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/private/implementation
Loading