Files
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

238 lines
6.8 KiB
TypeScript

import { UserInterface } from "../../models/user-model";
import NotFoundError from "../../utils/NotFoundError";
import InternalServerError from "../../utils/InternalServerError";
import sendEmailVerification from "../../utils/sendVerificationEmail";
import env from "../../enviroment/env";
import jwt from "jsonwebtoken";
import sendVerificationEmail from "../../utils/sendVerificationEmail";
import sendPasswordResetEmail from "../../utils/sendPasswordResetEmail";
import ForbiddenError from "../../utils/ForbiddenError";
import ConflictError from "../../utils/ConflictError";
import NotAuthorizedError from "../../utils/NotAuthorizedError";
import { comparePassword } from "../../utils/userCrypto";
import { getUserDB } from "../../db/dbFactory";
type UserDataType = {
email: string;
password: string;
};
type jwtType = {
iv: Buffer;
_id: string;
};
const userDB = getUserDB();
class UserService {
constructor() {}
login = async (userData: UserDataType, uuid: string | undefined) => {
const email = userData.email;
const password = userData.password;
const user = await userDB.findByCreds(email, password);
if (!user) throw new NotFoundError("Cannot Find User");
const { accessToken, refreshToken } = await user.generateAuthToken(uuid);
if (!accessToken || !refreshToken)
throw new NotFoundError("Login User Not Found Error");
return { user, accessToken, refreshToken };
};
logout = async (userID: string, refreshToken: string) => {
const user = await userDB.getUserInfo(userID);
if (!user) throw new NotFoundError("Could Not Find User");
if (refreshToken) {
const decoded = jwt.verify(refreshToken, env.passwordRefresh!) as jwtType;
const encrpytionKey = user.getEncryptionKey();
const encryptedToken = user.encryptToken(
refreshToken,
encrpytionKey,
decoded.iv
);
for (let i = 0; i < user.tokens.length; i++) {
const currentEncryptedToken = user.tokens[i].token;
if (currentEncryptedToken === encryptedToken) {
user.tokens.splice(i, 1);
await user.save();
break;
}
}
}
await user.save();
};
logoutAll = async (userID: string) => {
const user = await userDB.getUserInfo(userID);
if (!user) throw new NotFoundError("Could Not Find User");
user.tokens = [];
user.tempTokens = [];
await user.save();
};
create = async (userData: any, uuid: string | undefined) => {
if (env.createAcctBlocked) {
throw new ForbiddenError("Account Creation Blocked");
}
const userExistsLookedUp = await userDB.findByEmail(userData.email);
if (userExistsLookedUp) {
throw new ConflictError("Email Already Exists");
}
const user = await userDB.createUser(userData.email, userData.password);
if (!user) throw new NotFoundError("User Not Found");
await user.generateEncryptionKeys();
const { accessToken, refreshToken } = await user.generateAuthToken(uuid);
let emailSent = false;
if (env.emailVerification === "true") {
const emailToken = await user.generateEmailVerifyToken();
emailSent = await sendEmailVerification(user, emailToken);
}
if (!accessToken || !refreshToken)
throw new InternalServerError("Could Not Create New User Error");
return { user, accessToken, refreshToken, emailSent };
};
changePassword = async (
userID: string,
oldPassword: string,
newPassword: string,
oldRefreshToken: string,
uuid: string | undefined
) => {
const user = await userDB.getUserInfo(userID);
if (!user) throw new NotAuthorizedError("User information is incorrect");
const date = new Date();
const isMatch = await comparePassword(oldPassword, user.password);
if (!isMatch) throw new NotAuthorizedError("User information is incorrect");
const encryptionKey = user.getEncryptionKey();
user.password = newPassword;
user.tokens = [];
user.tempTokens = [];
user.passwordLastModified = date.getTime();
await user.save();
await user.changeEncryptionKey(encryptionKey!);
const { accessToken, refreshToken } = await user.generateAuthToken(uuid);
return { accessToken, refreshToken };
};
getUserDetailed = async (userID: string) => {
const user = await userDB.getUserInfo(userID);
if (!user) throw new NotFoundError("Cannot find user");
return user;
};
verifyEmail = async (verifyToken: any) => {
const decoded: any = jwt.verify(verifyToken!, env.passwordAccess!);
const iv = decoded.iv;
const user = (await userDB.getUserInfo(decoded._id)) as UserInterface;
const encrpytionKey = user.getEncryptionKey();
const encryptedToken = user.encryptToken(verifyToken, encrpytionKey, iv);
if (encryptedToken === user.emailToken) {
user.emailVerified = true;
await user.save();
return user;
} else {
throw new ForbiddenError("Email Token Verification Failed");
}
};
resendVerifyEmail = async (userID: string) => {
const user = await userDB.getUserInfo(userID);
if (!user) throw new NotFoundError("Cannot find user");
const verifiedEmail = user.emailVerified;
if (env.emailVerification !== "true") {
throw new ForbiddenError("Email Verification Disabled");
}
if (!verifiedEmail) {
const emailToken = await user.generateEmailVerifyToken();
const result = await sendVerificationEmail(user, emailToken);
if (!result) throw new InternalServerError("Email Verification Error");
} else {
throw new ForbiddenError("Email Already Authorized");
}
};
sendPasswordReset = async (email: string) => {
if (env.emailVerification !== "true") {
throw new ForbiddenError("Email Verification Not Enabled");
}
const user = await userDB.findByEmail(email);
if (!user) throw new NotFoundError("User Not Found Password Reset Email");
const passwordResetToken = await user.generatePasswordResetToken();
await sendPasswordResetEmail(user, passwordResetToken!);
};
resetPassword = async (newPassword: string, verifyToken: any) => {
const decoded: any = jwt.verify(verifyToken!, env.passwordAccess!);
const iv = decoded.iv;
const user = (await userDB.getUserInfo(decoded._id)) as UserInterface;
const encrpytionKey = user.getEncryptionKey();
const encryptedToken = user.encryptToken(verifyToken, encrpytionKey, iv);
if (encryptedToken === user.passwordResetToken) {
const encryptionKey = user.getEncryptionKey();
user.password = newPassword;
user.tokens = [];
user.tempTokens = [];
user.passwordResetToken = undefined;
await user.save();
await user.changeEncryptionKey(encryptionKey!);
} else {
throw new ForbiddenError("Reset Password Token Do Not Match");
}
};
}
export default UserService;