ecc340e7b1
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.
268 lines
7.3 KiB
TypeScript
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;
|