import crypto from "crypto"; import db from "../connections/sqlite"; import { ObjectId } from "mongodb"; import { IUserDB } from "../dbTypes"; import env from "../../enviroment/env"; import NotAuthorizedError from "../../utils/NotAuthorizedError"; import { encryptToken, decryptToken, getEncryptionKey, generateEncryptionKeyMaterial, hashPassword, comparePassword, signAuthTokens, signStreamVideoToken, signTempAuthToken, signEmailVerifyToken, signPasswordResetToken, } from "../../utils/userCrypto"; type TokenRecord = { token: string; uuid?: string; time?: number }; class SqliteUserDoc { _id: string; email: string; password: string; tokens: TokenRecord[]; tempTokens: TokenRecord[]; privateKey?: string; publicKey?: string; emailVerified?: boolean; emailToken?: string; passwordResetToken?: string; passwordLastModified?: number; createdAt: Date; updatedAt: Date; private originalPasswordHash: string; constructor(row: any) { this._id = row.id; this.email = row.email; this.password = row.password; this.originalPasswordHash = row.password; this.privateKey = row.privateKey || undefined; this.publicKey = row.publicKey || undefined; this.emailVerified = !!row.emailVerified; this.emailToken = row.emailToken || undefined; this.passwordResetToken = row.passwordResetToken || undefined; this.passwordLastModified = row.passwordLastModified || undefined; this.createdAt = new Date(row.createdAt); this.updatedAt = new Date(row.updatedAt); const tokenRows = db .prepare("SELECT * FROM user_tokens WHERE user_id = ?") .all(this._id) as any[]; this.tokens = tokenRows .filter((t) => !t.is_temp) .map((t) => ({ token: t.token, uuid: t.uuid, time: t.time })); this.tempTokens = tokenRows .filter((t) => t.is_temp) .map((t) => ({ token: t.token, uuid: t.uuid, time: t.time })); } getEncryptionKey = () => getEncryptionKey(this as any); encryptToken = (token: string, key: any, iv: any) => encryptToken(token, key, iv); decryptToken = (encryptedToken: any, key: string, iv: any) => decryptToken(encryptedToken, key, iv); private persistTokens = () => { db.prepare("DELETE FROM user_tokens WHERE user_id = ?").run(this._id); const insert = db.prepare( "INSERT INTO user_tokens (user_id, token, is_temp, uuid, time) VALUES (?, ?, ?, ?, ?)" ); for (const t of this.tokens) { insert.run(this._id, t.token, 0, t.uuid ?? null, t.time ?? null); } for (const t of this.tempTokens) { insert.run(this._id, t.token, 1, t.uuid ?? null, t.time ?? null); } }; generateAuthToken = async (uuid: string | undefined) => { const { accessToken, refreshToken, encryptedRefreshToken, time } = signAuthTokens(this as any); this.tokens.push({ token: encryptedRefreshToken, uuid: uuid || "unknown", time, }); this.persistTokens(); return { accessToken, refreshToken }; }; generateAuthTokenStreamVideo = async (uuid: string | undefined) => { const { token, encryptedToken: enc, time } = signStreamVideoToken( this as any ); this.tempTokens.push({ token: enc, uuid: uuid || "unknown", time }); this.persistTokens(); return token; }; generateTempAuthToken = async () => { const { token, encryptedToken: enc } = signTempAuthToken(this as any); this.tempTokens.push({ token: enc }); this.persistTokens(); return token; }; generateEncryptionKeys = async () => { const randomKey = crypto.randomBytes(32); const { privateKey, publicKey } = generateEncryptionKeyMaterial( this.password, randomKey ); this.privateKey = privateKey; this.publicKey = publicKey; await this.save(); }; changeEncryptionKey = async (randomKey: Buffer) => { const { privateKey, publicKey } = generateEncryptionKeyMaterial( this.password, randomKey ); this.privateKey = privateKey; this.publicKey = publicKey; await this.save(); }; generateEmailVerifyToken = async () => { const { token, encryptedToken: enc } = signEmailVerifyToken(this as any); this.emailToken = enc; await this.save(); return token; }; generatePasswordResetToken = async () => { const { token, encryptedToken: enc } = signPasswordResetToken( this as any ); this.passwordResetToken = enc; await this.save(); return token; }; save = async () => { if (this.password !== this.originalPasswordHash) { this.password = await hashPassword(this.password); this.originalPasswordHash = this.password; } db.prepare( `UPDATE users SET email = ?, password = ?, privateKey = ?, publicKey = ?, emailVerified = ?, emailToken = ?, passwordResetToken = ?, passwordLastModified = ?, updatedAt = ? WHERE id = ?` ).run( this.email, this.password, this.privateKey ?? null, this.publicKey ?? null, this.emailVerified ? 1 : 0, this.emailToken ?? null, this.passwordResetToken ?? null, this.passwordLastModified ?? null, new Date().toISOString(), this._id ); this.persistTokens(); }; toJSON = () => { const obj: any = { _id: this._id, email: this.email, createdAt: this.createdAt, updatedAt: this.updatedAt, }; if (env.emailVerification === "true") { obj.emailVerified = this.emailVerified || false; } return obj; }; } const rowToUser = (row: any) => (row ? new SqliteUserDoc(row) : null); class UserDB implements IUserDB { constructor() {} getUserInfo = async (userID: string) => { const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userID); return rowToUser(row); }; findByEmail = async (email: string) => { const row = db.prepare("SELECT * FROM users WHERE email = ?").get(email); return rowToUser(row); }; findByCreds = async (email: string, password: string) => { const row = db.prepare("SELECT * FROM users WHERE email = ?").get(email); if (!row) throw new NotAuthorizedError("User not found"); const user = rowToUser(row)!; const isMatch = await comparePassword(password, user.password); if (!isMatch) throw new NotAuthorizedError("Incorrect password"); return user; }; createUser = async (email: string, password: string) => { const id = new ObjectId().toString(); const now = new Date().toISOString(); const hashedPassword = await hashPassword(password); db.prepare( `INSERT INTO users (id, email, password, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)` ).run(id, email, hashedPassword, now, now); const row = db.prepare("SELECT * FROM users WHERE id = ?").get(id); return rowToUser(row); }; removeOldTokens = async ( userID: string, uuid: string | undefined, minusTime: number, isTemp: boolean ) => { uuid = uuid ? uuid : "unknown"; if (uuid === "unknown") return; db.prepare( "DELETE FROM user_tokens WHERE user_id = ? AND is_temp = ? AND uuid = ? AND time < ?" ).run(userID, isTemp ? 1 : 0, uuid, minusTime); }; removeTempTokenByValue = async (userID: string, token: string) => { db.prepare( "DELETE FROM user_tokens WHERE user_id = ? AND is_temp = 1 AND token = ?" ).run(userID, token); }; } export default UserDB;