fix: keep one active email OTP per account
Staging Backend Quality Gate / validate (push) Successful in 12m39s
Staging Backend Quality Gate / publish (push) Successful in 12m9s

This commit is contained in:
Jesse_Chen
2026-08-10 10:49:25 +08:00
parent 43d0647de2
commit 8696eca022
3 changed files with 78 additions and 0 deletions
@@ -0,0 +1,18 @@
lock table identity.verifications in share row exclusive mode;
with ranked as (
select
id,
row_number() over (
partition by identifier
order by created_at desc, id desc
) as position
from identity.verifications
)
delete from identity.verifications verification
using ranked
where verification.id = ranked.id
and ranked.position > 1;
create unique index if not exists identity_verifications_identifier_key
on identity.verifications (identifier);
@@ -18,6 +18,12 @@ const identityMigration = fileURLToPath(
import.meta.url,
),
);
const uniqueVerificationMigration = fileURLToPath(
new URL(
"../db/migrations/20260810024500_unique_verification_identifiers.sql",
import.meta.url,
),
);
test("self-hosted identity migration creates Better Auth tables with least privilege", () => {
const migrationSource = readFileSync(identityMigration, "utf8");
@@ -225,6 +231,29 @@ test("self-hosted identity migration creates Better Auth tables with least privi
),
"1",
);
fixture.psql("drop index identity.identity_verifications_identifier_key");
fixture.psql(`
insert into identity.verifications (identifier, value, expires_at, created_at)
values
('sign-in-otp-owner@example.com', 'older', now() + interval '5 minutes', now() - interval '1 second'),
('sign-in-otp-owner@example.com', 'newer', now() + interval '5 minutes', now())
`);
fixture.psql(readFileSync(uniqueVerificationMigration, "utf8"));
assert.equal(
fixture.psql(`
select value
from identity.verifications
where identifier = 'sign-in-otp-owner@example.com'
`),
"newer",
);
assert.throws(() =>
fixture.psql(`
insert into identity.verifications (identifier, value, expires_at)
values ('sign-in-otp-owner@example.com', 'duplicate', now() + interval '5 minutes')
`),
);
} finally {
fixture.stop();
}
@@ -204,6 +204,37 @@ test("Better Auth supports shared user OTP/password sessions for admins", async
);
assert.equal(unauthenticatedSet.status, 401);
const rotatedEmail = "rotated-otp@example.com";
for (let attempt = 0; attempt < 2; attempt += 1) {
const send = await handlers.POST(
request(userHost, "/api/auth/email-otp/send-verification-otp", {
email: rotatedEmail,
type: "sign-in",
}),
);
assert.equal(send.status, 200);
}
const rotatedMessage = sender.messages.at(-1);
assert.equal(
fixture.psql(`
select count(*)
from identity.verifications
where identifier = 'sign-in-otp-${rotatedEmail}'
`),
"1",
);
assert.equal(
(
await handlers.POST(
request(userHost, "/api/auth/sign-in/email-otp", {
email: rotatedEmail,
otp: rotatedMessage?.otp,
}),
)
).status,
200,
);
const newEmail = "new-user@example.com";
const firstPassword = "first-password";
const resetPassword = "reset-password";