- Wrap multiple DB operations in transactions for atomicity: - login-if-completed: device upsert + session + login_request deletion - forgot-password: delete old tokens + insert new token - signup: session + email_verification creation - Extract reusable DB model operations to packages/db/src/models/: - sessions.ts: insertSession() - user-devices.ts: upsertUserDevice(), isDeviceTrusted() - Update session.ts to use new model functions from @reviq/db - Fix type narrowing in admin.test.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
72 lines
1.7 KiB
TypeScript
72 lines
1.7 KiB
TypeScript
/**
|
|
* Database operations for user_devices table
|
|
*/
|
|
|
|
import type { Database } from "@reviq/db-schema";
|
|
import type { Kysely, Transaction } from "kysely";
|
|
|
|
/** Geo information for device tracking */
|
|
export interface DeviceGeoInfo {
|
|
ip: string | null;
|
|
city: string | null;
|
|
region: string | null;
|
|
country: string | null;
|
|
}
|
|
|
|
/**
|
|
* Upsert a user device record
|
|
* Creates new device if not exists, updates last_used_at if exists
|
|
* @returns The device ID
|
|
*/
|
|
export async function upsertUserDevice(
|
|
db: Kysely<Database> | Transaction<Database>,
|
|
userId: number,
|
|
deviceFingerprint: string,
|
|
geo: DeviceGeoInfo,
|
|
userAgent: string,
|
|
): Promise<number> {
|
|
const result = await db
|
|
.insertInto("user_devices")
|
|
.values({
|
|
user_id: userId,
|
|
device_fingerprint: deviceFingerprint,
|
|
user_agent: userAgent,
|
|
ip_address: geo.ip,
|
|
city: geo.city,
|
|
region: geo.region,
|
|
country: geo.country,
|
|
})
|
|
.onConflict((oc) =>
|
|
oc.columns(["user_id", "device_fingerprint"]).doUpdateSet({
|
|
ip_address: geo.ip,
|
|
city: geo.city,
|
|
region: geo.region,
|
|
country: geo.country,
|
|
user_agent: userAgent,
|
|
last_used_at: new Date(),
|
|
}),
|
|
)
|
|
.returning(["id"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
return Number(result.id);
|
|
}
|
|
|
|
/**
|
|
* Check if a device is trusted for a user
|
|
*/
|
|
export async function isDeviceTrusted(
|
|
db: Kysely<Database> | Transaction<Database>,
|
|
userId: number,
|
|
deviceFingerprint: string,
|
|
): Promise<boolean> {
|
|
const device = await db
|
|
.selectFrom("user_devices")
|
|
.select(["is_trusted"])
|
|
.where("user_id", "=", userId)
|
|
.where("device_fingerprint", "=", deviceFingerprint)
|
|
.executeTakeFirst();
|
|
|
|
return device?.is_trusted ?? false;
|
|
}
|