feat: 添加 passkey 登录功能

This commit is contained in:
Baobhan Sith
2025-05-08 14:13:32 +08:00
parent 56dcbc33e0
commit bc4ae93d7d
20 changed files with 1347 additions and 159 deletions
@@ -1 +1,132 @@
// This file is intentionally left empty as Passkey functionality has been removed.
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
export interface Passkey {
id: number;
user_id: number;
credential_id: string;
public_key: string;
counter: number;
transports: string | null; // JSON string
name: string | null;
backed_up: boolean; // SQLite stores booleans as 0 or 1
last_used_at: number | null;
created_at: number;
updated_at: number;
}
export interface NewPasskey {
user_id: number;
credential_id: string;
public_key: string;
counter: number;
transports?: string | null; // JSON string
name?: string | null;
backed_up?: boolean;
}
// Helper to convert DB result (0/1) to boolean for backed_up field
function mapPasskeyResult(dbResult: any): Passkey | null {
if (!dbResult) return null;
return {
...dbResult,
backed_up: !!dbResult.backed_up, // Ensure boolean
transports: dbResult.transports, // Already string or null
};
}
function mapPasskeyResults(dbResults: any[]): Passkey[] {
return dbResults.map(row => ({
...row,
backed_up: !!row.backed_up,
transports: row.transports,
}));
}
export class PasskeyRepository {
async createPasskey(passkeyData: NewPasskey): Promise<Passkey> {
const db = await getDbInstance();
const sql = `
INSERT INTO passkeys (user_id, credential_id, public_key, counter, transports, name, backed_up, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
RETURNING *
`;
// Note: RETURNING * might not work as expected with the 'sqlite3' package's run method.
// We'll do a SELECT after INSERT if needed, or rely on lastID and then select.
// For simplicity with 'sqlite3', we'll insert then select.
const insertSql = `
INSERT INTO passkeys (user_id, credential_id, public_key, counter, transports, name, backed_up, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
`;
const params = [
passkeyData.user_id,
passkeyData.credential_id,
passkeyData.public_key,
passkeyData.counter,
passkeyData.transports ?? null,
passkeyData.name ?? null,
passkeyData.backed_up ? 1 : 0, // Store boolean as 0 or 1
];
const { lastID } = await runDb(db, insertSql, params);
// Fetch the inserted row
const newPasskey = await this.getPasskeyById(lastID);
if (!newPasskey) {
throw new Error('Failed to create or retrieve passkey after insert.');
}
return newPasskey;
}
async getPasskeyById(id: number): Promise<Passkey | null> {
const db = await getDbInstance();
const sql = 'SELECT * FROM passkeys WHERE id = ?';
const result = await getDb<any>(db, sql, [id]);
return mapPasskeyResult(result);
}
async getPasskeyByCredentialId(credentialId: string): Promise<Passkey | null> {
const db = await getDbInstance();
const sql = 'SELECT * FROM passkeys WHERE credential_id = ?';
const result = await getDb<any>(db, sql, [credentialId]);
return mapPasskeyResult(result);
}
async getPasskeysByUserId(userId: number): Promise<Passkey[]> {
const db = await getDbInstance();
const sql = 'SELECT * FROM passkeys WHERE user_id = ? ORDER BY created_at DESC';
const results = await allDb<any>(db, sql, [userId]);
return mapPasskeyResults(results);
}
async updatePasskeyCounter(credentialId: string, newCounter: number): Promise<boolean> {
const db = await getDbInstance();
const sql = "UPDATE passkeys SET counter = ?, updated_at = strftime('%s', 'now') WHERE credential_id = ?";
const { changes } = await runDb(db, sql, [newCounter, credentialId]);
return changes > 0;
}
async updatePasskeyLastUsedAt(credentialId: string): Promise<boolean> {
const db = await getDbInstance();
const sql = "UPDATE passkeys SET last_used_at = strftime('%s', 'now'), updated_at = strftime('%s', 'now') WHERE credential_id = ?";
const { changes } = await runDb(db, sql, [credentialId]);
return changes > 0;
}
async deletePasskey(credentialId: string): Promise<boolean> {
const db = await getDbInstance();
const sql = 'DELETE FROM passkeys WHERE credential_id = ?';
const { changes } = await runDb(db, sql, [credentialId]);
return changes > 0;
}
async deletePasskeysByUserId(userId: number): Promise<boolean> {
const db = await getDbInstance();
const sql = 'DELETE FROM passkeys WHERE user_id = ?';
const { changes } = await runDb(db, sql, [userId]);
return changes > 0;
}
}
export const passkeyRepository = new PasskeyRepository();
@@ -0,0 +1,30 @@
import { getDbInstance, getDb, allDb } from '../database/connection';
export interface User {
id: number;
username: string;
hashed_password?: string; // Optional, as not always needed by consumers
two_factor_secret?: string | null;
created_at: number;
updated_at: number;
}
export class UserRepository {
async findUserById(id: number): Promise<User | null> {
const db = await getDbInstance();
const sql = 'SELECT id, username, hashed_password, two_factor_secret, created_at, updated_at FROM users WHERE id = ?';
const user = await getDb<User>(db, sql, [id]);
return user ?? null;
}
async findUserByUsername(username: string): Promise<User | null> {
const db = await getDbInstance();
const sql = 'SELECT id, username, hashed_password, two_factor_secret, created_at, updated_at FROM users WHERE username = ?';
const user = await getDb<User>(db, sql, [username]);
return user ?? null;
}
// Add other user-related methods if needed, e.g., createUser, updateUserPassword, etc.
}
export const userRepository = new UserRepository();