Files
myDrive/backend/models/user-model.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

323 lines
7.4 KiB
TypeScript

import mongoose, { Document } from "mongoose";
import validator from "validator";
import crypto from "crypto";
import jwt from "jsonwebtoken";
import env from "../enviroment/env";
import NotAuthorizedError from "../utils/NotAuthorizedError";
import {
encryptToken as encryptTokenUtil,
decryptToken as decryptTokenUtil,
getEncryptionKey as getEncryptionKeyUtil,
generateEncryptionKeyMaterial,
hashPassword,
comparePassword,
signAuthTokens,
signStreamVideoToken,
signTempAuthToken,
signEmailVerifyToken,
signPasswordResetToken,
} from "../utils/userCrypto";
const userSchema = new mongoose.Schema(
{
email: {
type: String,
required: true,
trim: true,
unique: true,
lowercase: true,
validate(value: any): any {
if (!validator.isEmail(value)) {
throw new Error("Email is invalid");
} else if (value.length > 320) {
throw new Error("Email length must be less than 320 characters");
} else if (value.length < 3) {
throw new Error("Email length must be at least 3 characters");
}
},
},
password: {
type: String,
trim: true,
required: true,
validate(value: any): any {
if (value.length < 6) {
throw new Error("Password length must be at least 6 characters");
} else if (value.length > 256) {
throw new Error("Password length must be less than 256 characters");
}
},
},
tokens: [
{
token: {
type: String,
required: true,
},
uuid: {
type: String,
required: true,
},
time: {
type: Number,
required: true,
},
},
],
tempTokens: [
{
token: {
type: String,
required: true,
},
uuid: {
type: String,
required: true,
},
time: {
type: Number,
required: true,
},
},
],
privateKey: {
type: String,
},
publicKey: {
type: String,
},
emailVerified: {
type: Boolean,
},
emailToken: {
type: String,
},
passwordResetToken: {
type: String,
},
passwordLastModified: Number,
},
{
timestamps: true,
}
);
export interface UserInterface extends Document {
_id: mongoose.Types.ObjectId;
email: string;
password: string;
tokens: any[];
tempTokens: any[];
privateKey?: string;
publicKey?: string;
token?: string;
emailVerified?: boolean;
emailToken?: string;
passwordResetToken?: string;
passwordLastModified?: number;
getEncryptionKey: () => Buffer | undefined;
generateTempAuthToken: () => Promise<any>;
encryptToken: (tempToken: any, key: any, publicKey: any) => any;
decryptToken: (encryptedToken: any, key: any, publicKey: any) => any;
findByCreds: (email: string, password: string) => Promise<UserInterface>;
generateAuthToken: (
uuid: string | undefined
) => Promise<{ accessToken: string; refreshToken: string }>;
generateAuthTokenStreamVideo: (uuid: string | undefined) => Promise<string>;
generateEncryptionKeys: () => Promise<void>;
changeEncryptionKey: (randomKey: Buffer) => Promise<void>;
generateEmailVerifyToken: () => Promise<string>;
generatePasswordResetToken: () => Promise<string>;
}
userSchema.pre("save", async function (this: any, next: any) {
const user = this;
if (user.isModified("password")) {
user.password = await hashPassword(user.password);
}
next();
});
userSchema.statics.findByCreds = async (email: string, password: string) => {
const user = await User.findOne({ email });
if (!user) {
throw new NotAuthorizedError("User not found");
}
const isMatch = await comparePassword(password, user.password);
if (!isMatch) {
throw new NotAuthorizedError("Incorrect password");
}
return user;
};
userSchema.methods.toJSON = function () {
const user = this;
const userObject = user.toObject();
delete userObject.password;
delete userObject.tokens;
delete userObject.tempTokens;
delete userObject.privateKey;
delete userObject.publicKey;
if (env.emailVerification !== "true") {
delete userObject.emailVerified;
} else {
userObject.emailVerified = user.emailVerified || false;
}
return userObject;
};
userSchema.methods.generateAuthTokenStreamVideo = async function (
uuid: string | undefined
) {
const user = this;
const { token, encryptedToken, time } = signStreamVideoToken(user);
uuid = uuid ? uuid : "unknown";
await User.updateOne(
{ _id: user._id },
{ $push: { tempTokens: { token: encryptedToken, uuid, time } } }
);
return token;
};
userSchema.methods.generateAuthToken = async function (
uuid: string | undefined
) {
const user = this;
const { accessToken, refreshToken, encryptedRefreshToken, time } =
signAuthTokens(user);
uuid = uuid ? uuid : "unknown";
await User.updateOne(
{ _id: user._id },
{
$push: {
tokens: { token: encryptedRefreshToken, uuid, time },
},
}
);
return { accessToken, refreshToken };
};
userSchema.methods.encryptToken = function (
token: string,
key: string,
iv: any
) {
return encryptTokenUtil(token, key, iv);
};
userSchema.methods.decryptToken = function (
encryptedToken: any,
key: string,
iv: any
) {
return decryptTokenUtil(encryptedToken, key, iv);
};
userSchema.methods.generateEncryptionKeys = async function () {
const user = this;
const randomKey = crypto.randomBytes(32);
const { privateKey, publicKey } = generateEncryptionKeyMaterial(
user.password,
randomKey
);
user.privateKey = privateKey;
user.publicKey = publicKey;
await user.save();
};
userSchema.methods.getEncryptionKey = function () {
return getEncryptionKeyUtil(this);
};
userSchema.methods.changeEncryptionKey = async function (randomKey: Buffer) {
const user = this;
const { privateKey, publicKey } = generateEncryptionKeyMaterial(
user.password,
randomKey
);
user.privateKey = privateKey;
user.publicKey = publicKey;
await user.save();
};
userSchema.methods.generateTempAuthToken = async function () {
const user = this as UserInterface;
const { token, encryptedToken } = signTempAuthToken(user);
user.tempTokens = user.tempTokens.concat({ token: encryptedToken });
await user.save();
return token;
};
userSchema.methods.generateEmailVerifyToken = async function () {
const user = this as UserInterface;
const { token, encryptedToken } = signEmailVerifyToken(user);
user.emailToken = encryptedToken;
await user.save();
return token;
};
userSchema.methods.generatePasswordResetToken = async function () {
const user = this as UserInterface;
const { token, encryptedToken } = signPasswordResetToken(user);
user.passwordResetToken = encryptedToken;
await user.save();
return token;
};
userSchema.methods.generateTempAuthTokenVideo = async function (
cookie: string
) {
const iv = crypto.randomBytes(16);
const user = this;
const token = jwt.sign(
{ _id: user._id.toString(), cookie, iv },
env.passwordAccess!,
{ expiresIn: "5h" }
);
const encryptionKey = user.getEncryptionKey();
const encryptedToken = user.encryptToken(token, encryptionKey, iv);
user.tempTokens = user.tempTokens.concat({ token: encryptedToken });
await user.save();
return token;
};
const User = mongoose.model<UserInterface>("User", userSchema);
export default User;
module.exports = User;