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.
323 lines
7.4 KiB
TypeScript
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;
|