@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
187 lines (165 loc) • 4.45 kB
text/typescript
import { sql } from "bun";
export interface KanadiUser {
id: string;
active_device_id?: string;
created_at: Date;
updated_at: Date;
}
export interface KanadiDevice {
id: string;
user_id: string;
session_id: string;
ip: string;
user_agent?: string;
referrer?: string;
fingerprint?: string;
client_fingerprint?: string;
server_fingerprint?: string;
created_at: Date;
updated_at: Date;
}
export class UserRepository {
async findOrCreateUser(deviceInfo: {
sessionId: string;
ip: string;
userAgent?: string;
referrer?: string;
fingerprint?: string;
clientFingerprint?: string;
serverFingerprint?: string;
}): Promise<{ user: KanadiUser; device: KanadiDevice; isNew: boolean }> {
if (deviceInfo.fingerprint) {
const existingDeviceByFingerprint = await sql`
SELECT * FROM kanadi_devices
WHERE fingerprint = ${deviceInfo.fingerprint}
ORDER BY updated_at DESC
LIMIT 1
`;
if (existingDeviceByFingerprint.length > 0) {
const device = existingDeviceByFingerprint[0] as KanadiDevice;
await sql`
UPDATE kanadi_devices
SET session_id = ${deviceInfo.sessionId},
ip = ${deviceInfo.ip},
user_agent = ${deviceInfo.userAgent || null},
referrer = ${deviceInfo.referrer || null},
updated_at = CURRENT_TIMESTAMP
WHERE id = ${device.id}
`;
const user = await this.findById(device.user_id);
return { user: user!, device, isNew: false };
}
}
const existingDeviceBySession = await sql`
SELECT * FROM kanadi_devices
WHERE session_id = ${deviceInfo.sessionId}
LIMIT 1
`;
if (existingDeviceBySession.length > 0) {
const device = existingDeviceBySession[0] as KanadiDevice;
if (deviceInfo.fingerprint) {
await sql`
UPDATE kanadi_devices
SET fingerprint = ${deviceInfo.fingerprint},
client_fingerprint = ${deviceInfo.clientFingerprint || null},
server_fingerprint = ${deviceInfo.serverFingerprint || null},
updated_at = CURRENT_TIMESTAMP
WHERE id = ${device.id}
`;
}
const user = await this.findById(device.user_id);
return { user: user!, device, isNew: false };
}
const newUser = await this.createUser();
const newDevice = await this.createDevice({
userId: newUser.id,
...deviceInfo,
});
await sql`
UPDATE kanadi_users
SET active_device_id = ${newDevice.id}
WHERE id = ${newUser.id}
`;
return { user: newUser, device: newDevice, isNew: true };
}
async createUser(): Promise<KanadiUser> {
const result = await sql`
INSERT INTO kanadi_users DEFAULT VALUES
RETURNING *
`;
return result[0] as KanadiUser;
}
async findById(id: string): Promise<KanadiUser | null> {
const result = await sql`
SELECT * FROM kanadi_users
WHERE id = ${id}
LIMIT 1
`;
return result.length > 0 ? (result[0] as KanadiUser) : null;
}
async createDevice(data: {
userId: string;
sessionId: string;
ip: string;
userAgent?: string;
referrer?: string;
fingerprint?: string;
clientFingerprint?: string;
serverFingerprint?: string;
}): Promise<KanadiDevice> {
const result = await sql`
INSERT INTO kanadi_devices (
user_id,
session_id,
ip,
user_agent,
referrer,
fingerprint,
client_fingerprint,
server_fingerprint
)
VALUES (
${data.userId},
${data.sessionId},
${data.ip},
${data.userAgent || null},
${data.referrer || null},
${data.fingerprint || null},
${data.clientFingerprint || null},
${data.serverFingerprint || null}
)
RETURNING *
`;
return result[0] as KanadiDevice;
}
async findDeviceBySessionId(sessionId: string): Promise<KanadiDevice | null> {
const result = await sql`
SELECT * FROM kanadi_devices
WHERE session_id = ${sessionId}
LIMIT 1
`;
return result.length > 0 ? (result[0] as KanadiDevice) : null;
}
async getStats(): Promise<{
totalUsers: number;
totalDevices: number;
activeToday: number;
}> {
const userResult = await sql`
SELECT COUNT(*) as count FROM kanadi_users
`;
const deviceResult = await sql`
SELECT COUNT(*) as count FROM kanadi_devices
`;
const activeTodayResult = await sql`
SELECT COUNT(DISTINCT user_id) as count
FROM kanadi_devices
WHERE created_at >= CURRENT_DATE
`;
return {
totalUsers: Number(userResult[0]?.count || 0),
totalDevices: Number(deviceResult[0]?.count || 0),
activeToday: Number(activeTodayResult[0]?.count || 0),
};
}
}