2026-07-19 12:29:52 -05:00
|
|
|
import { and, eq } from "drizzle-orm";
|
|
|
|
|
import { webauthnCredentials } from "../db/schema.js";
|
|
|
|
|
import type { DatabaseContext } from "./database-context.js";
|
2026-08-06 14:41:39 -05:00
|
|
|
import { rowsAffected } from "./mutation-result.js";
|
|
|
|
|
import { insertReturning } from "./returning.js";
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
export type WebauthnCredentialRecord = typeof webauthnCredentials.$inferSelect;
|
|
|
|
|
export type NewWebauthnCredentialRecord =
|
|
|
|
|
typeof webauthnCredentials.$inferInsert;
|
|
|
|
|
|
|
|
|
|
export interface WebauthnCredentialAuthState {
|
|
|
|
|
counter: number;
|
|
|
|
|
backedUp: boolean;
|
|
|
|
|
deviceType: string | null;
|
|
|
|
|
lastUsedAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class WebauthnCredentialRepository {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly context: DatabaseContext,
|
|
|
|
|
private readonly onWrite?: () => void | Promise<void>,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async listByUserId(userId: string): Promise<WebauthnCredentialRecord[]> {
|
|
|
|
|
return this.context.drizzle
|
|
|
|
|
.select()
|
|
|
|
|
.from(webauthnCredentials)
|
|
|
|
|
.where(eq(webauthnCredentials.userId, userId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async findByCredentialId(
|
|
|
|
|
credentialId: string,
|
|
|
|
|
): Promise<WebauthnCredentialRecord | null> {
|
|
|
|
|
const rows = await this.context.drizzle
|
|
|
|
|
.select()
|
|
|
|
|
.from(webauthnCredentials)
|
|
|
|
|
.where(eq(webauthnCredentials.credentialId, credentialId))
|
|
|
|
|
.limit(1);
|
|
|
|
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async create(
|
|
|
|
|
record: NewWebauthnCredentialRecord,
|
|
|
|
|
): Promise<WebauthnCredentialRecord> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const rows = await insertReturning(
|
|
|
|
|
this.context,
|
|
|
|
|
webauthnCredentials,
|
|
|
|
|
record,
|
|
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
return rows[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateAuthState(
|
|
|
|
|
id: string,
|
|
|
|
|
state: WebauthnCredentialAuthState,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await this.context.drizzle
|
|
|
|
|
.update(webauthnCredentials)
|
|
|
|
|
.set(state)
|
|
|
|
|
.where(eq(webauthnCredentials.id, id));
|
|
|
|
|
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteForUser(userId: string, id: string): Promise<boolean> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(webauthnCredentials)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(webauthnCredentials.id, id),
|
|
|
|
|
eq(webauthnCredentials.userId, userId),
|
|
|
|
|
),
|
2026-08-06 14:41:39 -05:00
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
return rowsAffected(result) > 0;
|
2026-07-19 12:29:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async afterWrite(): Promise<void> {
|
|
|
|
|
await this.onWrite?.();
|
|
|
|
|
}
|
|
|
|
|
}
|