Files
myDrive/backend/db/sqliteDB/userDB.ts
T
oxmc ecc340e7b1
Release Please / release-please (push) Has been cancelled
Docker Build and Push (Development) / build-and-push-dev (push) Has been cancelled
feat: add dark mode UI and SQLite as alternate database backend
Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage,
with a theme toggle in the Header and Settings page. Foundation didn't exist
yet despite prior assumption, so it was built from scratch.

SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at
runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail
bytes continue to use the existing fs/S3 storage path. Implemented via a shared
DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations,
selected through dbFactory.ts, with JWT/encryption logic extracted into
userCrypto.ts so both engines share it.
2026-07-05 04:18:33 -07:00

268 lines
7.3 KiB
TypeScript

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;