diff --git a/.env.example b/.env.example index 3a40463..885cb46 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,20 @@ # If you are using Docker, set DOCKER=true DOCKER=true -# MongoDB URL: Connection string for your MongoDB database +# Database Engine: Choose between "mongo" and "sqlite", this specifies where file/folder/user METADATA is stored. +# This is separate from DB_TYPE below, which controls where the raw file BYTES are stored. +# mongo = MongoDB (default) +# sqlite = embedded SQLite file, no separate database server needed +DB_ENGINE=mongo + +# MongoDB URL: Connection string for your MongoDB database (only used when DB_ENGINE=mongo) # Note: if using the compose file provided, the connection string should be as follows: MONGODB_URL=mongodb://username:password@mongo:27017/mydrive?authSource=admin +# SQLite DB Path (only used when DB_ENGINE=sqlite): path to the SQLite database file. +# Should live inside a persisted volume (e.g. the same one used for FS_DIRECTORY) so it survives restarts. +SQLITE_DB_PATH=/data/mydrive.db + # Database Type: Choose between "fs" and "s3", this specifies where the files will be stored. # fs = Filesystem # s3 = Amazon S3 diff --git a/backend/controllers/file-controller.ts b/backend/controllers/file-controller.ts index 5edbc86..27fae70 100644 --- a/backend/controllers/file-controller.ts +++ b/backend/controllers/file-controller.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from "express"; import FileService from "../services/file-service/file-service"; -import User, { UserInterface } from "../models/user-model"; +import { UserInterface } from "../models/user-model"; import { createStreamVideoCookie, removeStreamVideoCookie, @@ -10,8 +10,10 @@ import streamToBuffer from "../utils/streamToBuffer"; import NotAuthorizedError from "../utils/NotAuthorizedError"; import { FileListQueryType } from "../types/file-types"; import fs from "fs"; +import { getUserDB } from "../db/dbFactory"; const fileService = new FileService(); +const userDB = getUserDB(); type userAccessType = { _id: string; emailVerified: boolean; @@ -318,9 +320,9 @@ class FileController { throw new NotAuthorizedError("No Access Token"); } - await User.updateOne( - { _id: userID }, - { $pull: { tempTokens: { token: accessTokenStreamVideo } } } + await userDB.removeTempTokenByValue( + userID.toString(), + accessTokenStreamVideo ); removeStreamVideoCookie(res); @@ -534,7 +536,7 @@ class FileController { const trashedFile = await fileService.trashFile(userID, fileID); - res.send(trashedFile.toObject()); + res.send(trashedFile); } catch (e) { next(e); } diff --git a/backend/db/connections/sqlite.ts b/backend/db/connections/sqlite.ts new file mode 100644 index 0000000..b1856bc --- /dev/null +++ b/backend/db/connections/sqlite.ts @@ -0,0 +1,104 @@ +import Database from "better-sqlite3"; +import fs from "fs"; +import path from "path"; +import env from "../../enviroment/env"; + +const dbPath = env.sqliteDBPath as string; + +fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + +const db = new Database(dbPath); + +db.pragma("journal_mode = WAL"); +db.pragma("foreign_keys = ON"); + +db.exec(` + CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY, + filename TEXT NOT NULL, + length INTEGER NOT NULL, + chunkSize INTEGER, + uploadDate TEXT NOT NULL, + owner TEXT NOT NULL, + parent TEXT NOT NULL, + parentList TEXT NOT NULL, + hasThumbnail INTEGER NOT NULL DEFAULT 0, + isVideo INTEGER NOT NULL DEFAULT 0, + thumbnailID TEXT, + size INTEGER NOT NULL, + iv BLOB NOT NULL, + linkType TEXT, + link TEXT, + filePath TEXT, + s3ID TEXT, + personalFile INTEGER, + trashed INTEGER, + processingFile INTEGER + ); + + CREATE INDEX IF NOT EXISTS idx_files_owner_parent_filename ON files(owner, parent, filename COLLATE NOCASE); + CREATE INDEX IF NOT EXISTS idx_files_owner_parent_upload ON files(owner, parent, uploadDate); + CREATE INDEX IF NOT EXISTS idx_files_trashed ON files(trashed); + CREATE INDEX IF NOT EXISTS idx_files_hasThumbnail ON files(hasThumbnail); + CREATE INDEX IF NOT EXISTS idx_files_parentList ON files(parentList); + + CREATE TABLE IF NOT EXISTS folders ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent TEXT NOT NULL, + owner TEXT NOT NULL, + parentList TEXT NOT NULL, + personalFolder INTEGER, + trashed INTEGER, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_folders_owner ON folders(owner); + CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent); + CREATE INDEX IF NOT EXISTS idx_folders_trashed ON folders(trashed); + CREATE INDEX IF NOT EXISTS idx_folders_name ON folders(name COLLATE NOCASE); + CREATE INDEX IF NOT EXISTS idx_folders_createdAt ON folders(createdAt); + CREATE INDEX IF NOT EXISTS idx_folders_parentList ON folders(parentList); + + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + privateKey TEXT, + publicKey TEXT, + emailVerified INTEGER, + emailToken TEXT, + passwordResetToken TEXT, + passwordLastModified INTEGER, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS user_tokens ( + user_id TEXT NOT NULL, + token TEXT NOT NULL, + is_temp INTEGER NOT NULL DEFAULT 0, + uuid TEXT, + time INTEGER + ); + + CREATE INDEX IF NOT EXISTS idx_user_tokens_user ON user_tokens(user_id, is_temp); + + CREATE TABLE IF NOT EXISTS thumbnails ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + owner TEXT NOT NULL, + data BLOB, + path TEXT, + iv BLOB, + s3ID TEXT, + personalFile INTEGER, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_thumbnails_owner ON thumbnails(owner); +`); + +export default db; diff --git a/backend/db/dbFactory.ts b/backend/db/dbFactory.ts new file mode 100644 index 0000000..300ae4d --- /dev/null +++ b/backend/db/dbFactory.ts @@ -0,0 +1,45 @@ +import env from "../enviroment/env"; +import { IFileDB, IFolderDB, IUserDB, IThumbnailDB } from "./dbTypes"; + +import MongoFileDB from "./mongoDB/fileDB"; +import MongoFolderDB from "./mongoDB/folderDB"; +import MongoUserDB from "./mongoDB/userDB"; +import MongoThumbnailDB from "./mongoDB/thumbnailDB"; + +// The sqlite/* modules are required lazily (rather than statically imported) +// so that opening the sqlite connection - a side effect of importing +// db/connections/sqlite - only happens when DB_ENGINE=sqlite is actually +// selected, not on every process that merely imports this factory. +const isSqlite = () => env.dbEngine === "sqlite"; + +export const getFileDB = (): IFileDB => { + if (isSqlite()) { + const SqliteFileDB = require("./sqliteDB/fileDB").default; + return new SqliteFileDB(); + } + return new MongoFileDB(); +}; + +export const getFolderDB = (): IFolderDB => { + if (isSqlite()) { + const SqliteFolderDB = require("./sqliteDB/folderDB").default; + return new SqliteFolderDB(); + } + return new MongoFolderDB(); +}; + +export const getUserDB = (): IUserDB => { + if (isSqlite()) { + const SqliteUserDB = require("./sqliteDB/userDB").default; + return new SqliteUserDB(); + } + return new MongoUserDB(); +}; + +export const getThumbnailDB = (): IThumbnailDB => { + if (isSqlite()) { + const SqliteThumbnailDB = require("./sqliteDB/thumbnailDB").default; + return new SqliteThumbnailDB(); + } + return new MongoThumbnailDB(); +}; diff --git a/backend/db/dbTypes.ts b/backend/db/dbTypes.ts new file mode 100644 index 0000000..e06c5bd --- /dev/null +++ b/backend/db/dbTypes.ts @@ -0,0 +1,150 @@ +import { FileListQueryType } from "../types/file-types"; +import { FolderListQueryType } from "../types/folder-types"; + +export interface IFileDB { + getPublicFile(fileID: string): Promise; + getPublicInfo(fileID: string, tempToken: string): Promise; + getFileInfo(fileID: string, userID: string): Promise; + getQuickList(userID: string, limit: number): Promise; + getList( + queryData: FileListQueryType, + sortBy: string, + limit: number + ): Promise; + getFileSearchList( + userID: string, + searchQuery: RegExp | string, + trashMode: boolean, + mediaMode: boolean + ): Promise; + getFileListByIncludedParent( + userID: string, + parentListString: string + ): Promise; + getFileListByOwner(userID: string): Promise; + getFileListByParent(userID: string, parent: string): Promise; + updateFileUploadedFile( + fileID: string, + userID: string, + parent: string, + parentList: string + ): Promise; + updateFolderUploadedFile( + fileID: string, + userID: string, + parent: string, + parentList: string + ): Promise; + setThumbnail(fileID: string, thumbnailID: string): Promise; + removeOneTimePublicLink(fileID: string): Promise; + removeLink(fileID: string, userID: string): Promise; + makePublic(fileID: string, userID: string, token: string): Promise; + makeOneTimePublic( + fileID: string, + userID: string, + token: string + ): Promise; + trashFile( + fileID: string, + parent: string, + parentList: string, + userID: string + ): Promise; + restoreFile(fileID: string, userID: string): Promise; + removeTempToken(user: any, tempToken: string): Promise; + trashFilesByParent(parentList: string, userID: string): Promise; + restoreFilesByParent(parentList: string, userID: string): Promise; + renameFile(fileID: string, userID: string, title: string): Promise; + moveFile( + fileID: string, + userID: string, + parent: string, + parentList: string + ): Promise; + moveMultipleFiles( + userID: string, + currentParent: string, + newParent: string, + newParentList: string + ): Promise; + deleteFile(fileID: string, userID: string): Promise; + createFile(data: { filename: string; uploadDate: string; length: number; metadata: any }): Promise; + setVideoThumbnail( + fileID: string, + userID: string, + thumbnailID: string + ): Promise; +} + +export interface IFolderDB { + getFolderSearchList( + userID: string, + searchQuery: RegExp | string, + trashMode: boolean + ): Promise; + getFolderInfo(folderID: string, userID: string): Promise; + getFolderList( + queryData: FolderListQueryType, + sortBy: string + ): Promise; + getMoveFolderList( + userID: string, + parent?: string, + search?: string, + folderIDs?: string[] + ): Promise; + getFolderListByIncludedParent( + userID: string, + parent: string + ): Promise; + findAllFoldersByParent(parentID: string, userID: string): Promise; + moveFolder( + folderID: string, + userID: string, + parent: string, + parentList: string[] + ): Promise; + renameFolder(folderID: string, userID: string, title: string): Promise; + trashFoldersByParent(parentList: string[], userID: string): Promise; + restoreFolder(folderID: string, userID: string): Promise; + restoreFoldersByParent(parentList: string[], userID: string): Promise; + trashFolder(folderID: string, userID: string): Promise; + createFolder(folderData: { + name: string; + parent: string; + parentList: string[]; + owner: string; + }): Promise; + deleteFolder(folderID: string, userID: string): Promise; + deleteFoldersByParentList( + parentList: string[], + userID: string + ): Promise; + deleteFoldersByOwner(userID: string): Promise; +} + +export interface IUserDB { + getUserInfo(userID: string): Promise; + findByEmail(email: string): Promise; + findByCreds(email: string, password: string): Promise; + createUser(email: string, password: string): Promise; + removeOldTokens( + userID: string, + uuid: string | undefined, + minusTime: number, + isTemp: boolean + ): Promise; + removeTempTokenByValue(userID: string, token: string): Promise; +} + +export interface IThumbnailDB { + getThumbnailInfo(userID: string, thumbnailID: string): Promise; + removeThumbnail(userID: string, thumbnailID: any): Promise; + createThumbnail(data: { + name: string; + owner: string; + IV: Buffer; + path?: string; + s3ID?: string; + }): Promise; +} diff --git a/backend/db/mongoDB/fileDB.ts b/backend/db/mongoDB/fileDB.ts index 7291c2f..d75fd4c 100644 --- a/backend/db/mongoDB/fileDB.ts +++ b/backend/db/mongoDB/fileDB.ts @@ -5,8 +5,9 @@ import { UserInterface } from "../../models/user-model"; import { createFileQuery } from "../../utils/createQuery"; import { FileListQueryType } from "../../types/file-types"; import sortBySwitch from "../../utils/sortBySwitch"; +import { IFileDB } from "../dbTypes"; -class DbUtil { +class DbUtil implements IFileDB { constructor() {} // READ @@ -370,6 +371,45 @@ class DbUtil { }); return result; }; + + // CREATE + + createFile = async (data: { + filename: string; + uploadDate: string; + length: number; + metadata: any; + }) => { + const file = new File(data as any); + await file.save(); + return file; + }; + + setVideoThumbnail = async ( + fileID: string, + userID: string, + thumbnailID: string + ) => { + const updateResponse = await File.updateOne( + { _id: new ObjectId(fileID), "metadata.owner": userID }, + { + $set: { + "metadata.hasThumbnail": true, + "metadata.thumbnailID": thumbnailID, + "metadata.isVideo": true, + }, + } + ); + + if (updateResponse.modifiedCount === 0) return null; + + const updatedFile = await File.findOne({ + _id: new ObjectId(fileID), + "metadata.owner": userID, + }); + + return updatedFile; + }; } export default DbUtil; diff --git a/backend/db/mongoDB/folderDB.ts b/backend/db/mongoDB/folderDB.ts index 0ef7f53..2642412 100644 --- a/backend/db/mongoDB/folderDB.ts +++ b/backend/db/mongoDB/folderDB.ts @@ -3,8 +3,9 @@ import { ObjectId } from "mongodb"; import { FolderListQueryType } from "../../types/folder-types"; import { createFolderQuery } from "../../utils/createQuery"; import sortBySwitch from "../../utils/sortBySwitchFolder"; +import { IFolderDB } from "../dbTypes"; -class DbUtil { +class DbUtil implements IFolderDB { constructor() {} // READ diff --git a/backend/db/mongoDB/thumbnailDB.ts b/backend/db/mongoDB/thumbnailDB.ts index 474d910..17221cd 100644 --- a/backend/db/mongoDB/thumbnailDB.ts +++ b/backend/db/mongoDB/thumbnailDB.ts @@ -1,7 +1,8 @@ import Thumbnail from "../../models/thumbnail-model"; import { ObjectId } from "mongodb"; +import { IThumbnailDB } from "../dbTypes"; -class ThumbnailDB { +class ThumbnailDB implements IThumbnailDB { constructor() {} // READ @@ -23,6 +24,20 @@ class ThumbnailDB { }); return result; }; + + // CREATE + + createThumbnail = async (data: { + name: string; + owner: string; + IV: Buffer; + path?: string; + s3ID?: string; + }) => { + const thumbnail = new Thumbnail(data as any); + await thumbnail.save(); + return thumbnail; + }; } export default ThumbnailDB; diff --git a/backend/db/mongoDB/userDB.ts b/backend/db/mongoDB/userDB.ts index eae997e..195bbc9 100644 --- a/backend/db/mongoDB/userDB.ts +++ b/backend/db/mongoDB/userDB.ts @@ -1,15 +1,59 @@ import { ObjectId } from "mongodb"; import User from "../../models/user-model"; +import { IUserDB } from "../dbTypes"; // READ -class UserDB { +class UserDB implements IUserDB { constructor() {} getUserInfo = async (userID: string) => { const user = await User.findOne({ _id: new ObjectId(userID) }); return user; }; + + findByEmail = async (email: string) => { + const user = await User.findOne({ email }); + return user; + }; + + findByCreds = async (email: string, password: string) => { + const userStatics = User as unknown as { + findByCreds: (email: string, password: string) => Promise; + }; + return userStatics.findByCreds(email, password); + }; + + createUser = async (email: string, password: string) => { + const user = new User({ email, password }); + await user.save(); + return user; + }; + + removeOldTokens = async ( + userID: string, + uuid: string | undefined, + minusTime: number, + isTemp: boolean + ) => { + uuid = uuid ? uuid : "unknown"; + + if (uuid === "unknown") return; + + const field = isTemp ? "tempTokens" : "tokens"; + + await User.updateOne( + { _id: new ObjectId(userID) }, + { $pull: { [field]: { uuid, time: { $lt: minusTime } } } } + ); + }; + + removeTempTokenByValue = async (userID: string, token: string) => { + await User.updateOne( + { _id: new ObjectId(userID) }, + { $pull: { tempTokens: { token } } } + ); + }; } export default UserDB; diff --git a/backend/db/sqliteDB/fileDB.ts b/backend/db/sqliteDB/fileDB.ts new file mode 100644 index 0000000..8ac21bd --- /dev/null +++ b/backend/db/sqliteDB/fileDB.ts @@ -0,0 +1,452 @@ +import db from "../connections/sqlite"; +import { ObjectId } from "mongodb"; +import { IFileDB } from "../dbTypes"; +import { FileListQueryType } from "../../types/file-types"; + +const rowToFile = (row: any) => { + if (!row) return null; + + return { + _id: row.id, + length: row.length, + chunkSize: row.chunkSize, + uploadDate: row.uploadDate, + filename: row.filename, + metadata: { + owner: row.owner, + parent: row.parent, + parentList: row.parentList, + hasThumbnail: !!row.hasThumbnail, + isVideo: !!row.isVideo, + thumbnailID: row.thumbnailID || undefined, + size: row.size, + IV: row.iv, + linkType: row.linkType || undefined, + link: row.link || undefined, + filePath: row.filePath || undefined, + s3ID: row.s3ID || undefined, + personalFile: row.personalFile ? true : undefined, + trashed: row.trashed ? true : null, + processingFile: row.processingFile ? true : null, + }, + }; +}; + +const sortColumn = (sortBy: string): { column: string; direction: string } => { + if (sortBy === "date_desc") return { column: "uploadDate", direction: "DESC" }; + if (sortBy === "date_asc") return { column: "uploadDate", direction: "ASC" }; + if (sortBy === "alp_desc") + return { column: "filename COLLATE NOCASE", direction: "DESC" }; + return { column: "filename COLLATE NOCASE", direction: "ASC" }; +}; + +class FileDB implements IFileDB { + constructor() {} + + // READ + + getPublicFile = async (fileID: string) => { + const row = db.prepare("SELECT * FROM files WHERE id = ?").get(fileID); + return rowToFile(row); + }; + + getPublicInfo = async (fileID: string, tempToken: string) => { + const row = db + .prepare("SELECT * FROM files WHERE id = ? AND link = ?") + .get(fileID, tempToken); + return rowToFile(row); + }; + + getFileInfo = async (fileID: string, userID: string) => { + const row = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + return rowToFile(row); + }; + + getQuickList = async (userID: string, limit: number) => { + const rows = db + .prepare( + `SELECT * FROM files WHERE owner = ? AND trashed = 0 AND processingFile = 0 + ORDER BY uploadDate DESC LIMIT ?` + ) + .all(userID, limit); + return rows.map(rowToFile); + }; + + getList = async ( + queryData: FileListQueryType, + sortBy: string, + limit: number + ) => { + const { + userID, + search, + parent, + startAtDate, + startAtName, + trashMode, + mediaMode, + mediaFilter, + } = queryData; + + const conditions: string[] = ["owner = ?"]; + const params: any[] = [userID]; + + if (search && search !== "") { + conditions.push("filename LIKE ? COLLATE NOCASE"); + params.push(`%${search}%`); + } else if (!mediaMode) { + conditions.push("parent = ?"); + params.push(parent); + } + + if (sortBy === "date_desc" && startAtDate) { + conditions.push("uploadDate < ?"); + params.push(new Date(startAtDate).toISOString()); + } else if (sortBy === "date_asc" && startAtDate) { + conditions.push("uploadDate > ?"); + params.push(new Date(startAtDate).toISOString()); + } else if (sortBy === "alp_desc" && startAtName) { + conditions.push("filename < ? COLLATE NOCASE"); + params.push(startAtName); + } else if (sortBy === "alp_asc" && startAtName) { + conditions.push("filename > ? COLLATE NOCASE"); + params.push(startAtName); + } + + conditions.push("trashed = ?"); + params.push(trashMode ? 1 : 0); + + if (mediaMode) { + conditions.push("hasThumbnail = 1"); + if (mediaFilter === "photos") conditions.push("isVideo = 0"); + else if (mediaFilter === "videos") conditions.push("isVideo = 1"); + } + + conditions.push("processingFile = 0"); + + const { column, direction } = sortColumn(sortBy); + + const rows = db + .prepare( + `SELECT * FROM files WHERE ${conditions.join( + " AND " + )} ORDER BY ${column} ${direction} LIMIT ?` + ) + .all(...params, limit); + + return rows.map(rowToFile); + }; + + getFileSearchList = async ( + userID: string, + searchQuery: RegExp | string, + trashMode: boolean, + mediaMode: boolean + ) => { + const search = + searchQuery instanceof RegExp ? searchQuery.source : searchQuery; + + const conditions = [ + "owner = ?", + "filename LIKE ? COLLATE NOCASE", + "trashed = ?", + "processingFile = 0", + ]; + const params: any[] = [userID, `%${search}%`, trashMode ? 1 : 0]; + + if (mediaMode) conditions.push("hasThumbnail = 1"); + + const rows = db + .prepare( + `SELECT * FROM files WHERE ${conditions.join(" AND ")} LIMIT 10` + ) + .all(...params); + + return rows.map(rowToFile); + }; + + getFileListByIncludedParent = async ( + userID: string, + parentListString: string + ) => { + const rows = db + .prepare( + "SELECT * FROM files WHERE owner = ? AND parentList LIKE ?" + ) + .all(userID, `%${parentListString}%`); + return rows.map(rowToFile); + }; + + getFileListByOwner = async (userID: string) => { + const rows = db.prepare("SELECT * FROM files WHERE owner = ?").all(userID); + return rows.map(rowToFile); + }; + + getFileListByParent = async (userID: string, parent: string) => { + const rows = db + .prepare("SELECT * FROM files WHERE owner = ? AND parent = ?") + .all(userID, parent); + return rows.map(rowToFile); + }; + + // UPDATE + + updateFileUploadedFile = async ( + fileID: string, + userID: string, + parent: string, + parentList: string + ) => { + db.prepare( + `UPDATE files SET parent = ?, parentList = ?, processingFile = 0 + WHERE id = ? AND owner = ?` + ).run(parent, parentList, fileID, userID); + + return this.getFileInfo(fileID, userID); + }; + + updateFolderUploadedFile = async ( + fileID: string, + userID: string, + parent: string, + parentList: string + ) => { + return this.updateFileUploadedFile(fileID, userID, parent, parentList); + }; + + setThumbnail = async (fileID: string, thumbnailID: string) => { + const result = db + .prepare( + `UPDATE files SET hasThumbnail = 1, thumbnailID = ? + WHERE id = ? AND hasThumbnail = 0` + ) + .run(thumbnailID, fileID); + + if (result.changes === 0) return null; + + const row = db.prepare("SELECT * FROM files WHERE id = ?").get(fileID); + return rowToFile(row); + }; + + setVideoThumbnail = async ( + fileID: string, + userID: string, + thumbnailID: string + ) => { + const result = db + .prepare( + `UPDATE files SET hasThumbnail = 1, thumbnailID = ?, isVideo = 1 + WHERE id = ? AND owner = ?` + ) + .run(thumbnailID, fileID, userID); + + if (result.changes === 0) return null; + + return this.getFileInfo(fileID, userID); + }; + + removeOneTimePublicLink = async (fileID: string) => { + db.prepare( + "UPDATE files SET linkType = NULL, link = NULL WHERE id = ?" + ).run(fileID); + const row = db.prepare("SELECT * FROM files WHERE id = ?").get(fileID); + return rowToFile(row); + }; + + removeLink = async (fileID: string, userID: string) => { + db.prepare( + "UPDATE files SET linkType = NULL, link = NULL WHERE id = ? AND owner = ?" + ).run(fileID, userID); + return this.getFileInfo(fileID, userID); + }; + + makePublic = async (fileID: string, userID: string, token: string) => { + const existing: any = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE files SET linkType = 'public', link = ? WHERE id = ? AND owner = ?" + ).run(token, fileID, userID); + + return this.getFileInfo(fileID, userID); + }; + + makeOneTimePublic = async (fileID: string, userID: string, token: string) => { + const existing: any = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE files SET linkType = 'one', link = ? WHERE id = ? AND owner = ?" + ).run(token, fileID, userID); + + return this.getFileInfo(fileID, userID); + }; + + trashFile = async ( + fileID: string, + parent: string, + parentList: string, + userID: string + ) => { + const existing: any = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE files SET trashed = 1, parent = ?, parentList = ? WHERE id = ? AND owner = ?" + ).run(parent, parentList, fileID, userID); + + return this.getFileInfo(fileID, userID); + }; + + restoreFile = async (fileID: string, userID: string) => { + const existing: any = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + + if (!existing) return null; + + db.prepare("UPDATE files SET trashed = 0 WHERE id = ? AND owner = ?").run( + fileID, + userID + ); + + return this.getFileInfo(fileID, userID); + }; + + removeTempToken = async (user: any, tempToken: string) => { + user.tempTokens = user.tempTokens.filter( + (filterToken: any) => filterToken.token !== tempToken + ); + return user; + }; + + trashFilesByParent = async (parentList: string, userID: string) => { + const result = db + .prepare( + "UPDATE files SET trashed = 1 WHERE owner = ? AND parentList LIKE ?" + ) + .run(userID, `%${parentList}%`); + return result; + }; + + restoreFilesByParent = async (parentList: string, userID: string) => { + const result = db + .prepare( + "UPDATE files SET trashed = 0 WHERE owner = ? AND parentList LIKE ?" + ) + .run(userID, `%${parentList}%`); + return result; + }; + + renameFile = async (fileID: string, userID: string, title: string) => { + const existing: any = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + + if (!existing) return null; + + db.prepare("UPDATE files SET filename = ? WHERE id = ? AND owner = ?").run( + title, + fileID, + userID + ); + + return this.getFileInfo(fileID, userID); + }; + + moveFile = async ( + fileID: string, + userID: string, + parent: string, + parentList: string + ) => { + const existing: any = db + .prepare("SELECT * FROM files WHERE id = ? AND owner = ?") + .get(fileID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE files SET parent = ?, parentList = ? WHERE id = ? AND owner = ?" + ).run(parent, parentList, fileID, userID); + + return this.getFileInfo(fileID, userID); + }; + + moveMultipleFiles = async ( + userID: string, + currentParent: string, + newParent: string, + newParentList: string + ) => { + db.prepare( + "UPDATE files SET parent = ?, parentList = ? WHERE owner = ? AND parent = ?" + ).run(newParent, newParentList, userID, currentParent); + }; + + // DELETE + + deleteFile = async (fileID: string, userID: string) => { + const result = db + .prepare("DELETE FROM files WHERE id = ? AND owner = ?") + .run(fileID, userID); + return result; + }; + + // CREATE + + createFile = async (data: { + filename: string; + uploadDate: string; + length: number; + metadata: any; + }) => { + const id = new ObjectId().toString(); + const m = data.metadata || {}; + + db.prepare( + `INSERT INTO files ( + id, filename, length, chunkSize, uploadDate, owner, parent, parentList, + hasThumbnail, isVideo, thumbnailID, size, iv, linkType, link, filePath, + s3ID, personalFile, trashed, processingFile + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + id, + data.filename, + data.length, + null, + data.uploadDate, + m.owner, + m.parent, + m.parentList, + m.hasThumbnail ? 1 : 0, + m.isVideo ? 1 : 0, + m.thumbnailID || null, + m.size, + m.IV, + m.linkType || null, + m.link || null, + m.filePath || null, + m.s3ID || null, + m.personalFile ? 1 : 0, + m.trashed ? 1 : 0, + m.processingFile ? 1 : 0 + ); + + const row = db.prepare("SELECT * FROM files WHERE id = ?").get(id); + return rowToFile(row); + }; +} + +export default FileDB; diff --git a/backend/db/sqliteDB/folderDB.ts b/backend/db/sqliteDB/folderDB.ts new file mode 100644 index 0000000..8e8c1ad --- /dev/null +++ b/backend/db/sqliteDB/folderDB.ts @@ -0,0 +1,307 @@ +import db from "../connections/sqlite"; +import { ObjectId } from "mongodb"; +import { IFolderDB } from "../dbTypes"; +import { FolderListQueryType } from "../../types/folder-types"; + +const encodeParentList = (parentList: (string | any)[]): string => + parentList.map((id) => id.toString()).join(","); + +const decodeParentList = (parentList: string): string[] => + parentList ? parentList.split(",").filter((p) => p !== "") : []; + +const rowToFolder = (row: any) => { + if (!row) return null; + + return { + _id: row.id, + name: row.name, + parent: row.parent, + owner: row.owner, + parentList: decodeParentList(row.parentList), + personalFolder: row.personalFolder ? true : undefined, + trashed: row.trashed ? true : null, + createdAt: new Date(row.createdAt), + updatedAt: new Date(row.updatedAt), + }; +}; + +// Wraps the stored comma-joined parentList with delimiters at query time so +// membership checks can't match a partial id (mirrors Mongo's array $in/$all/$nin +// semantics, which match whole elements, not substrings). +const containsClause = () => "(',' || parentList || ',') LIKE ?"; +const containsParam = (id: string) => `%,${id},%`; + +const sortColumn = (sortBy: string): { column: string; direction: string } => { + if (sortBy === "alp_asc") return { column: "name COLLATE NOCASE", direction: "ASC" }; + if (sortBy === "alp_desc") return { column: "name COLLATE NOCASE", direction: "DESC" }; + if (sortBy === "date_asc") return { column: "createdAt", direction: "ASC" }; + return { column: "createdAt", direction: "DESC" }; +}; + +class FolderDB implements IFolderDB { + constructor() {} + + // READ + + getFolderSearchList = async ( + userID: string, + searchQuery: RegExp | string, + trashMode: boolean + ) => { + const search = + searchQuery instanceof RegExp ? searchQuery.source : searchQuery; + + const rows = db + .prepare( + `SELECT * FROM folders WHERE owner = ? AND name LIKE ? COLLATE NOCASE AND trashed = ? + LIMIT 10` + ) + .all(userID, `%${search}%`, trashMode ? 1 : 0); + + return rows.map(rowToFolder); + }; + + getFolderInfo = async (folderID: string, userID: string) => { + const row = db + .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") + .get(folderID, userID); + return rowToFolder(row); + }; + + getFolderList = async (queryData: FolderListQueryType, sortBy: string) => { + const { userID, search, parent, trashMode } = queryData; + + const conditions = ["owner = ?"]; + const params: any[] = [userID]; + + if (search && search !== "") { + conditions.push("name LIKE ? COLLATE NOCASE"); + params.push(`%${search}%`); + } else { + conditions.push("parent = ?"); + params.push(parent); + } + + conditions.push("trashed = ?"); + params.push(trashMode ? 1 : 0); + + const { column, direction } = sortColumn(sortBy); + + const rows = db + .prepare( + `SELECT * FROM folders WHERE ${conditions.join( + " AND " + )} ORDER BY ${column} ${direction}` + ) + .all(...params); + + return rows.map(rowToFolder); + }; + + getMoveFolderList = async ( + userID: string, + parent = "/", + search?: string, + folderIDs?: string[] + ) => { + const conditions = ["owner = ?", "trashed = 0"]; + const params: any[] = [userID]; + + if (!search || search === "") { + conditions.push("parent = ?"); + params.push(parent); + } else { + conditions.push("name LIKE ? COLLATE NOCASE"); + params.push(`%${search}%`); + } + + const rows = db + .prepare( + `SELECT * FROM folders WHERE ${conditions.join( + " AND " + )} ORDER BY createdAt DESC` + ) + .all(...params) as any[]; + + const filtered = + folderIDs && folderIDs.length > 0 + ? rows.filter((row) => { + if (folderIDs.includes(row.id)) return false; + const list = decodeParentList(row.parentList); + return !folderIDs.some((id) => list.includes(id)); + }) + : rows; + + return filtered.map(rowToFolder); + }; + + getFolderListByIncludedParent = async (userID: string, parent: string) => { + const rows = db + .prepare( + `SELECT * FROM folders WHERE owner = ? AND ${containsClause()} AND trashed = 0` + ) + .all(userID, containsParam(parent)); + return rows.map(rowToFolder); + }; + + findAllFoldersByParent = async (parentID: string, userID: string) => { + const rows = db + .prepare(`SELECT * FROM folders WHERE owner = ? AND ${containsClause()}`) + .all(userID, containsParam(parentID)); + return rows.map(rowToFolder); + }; + + // UPDATE + + moveFolder = async ( + folderID: string, + userID: string, + parent: string, + parentList: string[] + ) => { + const existing: any = db + .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") + .get(folderID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE folders SET parent = ?, parentList = ?, updatedAt = ? WHERE id = ? AND owner = ?" + ).run( + parent, + encodeParentList(parentList), + new Date().toISOString(), + folderID, + userID + ); + + return this.getFolderInfo(folderID, userID); + }; + + renameFolder = async (folderID: string, userID: string, title: string) => { + const existing: any = db + .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") + .get(folderID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE folders SET name = ?, updatedAt = ? WHERE id = ? AND owner = ?" + ).run(title, new Date().toISOString(), folderID, userID); + + return this.getFolderInfo(folderID, userID); + }; + + trashFoldersByParent = async (parentList: string[], userID: string) => { + const clauses = parentList.map(() => containsClause()).join(" AND "); + const params = parentList.map((id) => containsParam(id)); + + const result = db + .prepare( + `UPDATE folders SET trashed = 1, updatedAt = ? WHERE owner = ? AND ${clauses}` + ) + .run(new Date().toISOString(), userID, ...params); + return result; + }; + + restoreFolder = async (folderID: string, userID: string) => { + const existing: any = db + .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") + .get(folderID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE folders SET trashed = 0, updatedAt = ? WHERE id = ? AND owner = ?" + ).run(new Date().toISOString(), folderID, userID); + + return this.getFolderInfo(folderID, userID); + }; + + restoreFoldersByParent = async (parentList: string[], userID: string) => { + const clauses = parentList.map(() => containsClause()).join(" AND "); + const params = parentList.map((id) => containsParam(id)); + + const result = db + .prepare( + `UPDATE folders SET trashed = 0, updatedAt = ? WHERE owner = ? AND ${clauses}` + ) + .run(new Date().toISOString(), userID, ...params); + return result; + }; + + trashFolder = async (folderID: string, userID: string) => { + const existing: any = db + .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") + .get(folderID, userID); + + if (!existing) return null; + + db.prepare( + "UPDATE folders SET trashed = 1, updatedAt = ? WHERE id = ? AND owner = ?" + ).run(new Date().toISOString(), folderID, userID); + + return this.getFolderInfo(folderID, userID); + }; + + // CREATE + + createFolder = async (folderData: { + name: string; + parent: string; + parentList: string[]; + owner: string; + }) => { + const id = new ObjectId().toString(); + const now = new Date().toISOString(); + + db.prepare( + `INSERT INTO folders (id, name, parent, owner, parentList, personalFolder, trashed, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + id, + folderData.name, + folderData.parent, + folderData.owner, + encodeParentList(folderData.parentList), + 0, + 0, + now, + now + ); + + const row = db.prepare("SELECT * FROM folders WHERE id = ?").get(id); + return rowToFolder(row); + }; + + // DELETE + + deleteFolder = async (folderID: string, userID: string) => { + const result = db + .prepare("DELETE FROM folders WHERE id = ? AND owner = ?") + .run(folderID, userID); + return result; + }; + + deleteFoldersByParentList = async ( + parentList: string[], + userID: string + ) => { + const clauses = parentList.map(() => containsClause()).join(" AND "); + const params = parentList.map((id) => containsParam(id.toString())); + + const result = db + .prepare(`DELETE FROM folders WHERE owner = ? AND ${clauses}`) + .run(userID, ...params); + return result; + }; + + deleteFoldersByOwner = async (userID: string) => { + const result = db + .prepare("DELETE FROM folders WHERE owner = ?") + .run(userID); + return result; + }; +} + +export default FolderDB; diff --git a/backend/db/sqliteDB/thumbnailDB.ts b/backend/db/sqliteDB/thumbnailDB.ts new file mode 100644 index 0000000..57cb0aa --- /dev/null +++ b/backend/db/sqliteDB/thumbnailDB.ts @@ -0,0 +1,76 @@ +import db from "../connections/sqlite"; +import { ObjectId } from "mongodb"; +import { IThumbnailDB } from "../dbTypes"; + +const rowToThumbnail = (row: any) => { + if (!row) return null; + + return { + _id: row.id, + name: row.name, + owner: row.owner, + data: row.data || undefined, + path: row.path || undefined, + IV: row.iv || undefined, + s3ID: row.s3ID || undefined, + personalFile: row.personalFile ? true : undefined, + createdAt: new Date(row.createdAt), + updatedAt: new Date(row.updatedAt), + }; +}; + +class ThumbnailDB implements IThumbnailDB { + constructor() {} + + // READ + + getThumbnailInfo = async (userID: string, thumbnailID: string) => { + const row = db + .prepare("SELECT * FROM thumbnails WHERE id = ? AND owner = ?") + .get(thumbnailID, userID); + return rowToThumbnail(row); + }; + + // DELETE + + removeThumbnail = async (userID: string, thumbnailID: any) => { + const result = db + .prepare("DELETE FROM thumbnails WHERE id = ? AND owner = ?") + .run(thumbnailID.toString(), userID); + return result; + }; + + // CREATE + + createThumbnail = async (data: { + name: string; + owner: string; + IV: Buffer; + path?: string; + s3ID?: string; + }) => { + const id = new ObjectId().toString(); + const now = new Date().toISOString(); + + db.prepare( + `INSERT INTO thumbnails (id, name, owner, data, path, iv, s3ID, personalFile, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + id, + data.name, + data.owner, + null, + data.path || null, + data.IV, + data.s3ID || null, + 0, + now, + now + ); + + const row = db.prepare("SELECT * FROM thumbnails WHERE id = ?").get(id); + return rowToThumbnail(row); + }; +} + +export default ThumbnailDB; diff --git a/backend/db/sqliteDB/userDB.ts b/backend/db/sqliteDB/userDB.ts new file mode 100644 index 0000000..3ec9fae --- /dev/null +++ b/backend/db/sqliteDB/userDB.ts @@ -0,0 +1,267 @@ +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; diff --git a/backend/enviroment/env.ts b/backend/enviroment/env.ts index 65f80da..c403988 100644 --- a/backend/enviroment/env.ts +++ b/backend/enviroment/env.ts @@ -8,6 +8,8 @@ export default { root: process.env.ROOT, url: process.env.URL, mongoURL: process.env.MONGODB_URL, + dbEngine: process.env.DB_ENGINE || "mongo", + sqliteDBPath: process.env.SQLITE_DB_PATH || "./data/mydrive.db", dbType: process.env.DB_TYPE, fsDirectory: process.env.FS_DIRECTORY, s3ID: process.env.S3_ID, @@ -46,6 +48,8 @@ module.exports = { root: process.env.ROOT, url: process.env.URL, mongoURL: process.env.MONGODB_URL, + dbEngine: process.env.DB_ENGINE || "mongo", + sqliteDBPath: process.env.SQLITE_DB_PATH || "./data/mydrive.db", dbType: process.env.DB_TYPE, fsDirectory: process.env.FS_DIRECTORY, s3ID: process.env.S3_ID, diff --git a/backend/middleware/authFullUser.ts b/backend/middleware/authFullUser.ts index 42824b1..f3bec88 100644 --- a/backend/middleware/authFullUser.ts +++ b/backend/middleware/authFullUser.ts @@ -1,7 +1,10 @@ import jwt from "jsonwebtoken"; -import User, { UserInterface } from "../models/user-model"; +import { UserInterface } from "../models/user-model"; import env from "../enviroment/env"; import { Request, Response, NextFunction } from "express"; +import { getUserDB } from "../db/dbFactory"; + +const userDB = getUserDB(); interface RequestType extends Request { user?: UserInterface; @@ -39,7 +42,7 @@ const authFullUser = async ( if (!user) throw new Error("No User"); - const fullUser = await User.findById(user._id); + const fullUser = await userDB.getUserInfo(user._id); if (!fullUser) throw new Error("No User"); diff --git a/backend/middleware/authRefresh.ts b/backend/middleware/authRefresh.ts index 8e2e74a..e6945f7 100644 --- a/backend/middleware/authRefresh.ts +++ b/backend/middleware/authRefresh.ts @@ -1,9 +1,10 @@ import jwt from "jsonwebtoken"; -import User, { UserInterface } from "../models/user-model"; +import { UserInterface } from "../models/user-model"; import env from "../enviroment/env"; import { Request, Response, NextFunction } from "express"; -import { ObjectId } from "mongodb"; -import mongoose from "mongoose"; +import { getUserDB } from "../db/dbFactory"; + +const userDB = getUserDB(); interface RequestType extends Request { user?: UserInterface; @@ -18,22 +19,14 @@ type jwtType = { }; const removeOldTokens = async ( - userID: mongoose.Types.ObjectId, + userID: string, uuid: string | undefined, oldTime: number ) => { try { const minusTime = oldTime - 1000 * 60 * 60; - //const minusTime = oldTime - (1000); - uuid = uuid ? uuid : "unknown"; - - if (uuid === "unknown") return; - - await User.updateOne( - { _id: userID }, - { $pull: { tokens: { uuid, time: { $lt: minusTime } } } } - ); + await userDB.removeOldTokens(userID, uuid, minusTime, false); } catch (e) { console.log("cannot remove old tokens", e); } @@ -54,7 +47,7 @@ const authRefresh = async ( const time = decoded.time; - const user = await User.findById(new ObjectId(decoded._id)); + const user = await userDB.getUserInfo(decoded._id); if (!user) throw new Error("No User"); @@ -72,7 +65,7 @@ const authRefresh = async ( if (currentEncryptedToken === encryptedToken) { tokenFound = true; - removeOldTokens(user._id, currentUUID, time); + removeOldTokens(user._id.toString(), currentUUID, time); break; } } diff --git a/backend/middleware/authStreamVideo.ts b/backend/middleware/authStreamVideo.ts index 3845746..226fc3a 100644 --- a/backend/middleware/authStreamVideo.ts +++ b/backend/middleware/authStreamVideo.ts @@ -1,9 +1,10 @@ import jwt from "jsonwebtoken"; -import User, { UserInterface } from "../models/user-model"; +import { UserInterface } from "../models/user-model"; import env from "../enviroment/env"; import { Request, Response, NextFunction } from "express"; -import { ObjectId } from "mongodb"; -import mongoose from "mongoose"; +import { getUserDB } from "../db/dbFactory"; + +const userDB = getUserDB(); interface RequestType extends Request { user?: UserInterface; @@ -19,21 +20,14 @@ type jwtType = { }; const removeOldTokens = async ( - userID: mongoose.Types.ObjectId, + userID: string, uuid: string | undefined, oldTime: number ) => { try { const minusTime = oldTime - 60 * 1000 * 60 * 24; - uuid = uuid ? uuid : "unknown"; - - if (uuid === "unknown") return; - - await User.updateOne( - { _id: userID }, - { $pull: { tempTokens: { uuid, time: { $lt: minusTime } } } } - ); + await userDB.removeOldTokens(userID, uuid, minusTime, true); } catch (e) { console.log("cannot remove old tokens", e); } @@ -57,7 +51,7 @@ const authStreamVideo = async ( const time = decoded.time; - const user = await User.findById(new ObjectId(decoded._id)); + const user = await userDB.getUserInfo(decoded._id); if (!user) throw new Error("No User"); @@ -75,7 +69,7 @@ const authStreamVideo = async ( if (currentEncryptedToken === encryptedToken) { tokenFound = true; - removeOldTokens(user._id, currentUUID, time); + removeOldTokens(user._id.toString(), currentUUID, time); break; } } diff --git a/backend/models/user-model.ts b/backend/models/user-model.ts index 2881e59..8ad9250 100644 --- a/backend/models/user-model.ts +++ b/backend/models/user-model.ts @@ -1,10 +1,22 @@ import mongoose, { Document } from "mongoose"; import validator from "validator"; import crypto from "crypto"; -import bcrypt from "bcryptjs"; 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( { @@ -119,16 +131,11 @@ export interface UserInterface extends Document { generatePasswordResetToken: () => Promise; } -const maxAgeAccess = 60 * 1000 * 20 + 1000 * 60; -const maxAgeRefresh = 60 * 1000 * 60 * 24 * 30 + 1000 * 60; - -const maxAgeAccessStreamVideo = 60 * 1000 * 60 * 24; - userSchema.pre("save", async function (this: any, next: any) { const user = this; if (user.isModified("password")) { - user.password = await bcrypt.hash(user.password, 8); + user.password = await hashPassword(user.password); } next(); @@ -141,7 +148,7 @@ userSchema.statics.findByCreds = async (email: string, password: string) => { throw new NotAuthorizedError("User not found"); } - const isMatch = await bcrypt.compare(password, user.password); + const isMatch = await comparePassword(password, user.password); if (!isMatch) { throw new NotAuthorizedError("Incorrect password"); @@ -173,26 +180,9 @@ userSchema.methods.toJSON = function () { userSchema.methods.generateAuthTokenStreamVideo = async function ( uuid: string | undefined ) { - const iv = crypto.randomBytes(16); - const user = this; - const date = new Date(); - const time = date.getTime(); - - let accessTokenStreamVideo = jwt.sign( - { _id: user._id.toString(), iv, time }, - env.passwordAccess!, - { expiresIn: maxAgeAccessStreamVideo.toString() } - ); - - const encryptionKey = user.getEncryptionKey(); - - const encryptedToken = user.encryptToken( - accessTokenStreamVideo, - encryptionKey, - iv - ); + const { token, encryptedToken, time } = signStreamVideoToken(user); uuid = uuid ? uuid : "unknown"; @@ -201,43 +191,26 @@ userSchema.methods.generateAuthTokenStreamVideo = async function ( { $push: { tempTokens: { token: encryptedToken, uuid, time } } } ); - return accessTokenStreamVideo; + return token; }; userSchema.methods.generateAuthToken = async function ( uuid: string | undefined ) { - const iv = crypto.randomBytes(16); - const user = this; - const date = new Date(); - const time = date.getTime(); - - const userObj = { - _id: user._id, - emailVerified: user.emailVerified, - email: user.email, - }; - - let accessToken = jwt.sign({ user: userObj, iv }, env.passwordAccess!, { - expiresIn: maxAgeAccess.toString(), - }); - let refreshToken = jwt.sign( - { _id: user._id.toString(), iv, time }, - env.passwordRefresh!, - { expiresIn: maxAgeRefresh.toString() } - ); - - const encryptionKey = user.getEncryptionKey(); - - const encryptedToken = user.encryptToken(refreshToken, encryptionKey, iv); + const { accessToken, refreshToken, encryptedRefreshToken, time } = + signAuthTokens(user); uuid = uuid ? uuid : "unknown"; await User.updateOne( { _id: user._id }, - { $push: { tokens: { token: encryptedToken, uuid, time } } } + { + $push: { + tokens: { token: encryptedRefreshToken, uuid, time }, + }, + } ); return { accessToken, refreshToken }; @@ -248,13 +221,7 @@ userSchema.methods.encryptToken = function ( key: string, iv: any ) { - iv = Buffer.from(iv, "hex"); - - const TOKEN_CIPHER_KEY = crypto.createHash("sha256").update(key).digest(); - const cipher = crypto.createCipheriv("aes-256-cbc", TOKEN_CIPHER_KEY, iv); - const encryptedText = cipher.update(token); - - return Buffer.concat([encryptedText, cipher.final()]).toString("hex"); + return encryptTokenUtil(token, key, iv); }; userSchema.methods.decryptToken = function ( @@ -262,136 +229,45 @@ userSchema.methods.decryptToken = function ( key: string, iv: any ) { - encryptedToken = Buffer.from(encryptedToken, "hex"); - iv = Buffer.from(iv, "hex"); - - const TOKEN_CIPHER_KEY = crypto.createHash("sha256").update(key).digest(); - const decipher = crypto.createDecipheriv("aes-256-cbc", TOKEN_CIPHER_KEY, iv); - - const tokenDecrypted = decipher.update(encryptedToken); - - return Buffer.concat([tokenDecrypted, decipher.final()]).toString(); + return decryptTokenUtil(encryptedToken, key, iv); }; userSchema.methods.generateEncryptionKeys = async function () { const user = this; - const userPassword = user.password; - const masterPassword = env.key!; - const randomKey = crypto.randomBytes(32); - const iv = crypto.randomBytes(16); - const USER_CIPHER_KEY = crypto - .createHash("sha256") - .update(userPassword) - .digest(); - const cipher = crypto.createCipheriv("aes-256-cbc", USER_CIPHER_KEY, iv); - let encryptedText = cipher.update(randomKey); - encryptedText = Buffer.concat([encryptedText, cipher.final()]); - - const MASTER_CIPHER_KEY = crypto - .createHash("sha256") - .update(masterPassword) - .digest(); - const masterCipher = crypto.createCipheriv( - "aes-256-cbc", - MASTER_CIPHER_KEY, - iv + const { privateKey, publicKey } = generateEncryptionKeyMaterial( + user.password, + randomKey ); - const masterEncryptedText = masterCipher.update(encryptedText); - user.privateKey = Buffer.concat([ - masterEncryptedText, - masterCipher.final(), - ]).toString("hex"); - user.publicKey = iv.toString("hex"); + user.privateKey = privateKey; + user.publicKey = publicKey; await user.save(); }; userSchema.methods.getEncryptionKey = function () { - try { - const user = this; - const userPassword = user.password; - const masterEncryptedText = user.privateKey; - const masterPassword = env.key!; - const iv = Buffer.from(user.publicKey, "hex"); - - const USER_CIPHER_KEY = crypto - .createHash("sha256") - .update(userPassword) - .digest(); - const MASTER_CIPHER_KEY = crypto - .createHash("sha256") - .update(masterPassword) - .digest(); - - const unhexMasterText = Buffer.from(masterEncryptedText, "hex"); - const masterDecipher = crypto.createDecipheriv( - "aes-256-cbc", - MASTER_CIPHER_KEY, - iv - ); - let masterDecrypted = masterDecipher.update(unhexMasterText); - masterDecrypted = Buffer.concat([masterDecrypted, masterDecipher.final()]); - - let decipher = crypto.createDecipheriv("aes-256-cbc", USER_CIPHER_KEY, iv); - let decrypted = decipher.update(masterDecrypted); - decrypted = Buffer.concat([decrypted, decipher.final()]); - - return decrypted; - } catch (e) { - console.log("Get Encryption Key Error", e); - return undefined; - } + return getEncryptionKeyUtil(this); }; userSchema.methods.changeEncryptionKey = async function (randomKey: Buffer) { const user = this; - const userPassword = user.password; - const masterPassword = env.key!; - const iv = crypto.randomBytes(16); - const USER_CIPHER_KEY = crypto - .createHash("sha256") - .update(userPassword) - .digest(); - const cipher = crypto.createCipheriv("aes-256-cbc", USER_CIPHER_KEY, iv); - let encryptedText = cipher.update(randomKey); - encryptedText = Buffer.concat([encryptedText, cipher.final()]); - - const MASTER_CIPHER_KEY = crypto - .createHash("sha256") - .update(masterPassword) - .digest(); - const masterCipher = crypto.createCipheriv( - "aes-256-cbc", - MASTER_CIPHER_KEY, - iv + const { privateKey, publicKey } = generateEncryptionKeyMaterial( + user.password, + randomKey ); - const masterEncryptedText = masterCipher.update(encryptedText); - user.privateKey = Buffer.concat([ - masterEncryptedText, - masterCipher.final(), - ]).toString("hex"); - user.publicKey = iv.toString("hex"); + user.privateKey = privateKey; + user.publicKey = publicKey; await user.save(); }; userSchema.methods.generateTempAuthToken = async function () { - const iv = crypto.randomBytes(16); - const user = this as UserInterface; - const token = jwt.sign( - { _id: user._id.toString(), iv }, - env.passwordAccess!, - { expiresIn: "3000ms" } - ); - - const encryptionKey = user.getEncryptionKey(); - const encryptedToken = user.encryptToken(token, encryptionKey, iv); + const { token, encryptedToken } = signTempAuthToken(user); user.tempTokens = user.tempTokens.concat({ token: encryptedToken }); @@ -400,17 +276,8 @@ userSchema.methods.generateTempAuthToken = async function () { }; userSchema.methods.generateEmailVerifyToken = async function () { - const iv = crypto.randomBytes(16); - const user = this as UserInterface; - const token = jwt.sign( - { _id: user._id.toString(), iv }, - env.passwordAccess!, - { expiresIn: "1d" } - ); - - const encryptionKey = user.getEncryptionKey(); - const encryptedToken = user.encryptToken(token, encryptionKey, iv); + const { token, encryptedToken } = signEmailVerifyToken(user); user.emailToken = encryptedToken; @@ -419,17 +286,8 @@ userSchema.methods.generateEmailVerifyToken = async function () { }; userSchema.methods.generatePasswordResetToken = async function () { - const iv = crypto.randomBytes(16); - const user = this as UserInterface; - const token = jwt.sign( - { _id: user._id.toString(), iv }, - env.passwordAccess!, - { expiresIn: "1h" } - ); - - const encryptionKey = user.getEncryptionKey(); - const encryptedToken = user.encryptToken(token, encryptionKey, iv); + const { token, encryptedToken } = signPasswordResetToken(user); user.passwordResetToken = encryptedToken; diff --git a/backend/server/server.ts b/backend/server/server.ts index 49d31f5..d92213c 100755 --- a/backend/server/server.ts +++ b/backend/server/server.ts @@ -41,7 +41,11 @@ if (process.env.SSL === "true") { server = http.createServer(app); -require("../db/connections/mongoose"); +if (env.dbEngine === "sqlite") { + require("../db/connections/sqlite"); +} else { + require("../db/connections/mongoose"); +} app.use(cors()); app.use(cookieParser(env.passwordCookie)); diff --git a/backend/services/chunk-service/chunk-service.ts b/backend/services/chunk-service/chunk-service.ts index 257e347..3e749fe 100644 --- a/backend/services/chunk-service/chunk-service.ts +++ b/backend/services/chunk-service/chunk-service.ts @@ -7,8 +7,12 @@ import uploadFileToStorage from "./utils/getBusboyData"; import videoChecker from "../../utils/videoChecker"; import uuid from "uuid"; import { FileInterface, FileMetadateInterface } from "../../models/file-model"; -import FileDB from "../../db/mongoDB/fileDB"; -import FolderDB from "../../db/mongoDB/folderDB"; +import { + getFileDB, + getFolderDB, + getThumbnailDB, + getUserDB, +} from "../../db/dbFactory"; import env from "../../enviroment/env"; import fixStartChunkLength from "./utils/fixStartChunkLength"; import { FolderInterface } from "../../models/folder-model"; @@ -17,8 +21,6 @@ import { S3Actions } from "./actions/S3-actions"; import { FilesystemActions } from "./actions/file-system-actions"; import { createGenericParams } from "./utils/storageHelper"; import { Readable } from "stream"; -import ThumbnailDB from "../../db/mongoDB/thumbnailDB"; -import UserDB from "../../db/mongoDB/userDB"; import fixEndChunkLength from "./utils/fixEndChunkLength"; import archiver from "archiver"; import async from "async"; @@ -28,10 +30,10 @@ import getThumbnailData from "./utils/getThumbnailData"; import getFileData from "./utils/getFileData"; import getPublicFileData from "./utils/getPublicFileData"; -const fileDB = new FileDB(); -const folderDB = new FolderDB(); -const thumbnailDB = new ThumbnailDB(); -const userDB = new UserDB(); +const fileDB = getFileDB(); +const folderDB = getFolderDB(); +const thumbnailDB = getThumbnailDB(); +const userDB = getUserDB(); const storageActions = getStorageActions(); diff --git a/backend/services/chunk-service/utils/createImageThumbnail.ts b/backend/services/chunk-service/utils/createImageThumbnail.ts index 3050649..e4b1c34 100644 --- a/backend/services/chunk-service/utils/createImageThumbnail.ts +++ b/backend/services/chunk-service/utils/createImageThumbnail.ts @@ -1,5 +1,4 @@ import crypto from "crypto"; -import Thumbnail from "../../../models/thumbnail-model"; import sharp from "sharp"; import { FileInterface } from "../../../models/file-model"; import { UserInterface } from "../../../models/user-model"; @@ -9,11 +8,12 @@ import { createGenericParams } from "./storageHelper"; import { S3Actions } from "../actions/S3-actions"; import { FilesystemActions } from "../actions/file-system-actions"; import { EventEmitter } from "stream"; -import FileDB from "../../../db/mongoDB/fileDB"; +import { getFileDB, getThumbnailDB } from "../../../db/dbFactory"; import { getStorageActions } from "../actions/helper-actions"; import { getFSStoragePath } from "../../../utils/getFSStoragePath"; -const fileDB = new FileDB(); +const fileDB = getFileDB(); +const thumbnailDB = getThumbnailDB(); const storageActions = getStorageActions(); @@ -55,16 +55,14 @@ const processData = ( const imageResize = sharp().resize(300); const handleFinish = async () => { - const thumbnailModel = new Thumbnail({ + const thumbnailModel = await thumbnailDB.createThumbnail({ name: filename, - owner: user._id, + owner: user._id.toString(), IV: thumbnailIV, path: getFSStoragePath() + thumbnailFilename, s3ID: thumbnailFilename, }); - await thumbnailModel.save(); - const updatedFile = await fileDB.setThumbnail( file._id!.toString(), thumbnailModel._id.toString() diff --git a/backend/services/chunk-service/utils/createVideoThumbnail.ts b/backend/services/chunk-service/utils/createVideoThumbnail.ts index 796655b..b5ea624 100644 --- a/backend/services/chunk-service/utils/createVideoThumbnail.ts +++ b/backend/services/chunk-service/utils/createVideoThumbnail.ts @@ -1,14 +1,10 @@ -import mongoose from "../../../db/connections/mongoose"; import crypto from "crypto"; -import Thumbnail from "../../../models/thumbnail-model"; import sharp from "sharp"; import { FileInterface } from "../../../models/file-model"; import { UserInterface } from "../../../models/user-model"; import fs from "fs"; import uuid from "uuid"; import env from "../../../enviroment/env"; -import { ObjectId } from "mongodb"; -import File from "../../../models/file-model"; import ffmpeg from "fluent-ffmpeg"; import tempCreateVideoThumbnail from "./tempCreateVideoThumbnail"; import { S3Actions } from "../actions/S3-actions"; @@ -16,8 +12,11 @@ import { FilesystemActions } from "../actions/file-system-actions"; import { createGenericParams } from "./storageHelper"; import { getStorageActions } from "../actions/helper-actions"; import { getFSStoragePath } from "../../../utils/getFSStoragePath"; +import { getFileDB, getThumbnailDB } from "../../../db/dbFactory"; const storageActions = getStorageActions(); +const fileDB = getFileDB(); +const thumbnailDB = getThumbnailDB(); const attemptToRemoveChunks = async ( file: FileInterface, @@ -95,40 +94,27 @@ const createVideoThumbnail = ( try { if (error) return; - const thumbnailModel = new Thumbnail({ + const thumbnailModel = await thumbnailDB.createThumbnail({ name: filename, - owner: user._id, + owner: user._id.toString(), IV: thumbnailIV, path: getFSStoragePath() + thumbnailFilename, s3ID: thumbnailFilename, }); - await thumbnailModel.save(); if (!file._id) { return reject(); } - const updateFileResponse = await File.updateOne( - { _id: new ObjectId(file._id), "metadata.owner": user._id }, - { - $set: { - "metadata.hasThumbnail": true, - "metadata.thumbnailID": thumbnailModel._id, - "metadata.isVideo": true, - }, - } - ); - if (updateFileResponse.modifiedCount === 0) { - return reject(); - } - const updatedFile = await File.findById({ - _id: new ObjectId(file._id), - "metadata.owner": user._id, - }); + const updatedFile = await fileDB.setVideoThumbnail( + file._id.toString(), + user._id.toString(), + thumbnailModel._id.toString() + ); if (!updatedFile) return reject(); - resolve(updatedFile?.toObject()); + resolve(updatedFile); } catch (e) { console.log("thumbnail error", e); resolve(file); diff --git a/backend/services/chunk-service/utils/getBusboyData.ts b/backend/services/chunk-service/utils/getBusboyData.ts index 44c9f4e..d5beb0f 100644 --- a/backend/services/chunk-service/utils/getBusboyData.ts +++ b/backend/services/chunk-service/utils/getBusboyData.ts @@ -1,9 +1,6 @@ import { EventEmitter, Stream } from "stream"; import { UserInterface } from "../../../models/user-model"; -import File, { - FileInterface, - FileMetadateInterface, -} from "../../../models/file-model"; +import { FileInterface, FileMetadateInterface } from "../../../models/file-model"; import uuid from "uuid"; import crypto from "crypto"; import ForbiddenError from "../../../utils/ForbiddenError"; @@ -16,12 +13,10 @@ import createVideoThumbnail from "./createVideoThumbnail"; import createThumbnail from "./createImageThumbnail"; import { RequestTypeFullUser } from "../../../controllers/file-controller"; import { getFSStoragePath } from "../../../utils/getFSStoragePath"; - -// TODO: We should stop using moongoose directly here, -// Also in our fileDB make sure we are actually using File instead -// Of just modifying data directly so we get validation +import { getFileDB } from "../../../db/dbFactory"; const storageActions = getStorageActions(); +const fileDB = getFileDB(); type FileInfo = { file: FileInterface; @@ -56,7 +51,7 @@ const processData = ( const videoCheck = videoChecker(filename); - const currentFile = new File({ + const currentFile = await fileDB.createFile({ filename, uploadDate: date.toISOString(), length, @@ -66,8 +61,6 @@ const processData = ( }, }); - await currentFile.save(); - const imageCheck = imageChecker(currentFile.filename); if (videoCheck && env.videoThumbnailsEnabled) { diff --git a/backend/services/chunk-service/utils/getFileData.ts b/backend/services/chunk-service/utils/getFileData.ts index 85f0f81..88cd154 100644 --- a/backend/services/chunk-service/utils/getFileData.ts +++ b/backend/services/chunk-service/utils/getFileData.ts @@ -6,12 +6,12 @@ import NotFoundError from "../../../utils/NotFoundError"; import crypto from "crypto"; import { createGenericParams } from "./storageHelper"; import { getStorageActions } from "../actions/helper-actions"; -import FileDB from "../../../db/mongoDB/fileDB"; +import { getFileDB } from "../../../db/dbFactory"; import { FileInterface } from "../../../models/file-model"; import NotAuthorizedError from "../../../utils/NotAuthorizedError"; import sanitizeFilename from "../../../utils/sanitizeFilename"; -const fileDB = new FileDB(); +const fileDB = getFileDB(); const storageActions = getStorageActions(); diff --git a/backend/services/chunk-service/utils/getFolderUploadBusboyData.ts b/backend/services/chunk-service/utils/getFolderUploadBusboyData.ts index 82b4c66..837faf4 100644 --- a/backend/services/chunk-service/utils/getFolderUploadBusboyData.ts +++ b/backend/services/chunk-service/utils/getFolderUploadBusboyData.ts @@ -1,10 +1,7 @@ import { Stream } from "stream"; import uuid from "uuid"; import { UserInterface } from "../../../models/user-model"; -import File, { - FileInterface, - FileMetadateInterface, -} from "../../../models/file-model"; +import { FileInterface, FileMetadateInterface } from "../../../models/file-model"; import env from "../../../enviroment/env"; import { S3Actions } from "../actions/S3-actions"; import { FilesystemActions } from "../actions/file-system-actions"; @@ -19,6 +16,7 @@ import { EventEmitter } from "events"; import { getStorageActions } from "../actions/helper-actions"; import { RequestTypeFullUser } from "../../../controllers/file-controller"; import { getFSStoragePath } from "../../../utils/getFSStoragePath"; +import { getFileDB } from "../../../db/dbFactory"; type FileDataType = { name: string; @@ -31,6 +29,7 @@ type FileDataType = { }; const storageActions = getStorageActions(); +const fileDB = getFileDB(); type dataType = Record; @@ -69,15 +68,13 @@ const processData = ( length = metadata.size; } - const currentFile = new File({ + const currentFile = await fileDB.createFile({ filename, uploadDate: date.toISOString(), length, metadata, }); - await currentFile.save(); - const imageCheck = imageChecker(currentFile.filename); const videoCheck = videoChecker(currentFile.filename); diff --git a/backend/services/chunk-service/utils/getPublicFileData.ts b/backend/services/chunk-service/utils/getPublicFileData.ts index 6e36462..2d09778 100644 --- a/backend/services/chunk-service/utils/getPublicFileData.ts +++ b/backend/services/chunk-service/utils/getPublicFileData.ts @@ -6,13 +6,12 @@ import NotFoundError from "../../../utils/NotFoundError"; import crypto from "crypto"; import { createGenericParams } from "./storageHelper"; import { getStorageActions } from "../actions/helper-actions"; -import FileDB from "../../../db/mongoDB/fileDB"; +import { getFileDB, getUserDB } from "../../../db/dbFactory"; import NotAuthorizedError from "../../../utils/NotAuthorizedError"; -import UserDB from "../../../db/mongoDB/userDB"; import sanitizeFilename from "../../../utils/sanitizeFilename"; -const fileDB = new FileDB(); -const userDB = new UserDB(); +const fileDB = getFileDB(); +const userDB = getUserDB(); const storageActions = getStorageActions(); diff --git a/backend/services/chunk-service/utils/getThumbnailData.ts b/backend/services/chunk-service/utils/getThumbnailData.ts index d288d0b..bb80023 100644 --- a/backend/services/chunk-service/utils/getThumbnailData.ts +++ b/backend/services/chunk-service/utils/getThumbnailData.ts @@ -7,9 +7,9 @@ import crypto from "crypto"; import { createGenericParams } from "./storageHelper"; import { getStorageActions } from "../actions/helper-actions"; -import ThumbnailDB from "../../../db/mongoDB/thumbnailDB"; +import { getThumbnailDB } from "../../../db/dbFactory"; -const thumbnailDB = new ThumbnailDB(); +const thumbnailDB = getThumbnailDB(); const storageActions = getStorageActions(); diff --git a/backend/services/chunk-service/utils/tempCreateVideoThumbnail.ts b/backend/services/chunk-service/utils/tempCreateVideoThumbnail.ts index 6459c49..ccf4764 100644 --- a/backend/services/chunk-service/utils/tempCreateVideoThumbnail.ts +++ b/backend/services/chunk-service/utils/tempCreateVideoThumbnail.ts @@ -1,22 +1,21 @@ -import mongoose from "../../../db/connections/mongoose"; import crypto from "crypto"; -import Thumbnail from "../../../models/thumbnail-model"; import sharp from "sharp"; import { FileInterface } from "../../../models/file-model"; import { UserInterface } from "../../../models/user-model"; import fs from "fs"; import uuid from "uuid"; import env from "../../../enviroment/env"; -import { ObjectId } from "mongodb"; -import File from "../../../models/file-model"; import ffmpeg from "fluent-ffmpeg"; import { S3Actions } from "../actions/S3-actions"; import { FilesystemActions } from "../actions/file-system-actions"; import { createGenericParams } from "./storageHelper"; import { getStorageActions } from "../actions/helper-actions"; import { getFSStoragePath } from "../../../utils/getFSStoragePath"; +import { getFileDB, getThumbnailDB } from "../../../db/dbFactory"; const storageActions = getStorageActions(); +const fileDB = getFileDB(); +const thumbnailDB = getThumbnailDB(); const tempCreateVideoThumbnail = ( file: FileInterface, @@ -70,28 +69,22 @@ const tempCreateVideoThumbnail = ( } const handleFinish = async () => { - const thumbnailModel = new Thumbnail({ + const thumbnailModel = await thumbnailDB.createThumbnail({ name: filename, - owner: user._id, + owner: user._id.toString(), IV: thumbnailIV, path: getFSStoragePath() + thumbnailFilename, s3ID: thumbnailFilename, }); - await thumbnailModel.save(); if (!file._id) { return reject(); } - const updatedFile = await File.findOneAndUpdate( - { _id: new ObjectId(file._id), "metadata.owner": user._id }, - { - $set: { - "metadata.hasThumbnail": true, - "metadata.thumbnailID": thumbnailModel._id, - "metadata.isVideo": true, - }, - }, - { new: true } + + const updatedFile = await fileDB.setVideoThumbnail( + file._id.toString(), + user._id.toString(), + thumbnailModel._id.toString() ); if (!updatedFile) return reject(); diff --git a/backend/services/file-service/file-service.ts b/backend/services/file-service/file-service.ts index 348d156..7f702de 100644 --- a/backend/services/file-service/file-service.ts +++ b/backend/services/file-service/file-service.ts @@ -2,18 +2,17 @@ import NotAuthorizedError from "../../utils/NotAuthorizedError"; import NotFoundError from "../../utils/NotFoundError"; import env from "../../enviroment/env"; import jwt from "jsonwebtoken"; -import Folder, { FolderInterface } from "../../models/folder-model"; +import { FolderInterface } from "../../models/folder-model"; import sortBySwitch from "../../utils/sortBySwitch"; -import FileDB from "../../db/mongoDB/fileDB"; -import FolderDB from "../../db/mongoDB/folderDB"; +import { getFileDB, getFolderDB } from "../../db/dbFactory"; import { UserInterface } from "../../models/user-model"; import { FileInterface } from "../../models/file-model"; import tempStorage from "../../tempStorage/tempStorage"; import FolderService from "../folder-service/folder-service"; import { FileListQueryType } from "../../types/file-types"; -const fileDB = new FileDB(); -const folderDB = new FolderDB(); +const fileDB = getFileDB(); +const folderDB = getFolderDB(); const folderService = new FolderService(); type userAccessType = { @@ -31,7 +30,7 @@ class MongoFileService { if (!fileID) return; if (currentFile.metadata.linkType === "one") { - await fileDB.removeOneTimePublicLink(fileID); + await fileDB.removeOneTimePublicLink(fileID.toString()); } }; diff --git a/backend/services/folder-service/folder-service.ts b/backend/services/folder-service/folder-service.ts index 99d83f6..8f5c231 100644 --- a/backend/services/folder-service/folder-service.ts +++ b/backend/services/folder-service/folder-service.ts @@ -1,9 +1,7 @@ import InternalServerError from "../../utils/InternalServerError"; import NotFoundError from "../../utils/NotFoundError"; -import FileDB from "../../db/mongoDB/fileDB"; -import FolderDB from "../../db/mongoDB/folderDB"; import { FolderListQueryType } from "../../types/folder-types"; -import UserDB from "../../db/mongoDB/userDB"; +import { getFileDB, getFolderDB, getUserDB } from "../../db/dbFactory"; type userAccessType = { _id: string; @@ -12,9 +10,9 @@ type userAccessType = { s3Enabled: boolean; }; -const fileDB = new FileDB(); -const folderDB = new FolderDB(); -const userDB = new UserDB(); +const fileDB = getFileDB(); +const folderDB = getFolderDB(); +const userDB = getUserDB(); class FolderService { createFolder = async (userID: string, name: string, parent: string) => { diff --git a/backend/services/user-service/user-service.ts b/backend/services/user-service/user-service.ts index 114ada8..3db88b0 100644 --- a/backend/services/user-service/user-service.ts +++ b/backend/services/user-service/user-service.ts @@ -1,9 +1,7 @@ -import User, { UserInterface } from "../../models/user-model"; -import bcrypt from "bcryptjs"; +import { UserInterface } from "../../models/user-model"; import NotFoundError from "../../utils/NotFoundError"; import InternalServerError from "../../utils/InternalServerError"; import sendEmailVerification from "../../utils/sendVerificationEmail"; -import File from "../../models/file-model"; import env from "../../enviroment/env"; import jwt from "jsonwebtoken"; import sendVerificationEmail from "../../utils/sendVerificationEmail"; @@ -11,6 +9,8 @@ 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; @@ -22,11 +22,7 @@ type jwtType = { _id: string; }; -const uknownUserType = User as unknown; - -const UserStaticType = uknownUserType as { - findByCreds: (email: string, password: string) => Promise; -}; +const userDB = getUserDB(); class UserService { constructor() {} @@ -35,7 +31,7 @@ class UserService { const email = userData.email; const password = userData.password; - const user = await UserStaticType.findByCreds(email, password); + const user = await userDB.findByCreds(email, password); if (!user) throw new NotFoundError("Cannot Find User"); @@ -48,7 +44,7 @@ class UserService { }; logout = async (userID: string, refreshToken: string) => { - const user = await User.findById(userID); + const user = await userDB.getUserInfo(userID); if (!user) throw new NotFoundError("Could Not Find User"); @@ -76,7 +72,7 @@ class UserService { }; logoutAll = async (userID: string) => { - const user = await User.findById(userID); + const user = await userDB.getUserInfo(userID); if (!user) throw new NotFoundError("Could Not Find User"); @@ -91,17 +87,13 @@ class UserService { throw new ForbiddenError("Account Creation Blocked"); } - const userExistsLookedUp = await User.findOne({ email: userData.email }); + const userExistsLookedUp = await userDB.findByEmail(userData.email); if (userExistsLookedUp) { throw new ConflictError("Email Already Exists"); } - const user = new User({ - email: userData.email, - password: userData.password, - }); - await user.save(); + const user = await userDB.createUser(userData.email, userData.password); if (!user) throw new NotFoundError("User Not Found"); @@ -130,13 +122,13 @@ class UserService { oldRefreshToken: string, uuid: string | undefined ) => { - const user = await User.findById(userID); + const user = await userDB.getUserInfo(userID); if (!user) throw new NotAuthorizedError("User information is incorrect"); const date = new Date(); - const isMatch = await bcrypt.compare(oldPassword, user.password); + const isMatch = await comparePassword(oldPassword, user.password); if (!isMatch) throw new NotAuthorizedError("User information is incorrect"); @@ -157,7 +149,7 @@ class UserService { }; getUserDetailed = async (userID: string) => { - const user = await User.findById(userID); + const user = await userDB.getUserInfo(userID); if (!user) throw new NotFoundError("Cannot find user"); @@ -169,7 +161,7 @@ class UserService { const iv = decoded.iv; - const user = (await User.findOne({ _id: decoded._id })) as UserInterface; + const user = (await userDB.getUserInfo(decoded._id)) as UserInterface; const encrpytionKey = user.getEncryptionKey(); const encryptedToken = user.encryptToken(verifyToken, encrpytionKey, iv); @@ -183,7 +175,7 @@ class UserService { }; resendVerifyEmail = async (userID: string) => { - const user = await User.findById(userID); + const user = await userDB.getUserInfo(userID); if (!user) throw new NotFoundError("Cannot find user"); @@ -207,7 +199,7 @@ class UserService { throw new ForbiddenError("Email Verification Not Enabled"); } - const user = await User.findOne({ email }); + const user = await userDB.findByEmail(email); if (!user) throw new NotFoundError("User Not Found Password Reset Email"); @@ -221,7 +213,7 @@ class UserService { const iv = decoded.iv; - const user = (await User.findOne({ _id: decoded._id })) as UserInterface; + const user = (await userDB.getUserInfo(decoded._id)) as UserInterface; const encrpytionKey = user.getEncryptionKey(); const encryptedToken = user.encryptToken(verifyToken, encrpytionKey, iv); diff --git a/backend/utils/userCrypto.ts b/backend/utils/userCrypto.ts new file mode 100644 index 0000000..7a1d5ef --- /dev/null +++ b/backend/utils/userCrypto.ts @@ -0,0 +1,223 @@ +import crypto from "crypto"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import env from "../enviroment/env"; + +const maxAgeAccess = 60 * 1000 * 20 + 1000 * 60; +const maxAgeRefresh = 60 * 1000 * 60 * 24 * 30 + 1000 * 60; +const maxAgeAccessStreamVideo = 60 * 1000 * 60 * 24; + +export interface CryptoUser { + _id: any; + email: string; + password: string; + privateKey?: string; + publicKey?: string; + emailVerified?: boolean; +} + +export const encryptToken = (token: string, key: any, iv: any): string => { + iv = Buffer.from(iv, "hex"); + + const TOKEN_CIPHER_KEY = crypto.createHash("sha256").update(key).digest(); + const cipher = crypto.createCipheriv("aes-256-cbc", TOKEN_CIPHER_KEY, iv); + const encryptedText = cipher.update(token); + + return Buffer.concat([encryptedText, cipher.final()]).toString("hex"); +}; + +export const decryptToken = ( + encryptedToken: any, + key: string, + iv: any +): string => { + encryptedToken = Buffer.from(encryptedToken, "hex"); + iv = Buffer.from(iv, "hex"); + + const TOKEN_CIPHER_KEY = crypto.createHash("sha256").update(key).digest(); + const decipher = crypto.createDecipheriv("aes-256-cbc", TOKEN_CIPHER_KEY, iv); + const tokenDecrypted = decipher.update(encryptedToken); + + return Buffer.concat([tokenDecrypted, decipher.final()]).toString(); +}; + +export const getEncryptionKey = (user: any): Buffer | undefined => { + try { + const userPassword = user.password; + const masterEncryptedText = user.privateKey as string; + const masterPassword = env.key!; + const iv = Buffer.from(user.publicKey as string, "hex"); + + const USER_CIPHER_KEY = crypto + .createHash("sha256") + .update(userPassword) + .digest(); + const MASTER_CIPHER_KEY = crypto + .createHash("sha256") + .update(masterPassword) + .digest(); + + const unhexMasterText = Buffer.from(masterEncryptedText, "hex"); + const masterDecipher = crypto.createDecipheriv( + "aes-256-cbc", + MASTER_CIPHER_KEY, + iv + ); + let masterDecrypted = masterDecipher.update(unhexMasterText); + masterDecrypted = Buffer.concat([masterDecrypted, masterDecipher.final()]); + + const decipher = crypto.createDecipheriv("aes-256-cbc", USER_CIPHER_KEY, iv); + let decrypted = decipher.update(masterDecrypted); + decrypted = Buffer.concat([decrypted, decipher.final()]); + + return decrypted; + } catch (e) { + console.log("Get Encryption Key Error", e); + return undefined; + } +}; + +export const generateEncryptionKeyMaterial = ( + userPassword: string, + randomKey: Buffer +): { privateKey: string; publicKey: string } => { + const masterPassword = env.key!; + const iv = crypto.randomBytes(16); + + const USER_CIPHER_KEY = crypto + .createHash("sha256") + .update(userPassword) + .digest(); + const cipher = crypto.createCipheriv("aes-256-cbc", USER_CIPHER_KEY, iv); + let encryptedText = cipher.update(randomKey); + encryptedText = Buffer.concat([encryptedText, cipher.final()]); + + const MASTER_CIPHER_KEY = crypto + .createHash("sha256") + .update(masterPassword) + .digest(); + const masterCipher = crypto.createCipheriv( + "aes-256-cbc", + MASTER_CIPHER_KEY, + iv + ); + const masterEncryptedText = masterCipher.update(encryptedText); + + const privateKey = Buffer.concat([ + masterEncryptedText, + masterCipher.final(), + ]).toString("hex"); + const publicKey = iv.toString("hex"); + + return { privateKey, publicKey }; +}; + +export const hashPassword = async (password: string): Promise => { + return bcrypt.hash(password, 8); +}; + +export const comparePassword = async ( + password: string, + hash: string +): Promise => { + return bcrypt.compare(password, hash); +}; + +export const signAuthTokens = ( + user: any +): { + accessToken: string; + refreshToken: string; + encryptedRefreshToken: string; + iv: string; + time: number; +} => { + const iv = crypto.randomBytes(16); + const time = new Date().getTime(); + + const userObj = { + _id: user._id, + emailVerified: user.emailVerified, + email: user.email, + }; + + const accessToken = jwt.sign({ user: userObj, iv }, env.passwordAccess!, { + expiresIn: maxAgeAccess.toString(), + }); + const refreshToken = jwt.sign( + { _id: user._id.toString(), iv, time }, + env.passwordRefresh!, + { expiresIn: maxAgeRefresh.toString() } + ); + + const encryptionKey = getEncryptionKey(user); + const encryptedRefreshToken = encryptToken(refreshToken, encryptionKey, iv); + + return { + accessToken, + refreshToken, + encryptedRefreshToken, + iv: iv.toString("hex"), + time, + }; +}; + +export const signStreamVideoToken = ( + user: any +): { token: string; encryptedToken: string; iv: string; time: number } => { + const iv = crypto.randomBytes(16); + const time = new Date().getTime(); + + const token = jwt.sign( + { _id: user._id.toString(), iv, time }, + env.passwordAccess!, + { expiresIn: maxAgeAccessStreamVideo.toString() } + ); + + const encryptionKey = getEncryptionKey(user); + const encryptedToken = encryptToken(token, encryptionKey, iv); + + return { token, encryptedToken, iv: iv.toString("hex"), time }; +}; + +export const signTempAuthToken = ( + user: any +): { token: string; encryptedToken: string } => { + const iv = crypto.randomBytes(16); + const token = jwt.sign({ _id: user._id.toString(), iv }, env.passwordAccess!, { + expiresIn: "3000ms", + }); + + const encryptionKey = getEncryptionKey(user); + const encryptedToken = encryptToken(token, encryptionKey, iv); + + return { token, encryptedToken }; +}; + +export const signEmailVerifyToken = ( + user: any +): { token: string; encryptedToken: string } => { + const iv = crypto.randomBytes(16); + const token = jwt.sign({ _id: user._id.toString(), iv }, env.passwordAccess!, { + expiresIn: "1d", + }); + + const encryptionKey = getEncryptionKey(user); + const encryptedToken = encryptToken(token, encryptionKey, iv); + + return { token, encryptedToken }; +}; + +export const signPasswordResetToken = ( + user: any +): { token: string; encryptedToken: string } => { + const iv = crypto.randomBytes(16); + const token = jwt.sign({ _id: user._id.toString(), iv }, env.passwordAccess!, { + expiresIn: "1h", + }); + + const encryptionKey = getEncryptionKey(user); + const encryptedToken = encryptToken(token, encryptionKey, iv); + + return { token, encryptedToken }; +}; diff --git a/backend/utils/userUpdateCheck.ts b/backend/utils/userUpdateCheck.ts index a5f6901..2c61d4d 100644 --- a/backend/utils/userUpdateCheck.ts +++ b/backend/utils/userUpdateCheck.ts @@ -1,17 +1,12 @@ -import User, { UserInterface } from "../models/user-model"; -import mongoose from "mongoose"; -import { Request, Response, NextFunction } from "express"; +import { Response } from "express"; import NotFoundError from "./NotFoundError"; import { createLoginCookie } from "../cookies/create-cookies"; +import { getUserDB } from "../db/dbFactory"; -// interface RequestType extends Request { -// user?: userAccessType, -// token?: string, -// encryptedToken?: string, -// } +const userDB = getUserDB(); type userAccessType = { - _id: mongoose.Types.ObjectId; + _id: any; emailVerified: boolean; email: string; botChecked: boolean; @@ -19,10 +14,10 @@ type userAccessType = { const userUpdateCheck = async ( res: Response, - id: mongoose.Types.ObjectId, + id: string, uuid: string | undefined ) => { - const updatedUser = await User.findById(id); + const updatedUser = await userDB.getUserInfo(id); if (!updatedUser) throw new NotFoundError("Cannot find updated user auth"); diff --git a/docker-compose-test.yml b/docker-compose-test.yml index ffe0e6a..73e4f14 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -24,6 +24,10 @@ services: env_file: - .env.test # Copy .env.example to .env.test or .env and fill in the values + # Only needed when DB_ENGINE=mongo (the default) in your env file. + # If DB_ENGINE=sqlite, this service is unused and can be removed - + # metadata is instead stored in an embedded SQLite file under the + # mydrive-data volume (SQLITE_DB_PATH), so no separate database service is required. mongo: image: mongo:8 container_name: mongo diff --git a/docker-compose.yml b/docker-compose.yml index 170ab3c..7d9cbc2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,10 @@ services: env_file: - .env # Copy .env.example to .env and fill in the values + # Only needed when DB_ENGINE=mongo (the default) in your .env file. + # If DB_ENGINE=sqlite, this service is unused and can be removed - + # metadata is instead stored in an embedded SQLite file under the + # mydrive-data volume (SQLITE_DB_PATH), so no separate database service is required. mongo: image: mongo:8 container_name: mongo diff --git a/index.html b/index.html index 5e81dba..a3dcf0a 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,16 @@ + diff --git a/package.json b/package.json index 3489030..e229f9a 100755 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "aws-sdk": "^2.657.0", "axios": "^1.7.2", "bcryptjs": "^3.0.2", + "better-sqlite3": "^12.11.1", "body-parser": "^1.20.2", "bytes": "^3.1.0", "classnames": "^2.5.1", @@ -47,7 +48,7 @@ "history": "^4.10.1", "jsonwebtoken": "^9.0.2", "lodash.debounce": "^4.0.8", - "mongodb": "^6.7.0", + "mongodb": "^6.21.0", "mongoose": "^8.4.1", "nodemailer": "^6.9.14", "normalize.css": "^8.0.1", @@ -82,6 +83,7 @@ "@eslint/js": "^9.6.0", "@types/archiver": "^6.0.3", "@types/async": "^3.2.24", + "@types/better-sqlite3": "^7.6.13", "@types/bytes": "^3.1.4", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/src/components/AddNewDropdown/AddNewDropdown.tsx b/src/components/AddNewDropdown/AddNewDropdown.tsx index 224c802..8c78d00 100644 --- a/src/components/AddNewDropdown/AddNewDropdown.tsx +++ b/src/components/AddNewDropdown/AddNewDropdown.tsx @@ -132,7 +132,7 @@ const AddNewDropdown: React.FC = ({
  • @@ -152,7 +152,7 @@ const AddNewDropdown: React.FC = ({ {supportsWebkitDirectory && (
  • diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx index 08ffb71..b9a8444 100644 --- a/src/components/ContextMenu/ContextMenu.tsx +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -158,7 +158,7 @@ const ContextMenu: React.FC = memo((props) => { onClick={stopPropagation} ref={wrapperRef} className={classNames( - "fixed min-w-[215px] bg-white shadow-lg rounded-md z-50 animate-movement", + "fixed min-w-[215px] bg-white dark:bg-gray-900 shadow-lg rounded-md z-50 animate-movement", { "opacity-0": !animate, "opacity-100": animate, @@ -177,7 +177,7 @@ const ContextMenu: React.FC = memo((props) => { {!parentBarMode && (
    onAction("multi-select")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary rounded-t-md" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary rounded-t-md" >

    Multi-select

    @@ -186,7 +186,7 @@ const ContextMenu: React.FC = memo((props) => { {!isTrash && !isMedia && (
    onAction("rename")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary" >

    Rename

    @@ -195,7 +195,7 @@ const ContextMenu: React.FC = memo((props) => { {!folderMode && !isTrash && (
    onAction("share")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary" >

    Share

    @@ -204,7 +204,7 @@ const ContextMenu: React.FC = memo((props) => { {!isTrash && (
    onAction("download")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary" >

    Download

    @@ -213,7 +213,7 @@ const ContextMenu: React.FC = memo((props) => { {!isTrash && !isMedia && (
    onAction("move")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary" >

    Move

    @@ -222,7 +222,7 @@ const ContextMenu: React.FC = memo((props) => { {!isTrash && (
    onAction("trash")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary rounded-b-md" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary rounded-b-md" >

    Trash

    @@ -231,7 +231,7 @@ const ContextMenu: React.FC = memo((props) => { {isTrash && (
    onAction("restore")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-primary" >

    Restore

    @@ -240,7 +240,7 @@ const ContextMenu: React.FC = memo((props) => { {isTrash && (
    onAction("delete")} - className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-red-500 rounded-b-md" + className="text-gray-primary dark:text-gray-300 flex flex-row p-4 hover:bg-white-hover dark:hover:bg-gray-800 hover:text-red-500 rounded-b-md" >

    Delete

    diff --git a/src/components/DownloadPage/DownloadPage.tsx b/src/components/DownloadPage/DownloadPage.tsx index 9c17657..725ba89 100644 --- a/src/components/DownloadPage/DownloadPage.tsx +++ b/src/components/DownloadPage/DownloadPage.tsx @@ -99,7 +99,7 @@ const PublicDownloadPage = () => {

    -
    +
    {
    diff --git a/src/components/FileItem/FileItem.tsx b/src/components/FileItem/FileItem.tsx index 2e94038..3d9b022 100644 --- a/src/components/FileItem/FileItem.tsx +++ b/src/components/FileItem/FileItem.tsx @@ -170,10 +170,10 @@ const FileItem: React.FC = memo((props) => { return (
    = memo((props) => { )}
    = memo((props) => { "p-3 overflow-hidden text-ellipsis block w-full animate", elementSelected || elementMultiSelected ? "bg-primary text-white" - : "bg-white text-gray-primary" + : "bg-white dark:bg-gray-900 text-gray-primary dark:text-gray-300" )} >

    = memo((props) => { "m-0 font-normal max-w-full whitespace-nowrap text-xs animate block", elementSelected || elementMultiSelected ? "text-white" - : "text-gray-primary]" + : "text-gray-primary] dark:text-gray-300" )} > {formattedCreatedDate} diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 7f7526b..f399360 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -29,7 +29,7 @@ const Files = memo(() => {

  • @@ -54,7 +54,7 @@ const Files = memo(() => {
  • @@ -103,17 +103,17 @@ const Files = memo(() => {
    -

    Name

    +

    Name

    -

    Size

    +

    Size

    -

    Created

    +

    Created

    -

    Actions

    +

    Actions

    diff --git a/src/components/FolderItem/FolderItem.tsx b/src/components/FolderItem/FolderItem.tsx index ce9cc15..134b468 100644 --- a/src/components/FolderItem/FolderItem.tsx +++ b/src/components/FolderItem/FolderItem.tsx @@ -100,7 +100,7 @@ const FolderItem: React.FC = memo((props) => { return (
    = memo((props) => { "text-ellipsis block w-full animate", elementSelected || elementMultiSelected ? "bg-primary text-white" - : "bg-white text-gray-primary" + : "bg-white dark:bg-gray-900 text-gray-primary dark:text-gray-300" )} >

    = memo((props) => { "m-0 text-sm font-normal max-w-full overflow-hidden text-ellipsis whitespace-nowrap animate", elementSelected || elementMultiSelected ? "text-white" - : "text-black" + : "text-black dark:text-gray-100" )} > {folder.name} @@ -171,7 +171,7 @@ const FolderItem: React.FC = memo((props) => { "m-0 font-normal max-w-full whitespace-nowrap text-xs animate", elementSelected || elementMultiSelected ? "text-white" - : "text-gray-primary" + : "text-gray-primary dark:text-gray-300" )} > {dayjs(folder.createdAt).format("MM/DD/YY hh:mma")} diff --git a/src/components/Folders/Folders.tsx b/src/components/Folders/Folders.tsx index 3f9cddf..58a8e33 100644 --- a/src/components/Folders/Folders.tsx +++ b/src/components/Folders/Folders.tsx @@ -121,7 +121,7 @@ const Folders = memo( setEmail(e.target.value)} value={email} /> @@ -262,7 +262,7 @@ const LoginPage = () => { setPassword(e.target.value)} value={password} /> @@ -284,7 +284,7 @@ const LoginPage = () => { setVerifyPassword(e.target.value)} value={verifyPassword} /> diff --git a/src/components/Medias/Medias.tsx b/src/components/Medias/Medias.tsx index 9cd174a..11102cb 100644 --- a/src/components/Medias/Medias.tsx +++ b/src/components/Medias/Medias.tsx @@ -123,7 +123,7 @@ const Medias = memo( setSearch(e.target.value)} @@ -248,7 +248,7 @@ const MoverPopup = () => { { "bg-primary text-white hover:bg-primary-hover": selectedFolder?._id === folder._id, - "hover:bg-white-hover": + "hover:bg-white-hover dark:hover:bg-gray-800": selectedFolder?._id !== folder._id, } )} @@ -274,7 +274,7 @@ const MoverPopup = () => {

    )} -
    +

    {moveText}

    diff --git a/src/components/ParentBar/ParentBar.tsx b/src/components/ParentBar/ParentBar.tsx index 6acefcc..80d4db2 100644 --- a/src/components/ParentBar/ParentBar.tsx +++ b/src/components/ParentBar/ParentBar.tsx @@ -46,7 +46,7 @@ const ParentBar = memo(() => { }; return ( -
    +
    {contextMenuState.selected && (
    { )}
    -
    +
    {!isTrash ? "Home" : "Trash"} - +

    = memo((props) => { /> )} {thumbnailError && ( -

    +

    Error loading image

    )} diff --git a/src/components/QuickAccessItem/QuickAccessItem.tsx b/src/components/QuickAccessItem/QuickAccessItem.tsx index 221f442..e9616b1 100644 --- a/src/components/QuickAccessItem/QuickAccessItem.tsx +++ b/src/components/QuickAccessItem/QuickAccessItem.tsx @@ -99,10 +99,10 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => { return (
    { )}
    { "p-3 overflow-hidden text-ellipsis block w-full animate", elementSelected || elementMultiSelected ? "bg-[#3c85ee] text-white" - : "bg-white text-[#637381]" + : "bg-white dark:bg-gray-900 text-[#637381]" )} >

    { "m-0 font-normal max-w-full whitespace-nowrap text-xs animate", elementSelected || elementMultiSelected ? "text-white" - : "text-gray-primary]" + : "text-gray-primary] dark:text-gray-300" )} > {formattedDate} diff --git a/src/components/ResetPasswordPage/ResetPasswordPage.tsx b/src/components/ResetPasswordPage/ResetPasswordPage.tsx index 9f16947..ff2fd8a 100644 --- a/src/components/ResetPasswordPage/ResetPasswordPage.tsx +++ b/src/components/ResetPasswordPage/ResetPasswordPage.tsx @@ -64,9 +64,9 @@ const ResetPasswordPage = () => { return (

    -
    +
    -
    +
    {!loadingLogin && ( logo )} @@ -81,14 +81,14 @@ const ResetPasswordPage = () => { setPassword(e.target.value)} value={password} /> setVerifyPassword(e.target.value)} value={verifyPassword} /> diff --git a/src/components/RightSection/RightSection.tsx b/src/components/RightSection/RightSection.tsx index 33f326a..0e5185b 100644 --- a/src/components/RightSection/RightSection.tsx +++ b/src/components/RightSection/RightSection.tsx @@ -121,7 +121,7 @@ const RightSection = memo(() => { return (
    @@ -145,7 +145,7 @@ const RightSection = memo(() => {

    {bannerText}

    diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx index 94b4622..1aa403c 100644 --- a/src/components/SearchBar/SearchBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -133,16 +133,16 @@ const SearchBar = memo(() => { onChange={onChangeSearch} value={searchText} placeholder={searchTextPlaceholder} - className="w-full h-8 border border-gray-300 pl-11 pr-4 text-base text-black rounded-md" + className="w-full h-8 border border-gray-300 pl-11 pr-4 text-base text-black dark:text-gray-100 rounded-md" onFocus={onFocus} id="search-bar" autoComplete="off" />
    = ({ /> )} -
    +

    Account

    -
    -

    Email

    +
    +

    Email

    {user.email}

    {"emailVerified" in user && !user.emailVerified && ( -
    -

    Email not verified

    +
    +

    Email not verified

    {!user.emailVerified && (
    )} -
    -

    Change password

    +
    +

    Change password

    -
    -

    Logout account

    +
    +

    Logout account

    -
    -

    Logout all sessions

    +
    +

    Logout all sessions