Fix merge conflicts and add withTransaction helper

- Add withTransaction helper that gracefully handles nested transactions
  (reuses existing transaction in tests, starts new one otherwise)
- Update auth procedures to use withTransaction instead of direct .transaction()
- Add email config to all e2e test contexts (required by merged code)
- Remove duplicate verification token code from signup procedure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
igm
2026-01-12 17:07:14 +08:00
parent 8e65c2e698
commit e43c006bb1
10 changed files with 82 additions and 20 deletions

View File

@@ -39,6 +39,7 @@ import {
uniqueTestId,
withTestTransaction,
} from "@reviq/test-helpers";
import { createLoggingEmailClient } from "@reviq/emails";
import { router } from "../../router.js";
import { COOKIE_NAMES } from "../../utils/cookies.js";
import { hashToken } from "../../utils/crypto.js";
@@ -75,6 +76,11 @@ function createAPIContext(
rpName: TEST_RP.rpName,
reqHeaders,
resHeaders: new Headers(),
email: {
client: createLoggingEmailClient(),
fromAddress: "test@example.com",
baseUrl: TEST_RP.origin,
},
};
}

View File

@@ -50,6 +50,7 @@ import {
uniqueTestId,
withTestTransaction,
} from "@reviq/test-helpers";
import { createLoggingEmailClient } from "@reviq/emails";
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
import { router } from "../../router.js";
import { COOKIE_NAMES } from "../../utils/cookies.js";
@@ -100,6 +101,11 @@ function createAPIContext(
rpName: TEST_RP.rpName,
reqHeaders,
resHeaders: new Headers(),
email: {
client: createLoggingEmailClient(),
fromAddress: "test@example.com",
baseUrl: TEST_RP.origin,
},
};
}

View File

@@ -32,6 +32,7 @@ import {
uniqueTestId,
withTestTransaction,
} from "@reviq/test-helpers";
import { createLoggingEmailClient } from "@reviq/emails";
import { router } from "../../router.js";
import { COOKIE_NAMES } from "../../utils/cookies.js";
import { hashToken } from "../../utils/crypto.js";
@@ -82,6 +83,11 @@ function createAPIContext(
rpName: TEST_RP.rpName,
reqHeaders,
resHeaders: new Headers(),
email: {
client: createLoggingEmailClient(),
fromAddress: "test@example.com",
baseUrl: TEST_RP.origin,
},
};
}

View File

@@ -23,6 +23,7 @@ import {
uniqueTestId,
withTestTransaction,
} from "@reviq/test-helpers";
import { createLoggingEmailClient } from "@reviq/emails";
import { router } from "../../router.js";
import { COOKIE_NAMES } from "../../utils/cookies.js";
import { hashToken } from "../../utils/crypto.js";
@@ -58,6 +59,11 @@ function createAPIContext(
rpName: TEST_RP.rpName,
reqHeaders,
resHeaders: new Headers(),
email: {
client: createLoggingEmailClient(),
fromAddress: "test@example.com",
baseUrl: TEST_RP.origin,
},
};
}

View File

@@ -23,6 +23,7 @@ import {
uniqueTestId,
withTestTransaction,
} from "@reviq/test-helpers";
import { createLoggingEmailClient } from "@reviq/emails";
import { VirtualAuthenticator } from "@reviq/virtual-authenticator";
import { router } from "../../router.js";
import { COOKIE_NAMES } from "../../utils/cookies.js";
@@ -51,6 +52,11 @@ function createAPIContext(
rpName: TEST_RP.rpName,
reqHeaders,
resHeaders: new Headers(),
email: {
client: createLoggingEmailClient(),
fromAddress: "test@example.com",
baseUrl: TEST_RP.origin,
},
};
}
@@ -133,6 +139,11 @@ function createLoginRequestContext(
rpName: TEST_RP.rpName,
reqHeaders,
resHeaders: new Headers(),
email: {
client: createLoggingEmailClient(),
fromAddress: "test@example.com",
baseUrl: TEST_RP.origin,
},
};
}

View File

@@ -6,6 +6,7 @@
* This prevents attackers from determining which emails are registered
*/
import { withTransaction } from "@reviq/db";
import { sendPasswordResetEmail } from "@reviq/emails";
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import {
@@ -37,7 +38,7 @@ export const forgotPassword = os.auth.forgotPassword.handler(
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);
// Delete old tokens and insert new one in transaction
await context.db.transaction().execute(async (trx) => {
await withTransaction(context.db, async (trx) => {
// Delete any existing password reset tokens for this user (security measure)
await trx
.deleteFrom("password_resets")

View File

@@ -16,6 +16,7 @@
* e. Return { status: 'completed', redirectTo: '/dashboard' or '/auth/trust-device' }
*/
import { withTransaction } from "@reviq/db";
import {
COOKIE_NAMES,
COOKIE_OPTIONS,
@@ -90,9 +91,9 @@ export const loginIfRequestIsCompleted =
const userAgent = getUserAgent(context.reqHeaders);
// Create session in transaction (atomic: device upsert + session + login_request delete)
const { session, deviceTrusted } = await context.db
.transaction()
.execute(async (trx) => {
const { session, deviceTrusted } = await withTransaction(
context.db,
async (trx) => {
// Upsert user device
const deviceId = await upsertUserDevice(
trx,

View File

@@ -10,6 +10,7 @@ import type {
import type { Kysely } from "kysely";
import type { RPInfo } from "../../utils/webauthn.js";
import { ORPCError } from "@orpc/server";
import { withTransaction } from "@reviq/db";
import { sendVerificationEmail } from "@reviq/emails";
import { verifyRegistrationResponse } from "@simplewebauthn/server";
import {
@@ -159,7 +160,7 @@ export async function signupWithPasskey(
// Create user and passkey in a transaction (handle race condition if concurrent signup)
try {
const result = await db.transaction().execute(async (trx) => {
const result = await withTransaction(db, async (trx) => {
// Create user
const user = await trx
.insertInto("users")
@@ -276,7 +277,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
);
// Create session and email verification in transaction
const session = await context.db.transaction().execute(async (trx) => {
const session = await withTransaction(context.db, async (trx) => {
// Create session (7 days, trusted mode false initially, no device)
const newSession = await createSession(trx, {
userId,
@@ -307,20 +308,6 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
COOKIE_OPTIONS.session,
);
// Generate verification token
const verificationToken = generateSecureBase58Token();
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Store verification token (store raw token, not hash - it's already high-entropy)
await context.db
.insertInto("email_verifications")
.values({
user_id: userId,
token: verificationToken,
expires_at: expiresAt,
})
.execute();
// Send verification email
await sendVerificationEmail({
client: context.email.client,