- Remove unused biome suppression comment in completions.ts - Remove unnecessary if condition in execute-bootstrap.test.ts - Add eslint-disable comments for any type assertions in client.test.ts - Add eslint-disable comments for expect().rejects patterns - Fix template literal number expression with toString() - Fix error handling in test-db.ts to avoid object stringify Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
/**
|
|
* Check if login request is completed and create session if so
|
|
* Public procedure - no authentication required
|
|
*
|
|
* Flow:
|
|
* 1. Read rev.login_request_token cookie
|
|
* 2. If token not found in DB (fake or expired): return { status: 'pending' }
|
|
* 3. If valid login request:
|
|
* - Check if expired: return { status: 'expired' }
|
|
* - Check if not completed: return { status: 'pending' }
|
|
* - If completed:
|
|
* a. Create user_device record
|
|
* b. Create session (trusted_mode = true)
|
|
* c. Delete login_request row
|
|
* d. Set session cookie, clear login_request cookie
|
|
* e. Return { status: 'completed', redirectTo: '/dashboard' or '/auth/trust-device' }
|
|
*/
|
|
|
|
import { withTransaction } from "@reviq/db";
|
|
import {
|
|
COOKIE_NAMES,
|
|
COOKIE_OPTIONS,
|
|
deleteCookie,
|
|
getCookie,
|
|
setCookie,
|
|
} from "../../utils/cookies.js";
|
|
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
|
|
import {
|
|
createSession,
|
|
isDeviceTrusted,
|
|
upsertUserDevice,
|
|
} from "../../utils/session.js";
|
|
import { os } from "../base.js";
|
|
|
|
/**
|
|
* Login if request is completed handler
|
|
* Polls for login completion and creates session when ready
|
|
*/
|
|
export const loginIfRequestIsCompleted =
|
|
os.auth.loginIfRequestIsCompleted.handler(async ({ context }) => {
|
|
// Read login request token from cookie
|
|
const loginRequestToken = getCookie(
|
|
context.reqHeaders,
|
|
COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
|
|
);
|
|
|
|
// No cookie - return pending (shouldn't happen in normal flow)
|
|
if (!loginRequestToken) {
|
|
return { status: "pending" as const };
|
|
}
|
|
|
|
// Fetch login request from database by token
|
|
const loginRequest = await context.db
|
|
.selectFrom("login_requests")
|
|
.select([
|
|
"id",
|
|
"user_id",
|
|
"device_fingerprint",
|
|
"completed_at",
|
|
"expires_at",
|
|
])
|
|
.where("token", "=", loginRequestToken)
|
|
.executeTakeFirst();
|
|
|
|
// Login request not found - might have been deleted or invalid ID
|
|
if (!loginRequest) {
|
|
return { status: "pending" as const };
|
|
}
|
|
|
|
// Check if expired
|
|
if (new Date() > loginRequest.expires_at) {
|
|
return { status: "expired" as const };
|
|
}
|
|
|
|
// Check if not completed yet
|
|
if (loginRequest.completed_at === null) {
|
|
return { status: "pending" as const };
|
|
}
|
|
|
|
// Login request is completed - create session
|
|
const userId = loginRequest.user_id;
|
|
const deviceFingerprint = loginRequest.device_fingerprint;
|
|
|
|
// Device fingerprint should always be present, but handle null case defensively
|
|
if (!deviceFingerprint) {
|
|
return { status: "pending" as const };
|
|
}
|
|
|
|
// Get current request info
|
|
const geo = getGeoInfo(context.reqHeaders, context.clientIP);
|
|
const userAgent = getUserAgent(context.reqHeaders);
|
|
|
|
// Create session in transaction (atomic: device upsert + session + login_request delete)
|
|
const { session, deviceTrusted } = await withTransaction(
|
|
context.db,
|
|
async (trx) => {
|
|
// Upsert user device
|
|
const deviceId = await upsertUserDevice(
|
|
trx,
|
|
userId,
|
|
deviceFingerprint,
|
|
geo,
|
|
userAgent,
|
|
);
|
|
|
|
// Check if device is already trusted
|
|
const trusted = await isDeviceTrusted(trx, userId, deviceFingerprint);
|
|
|
|
// Create session with trusted mode = true (email-confirmed login)
|
|
const newSession = await createSession(trx, {
|
|
userId,
|
|
deviceId,
|
|
trustedMode: true,
|
|
geo,
|
|
userAgent,
|
|
});
|
|
|
|
// Delete the login request (it's been consumed)
|
|
await trx
|
|
.deleteFrom("login_requests")
|
|
.where("id", "=", loginRequest.id)
|
|
.execute();
|
|
|
|
return { session: newSession, deviceTrusted: trusted };
|
|
},
|
|
);
|
|
|
|
// Set session cookie
|
|
setCookie(
|
|
context.resHeaders,
|
|
COOKIE_NAMES.SESSION_TOKEN,
|
|
session.token,
|
|
COOKIE_OPTIONS.session,
|
|
);
|
|
|
|
// Clear login request cookie
|
|
deleteCookie(context.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN);
|
|
|
|
// Determine redirect path based on device trust status
|
|
const redirectTo = deviceTrusted ? "/dashboard" : "/auth/trust-device";
|
|
|
|
return {
|
|
status: "completed" as const,
|
|
redirectTo,
|
|
};
|
|
});
|