feat: add dark mode UI and SQLite as alternate database backend
Release Please / release-please (push) Has been cancelled
Docker Build and Push (Development) / build-and-push-dev (push) Has been cancelled

Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage,
with a theme toggle in the Header and Settings page. Foundation didn't exist
yet despite prior assumption, so it was built from scratch.

SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at
runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail
bytes continue to use the existing fs/S3 storage path. Implemented via a shared
DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations,
selected through dbFactory.ts, with JWT/encryption logic extracted into
userCrypto.ts so both engines share it.
This commit is contained in:
2026-07-05 04:18:33 -07:00
parent bae5dbbd53
commit ecc340e7b1
74 changed files with 2233 additions and 487 deletions
+11 -1
View File
@@ -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
+7 -5
View File
@@ -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);
}
+104
View File
@@ -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;
+45
View File
@@ -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();
};
+150
View File
@@ -0,0 +1,150 @@
import { FileListQueryType } from "../types/file-types";
import { FolderListQueryType } from "../types/folder-types";
export interface IFileDB {
getPublicFile(fileID: string): Promise<any>;
getPublicInfo(fileID: string, tempToken: string): Promise<any>;
getFileInfo(fileID: string, userID: string): Promise<any>;
getQuickList(userID: string, limit: number): Promise<any[]>;
getList(
queryData: FileListQueryType,
sortBy: string,
limit: number
): Promise<any[]>;
getFileSearchList(
userID: string,
searchQuery: RegExp | string,
trashMode: boolean,
mediaMode: boolean
): Promise<any[]>;
getFileListByIncludedParent(
userID: string,
parentListString: string
): Promise<any[]>;
getFileListByOwner(userID: string): Promise<any[]>;
getFileListByParent(userID: string, parent: string): Promise<any[]>;
updateFileUploadedFile(
fileID: string,
userID: string,
parent: string,
parentList: string
): Promise<any>;
updateFolderUploadedFile(
fileID: string,
userID: string,
parent: string,
parentList: string
): Promise<any>;
setThumbnail(fileID: string, thumbnailID: string): Promise<any>;
removeOneTimePublicLink(fileID: string): Promise<any>;
removeLink(fileID: string, userID: string): Promise<any>;
makePublic(fileID: string, userID: string, token: string): Promise<any>;
makeOneTimePublic(
fileID: string,
userID: string,
token: string
): Promise<any>;
trashFile(
fileID: string,
parent: string,
parentList: string,
userID: string
): Promise<any>;
restoreFile(fileID: string, userID: string): Promise<any>;
removeTempToken(user: any, tempToken: string): Promise<any>;
trashFilesByParent(parentList: string, userID: string): Promise<any>;
restoreFilesByParent(parentList: string, userID: string): Promise<any>;
renameFile(fileID: string, userID: string, title: string): Promise<any>;
moveFile(
fileID: string,
userID: string,
parent: string,
parentList: string
): Promise<any>;
moveMultipleFiles(
userID: string,
currentParent: string,
newParent: string,
newParentList: string
): Promise<any>;
deleteFile(fileID: string, userID: string): Promise<any>;
createFile(data: { filename: string; uploadDate: string; length: number; metadata: any }): Promise<any>;
setVideoThumbnail(
fileID: string,
userID: string,
thumbnailID: string
): Promise<any>;
}
export interface IFolderDB {
getFolderSearchList(
userID: string,
searchQuery: RegExp | string,
trashMode: boolean
): Promise<any[]>;
getFolderInfo(folderID: string, userID: string): Promise<any>;
getFolderList(
queryData: FolderListQueryType,
sortBy: string
): Promise<any[]>;
getMoveFolderList(
userID: string,
parent?: string,
search?: string,
folderIDs?: string[]
): Promise<any[]>;
getFolderListByIncludedParent(
userID: string,
parent: string
): Promise<any[]>;
findAllFoldersByParent(parentID: string, userID: string): Promise<any[]>;
moveFolder(
folderID: string,
userID: string,
parent: string,
parentList: string[]
): Promise<any>;
renameFolder(folderID: string, userID: string, title: string): Promise<any>;
trashFoldersByParent(parentList: string[], userID: string): Promise<any>;
restoreFolder(folderID: string, userID: string): Promise<any>;
restoreFoldersByParent(parentList: string[], userID: string): Promise<any>;
trashFolder(folderID: string, userID: string): Promise<any>;
createFolder(folderData: {
name: string;
parent: string;
parentList: string[];
owner: string;
}): Promise<any>;
deleteFolder(folderID: string, userID: string): Promise<any>;
deleteFoldersByParentList(
parentList: string[],
userID: string
): Promise<any>;
deleteFoldersByOwner(userID: string): Promise<any>;
}
export interface IUserDB {
getUserInfo(userID: string): Promise<any>;
findByEmail(email: string): Promise<any>;
findByCreds(email: string, password: string): Promise<any>;
createUser(email: string, password: string): Promise<any>;
removeOldTokens(
userID: string,
uuid: string | undefined,
minusTime: number,
isTemp: boolean
): Promise<any>;
removeTempTokenByValue(userID: string, token: string): Promise<any>;
}
export interface IThumbnailDB {
getThumbnailInfo(userID: string, thumbnailID: string): Promise<any>;
removeThumbnail(userID: string, thumbnailID: any): Promise<any>;
createThumbnail(data: {
name: string;
owner: string;
IV: Buffer;
path?: string;
s3ID?: string;
}): Promise<any>;
}
+41 -1
View File
@@ -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;
+2 -1
View File
@@ -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
+16 -1
View File
@@ -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;
+45 -1
View File
@@ -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<any>;
};
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;
+452
View File
@@ -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;
+307
View File
@@ -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;
+76
View File
@@ -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;
+267
View File
@@ -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;
+4
View File
@@ -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,
+5 -2
View File
@@ -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");
+8 -15
View File
@@ -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;
}
}
+8 -14
View File
@@ -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;
}
}
+40 -182
View File
@@ -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<string>;
}
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;
+5 -1
View File
@@ -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));
@@ -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();
@@ -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()
@@ -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);
@@ -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) {
@@ -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();
@@ -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<string, FileDataType>;
@@ -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);
@@ -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();
@@ -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();
@@ -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();
@@ -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());
}
};
@@ -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) => {
+16 -24
View File
@@ -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<UserInterface>;
};
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);
+223
View File
@@ -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<string> => {
return bcrypt.hash(password, 8);
};
export const comparePassword = async (
password: string,
hash: string
): Promise<boolean> => {
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 };
};
+6 -11
View File
@@ -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");
+4
View File
@@ -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
+4
View File
@@ -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
+10
View File
@@ -10,6 +10,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
(function () {
var stored = localStorage.getItem("theme");
var isDark =
stored === "dark" ||
(stored !== "light" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
if (isDark) document.documentElement.classList.add("dark");
})();
</script>
</head>
<body>
+3 -1
View File
@@ -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",
@@ -132,7 +132,7 @@ const AddNewDropdown: React.FC<AddNewDropdownProps> = ({
<li>
<div>
<a
className="flex items-center justify-start px-5 py-3 no-underline overflow-hidden text-sm bg-white hover:bg-white-hover"
className="flex items-center justify-start px-5 py-3 no-underline overflow-hidden text-sm bg-white dark:bg-gray-900 hover:bg-white-hover dark:hover:bg-gray-800"
onClick={triggerFileUpload}
>
<UploadFileIcon className="w-4 h-4 mr-2.5 text-primary" />
@@ -142,7 +142,7 @@ const AddNewDropdown: React.FC<AddNewDropdownProps> = ({
</li>
<li>
<a
className="flex items-center justify-start px-5 py-3 no-underline overflow-hidden text-sm bg-white hover:bg-white-hover"
className="flex items-center justify-start px-5 py-3 no-underline overflow-hidden text-sm bg-white dark:bg-gray-900 hover:bg-white-hover dark:hover:bg-gray-800"
onClick={createFolder}
>
<CreateFolderIcon className="w-4 h-4 mr-2.5 text-primary" />
@@ -152,7 +152,7 @@ const AddNewDropdown: React.FC<AddNewDropdownProps> = ({
{supportsWebkitDirectory && (
<li>
<a
className="flex items-center justify-start px-5 py-3 no-underline overflow-hidden text-sm bg-white hover:bg-white-hover"
className="flex items-center justify-start px-5 py-3 no-underline overflow-hidden text-sm bg-white dark:bg-gray-900 hover:bg-white-hover dark:hover:bg-gray-800"
onClick={triggerFolderUpload}
>
<FolderUploadIcon className="w-4 h-4 mr-2.5 text-primary" />
+9 -9
View File
@@ -158,7 +158,7 @@ const ContextMenu: React.FC<ContextMenuProps> = 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<ContextMenuProps> = memo((props) => {
{!parentBarMode && (
<div
onClick={() => 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"
>
<MultiSelectIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Multi-select</p>
@@ -186,7 +186,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{!isTrash && !isMedia && (
<div
onClick={() => 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"
>
<RenameIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Rename</p>
@@ -195,7 +195,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{!folderMode && !isTrash && (
<div
onClick={() => 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"
>
<ShareIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Share</p>
@@ -204,7 +204,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{!isTrash && (
<div
onClick={() => 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"
>
<DownloadIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Download</p>
@@ -213,7 +213,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{!isTrash && !isMedia && (
<div
onClick={() => 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"
>
<MoveIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Move</p>
@@ -222,7 +222,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{!isTrash && (
<div
onClick={() => 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"
>
<TrashIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Trash</p>
@@ -231,7 +231,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{isTrash && (
<div
onClick={() => 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"
>
<RestoreIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Restore</p>
@@ -240,7 +240,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
{isTrash && (
<div
onClick={() => 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"
>
<TrashIcon className="w-5 h-5" />
<p className="ml-2.5 text-sm">Delete</p>
+1 -1
View File
@@ -99,7 +99,7 @@ const PublicDownloadPage = () => {
</p>
</div>
</div>
<div className="w-[90%] sm:w-[500px] p-6 bg-white rounded-md animate-easy">
<div className="w-[90%] sm:w-[500px] p-6 bg-white dark:bg-gray-900 rounded-md animate-easy">
<div className="bg-light-primary p-6 rounded-md flex items-center space-x-2">
<input
className="rounded-md w-full text-xs h-10 p-2"
@@ -144,7 +144,7 @@ const FileInfoPopup = () => {
</div>
</div>
<div
className="w-[90%] sm:w-[500px] p-6 bg-white rounded-md animate-easy"
className="w-[90%] sm:w-[500px] p-6 bg-white dark:bg-gray-900 rounded-md animate-easy"
style={{ marginTop: !animate ? "calc(100vh + 350px" : 0 }}
>
<div className="bg-light-primary p-6 rounded-md flex items-center space-x-2">
+5 -5
View File
@@ -170,10 +170,10 @@ const FileItem: React.FC<FileItemProps> = memo((props) => {
return (
<div
className={classNames(
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[150px] animiate hover:border-primary overflow-hidden bg-white ",
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[150px] animiate hover:border-primary overflow-hidden bg-white dark:bg-gray-900 ",
elementSelected || elementMultiSelected
? "border-primary"
: "border-gray-third"
: "border-gray-third dark:border-gray-700"
)}
onClick={fileClick}
onContextMenu={onContextMenu}
@@ -193,7 +193,7 @@ const FileItem: React.FC<FileItemProps> = memo((props) => {
)}
<div
className={classNames(
"inline-flex items-center w-full bg-white relative",
"inline-flex items-center w-full bg-white dark:bg-gray-900 relative",
{
"mt-2": !thumbnailLoaded,
}
@@ -247,7 +247,7 @@ const FileItem: React.FC<FileItemProps> = 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"
)}
>
<p
@@ -267,7 +267,7 @@ const FileItem: React.FC<FileItemProps> = 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}
+6 -6
View File
@@ -29,7 +29,7 @@ const Files = memo(() => {
<li className="mr-4" onClick={changeListViewMode}>
<a
className={classNames({
"text-gray-primary": listView,
"text-gray-primary dark:text-gray-300": listView,
"text-primary": !listView,
})}
>
@@ -54,7 +54,7 @@ const Files = memo(() => {
<li className="mr-2" onClick={changeListViewMode}>
<a
className={classNames({
"text-gray-primary": !listView,
"text-gray-primary dark:text-gray-300": !listView,
"text-primary": listView,
})}
>
@@ -103,17 +103,17 @@ const Files = memo(() => {
<tr>
<th>
<div className="flex flex-row items-center mb-2">
<p className="text-black text-sm font-medium">Name</p>
<p className="text-black dark:text-gray-100 text-sm font-medium">Name</p>
</div>
</th>
<th className="hidden fileListShowDetails:table-cell">
<p className="text-black text-sm font-medium mb-2">Size</p>
<p className="text-black dark:text-gray-100 text-sm font-medium mb-2">Size</p>
</th>
<th className="hidden fileListShowDetails:table-cell">
<p className="text-black text-sm font-medium mb-2">Created</p>
<p className="text-black dark:text-gray-100 text-sm font-medium mb-2">Created</p>
</th>
<th>
<p className="text-black text-sm font-medium mb-2">Actions</p>
<p className="text-black dark:text-gray-100 text-sm font-medium mb-2">Actions</p>
</th>
</tr>
</thead>
+4 -4
View File
@@ -100,7 +100,7 @@ const FolderItem: React.FC<FolderItemProps> = memo((props) => {
return (
<div
className={classNames(
"p-3 border border-gray-third rounded-md overflow-hidden cursor-pointer animate hover:border-primary",
"p-3 border border-gray-third dark:border-gray-700 rounded-md overflow-hidden cursor-pointer animate hover:border-primary",
{
"bg-primary": elementSelected || elementMultiSelected,
}
@@ -151,7 +151,7 @@ const FolderItem: React.FC<FolderItemProps> = 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"
)}
>
<p
@@ -159,7 +159,7 @@ const FolderItem: React.FC<FolderItemProps> = 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<FolderItemProps> = 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")}
+1 -1
View File
@@ -121,7 +121,7 @@ const Folders = memo(
</svg>
</a>
<select
className="text-sm font-medium appearance-none bg-white"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900"
onChange={switchTypeOrderBy}
value={
sortBy === "alp_desc" || sortBy === "alp_asc" ? "name" : "date"
+21 -3
View File
@@ -6,12 +6,21 @@ import { closeDrawer, openDrawer } from "../../reducers/leftSection";
import { useUtils } from "../../hooks/utils";
import ChevronOutline from "../../icons/ChevronOutline";
import SettingsIconSolid from "../../icons/SettingsIconSolid";
import SunIcon from "../../icons/SunIcon";
import MoonIcon from "../../icons/MoonIcon";
import { useTheme } from "../../hooks/theme";
const Header = () => {
const drawerOpen = useAppSelector((state) => state.leftSection.drawOpen);
const { isSettings } = useUtils();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { mode, setMode } = useTheme();
const isDark = mode === "dark";
const toggleTheme = () => {
setMode(isDark ? "light" : "dark");
};
const openDrawerClick = () => {
dispatch(openDrawer());
@@ -24,7 +33,7 @@ const Header = () => {
return (
<header
id="header"
className="select-none border-b fixed top-0 left-0 w-full bg-white z-10"
className="select-none border-b dark:border-gray-700 fixed top-0 left-0 w-full bg-white dark:bg-gray-900 z-10"
>
<div className="px-6 flex justify-between items-center py-3">
<div className="items-center w-[260px] hidden desktopMode:flex">
@@ -55,14 +64,23 @@ const Header = () => {
</div>
)}
<SearchBar />
<div className="justify-end w-[260px] hidden desktopMode:flex">
<div className="justify-end w-[260px] hidden desktopMode:flex items-center gap-4">
<div>
<a onClick={toggleTheme} className="cursor-pointer">
{isDark ? (
<SunIcon className="w-6 h-6 text-gray-primary dark:text-gray-300" />
) : (
<MoonIcon className="w-6 h-6 text-gray-primary dark:text-gray-300" />
)}
</a>
</div>
<div>
<div>
<a
onClick={() => navigate("/settings")}
className="cursor-pointer"
>
<SettingsIconSolid className="w-6 h-6 text-gray-primary" />
<SettingsIconSolid className="w-6 h-6 text-gray-primary dark:text-gray-300" />
</a>
</div>
</div>
+10 -10
View File
@@ -110,7 +110,7 @@ const LeftSection = ({
<div
ref={wrapperRef}
className={classNames(
"p-6 fixed desktopMode:relative border-r w-[270px] min-w-[270px] bg-white h-full z-20 desktopMode:z-0 animate-movement mt-1.5",
"p-6 fixed desktopMode:relative border-r w-[270px] min-w-[270px] bg-white dark:bg-gray-900 h-full z-20 desktopMode:z-0 animate-movement mt-1.5",
{
"-left-[270px] desktopMode:left-0": !leftSectionOpen,
"left-0": leftSectionOpen,
@@ -138,10 +138,10 @@ const LeftSection = ({
<div
className={classNames(
"pl-2 mr-5 py-2 hover:bg-white-hover rounded-md cursor-pointer animate flex flex-row items-center w-full",
"pl-2 mr-5 py-2 hover:bg-white-hover dark:hover:bg-gray-800 rounded-md cursor-pointer animate flex flex-row items-center w-full",
isHome || isHomeFolder
? "text-primary bg-white-hover"
: "text-gray-primary"
? "text-primary bg-white-hover dark:bg-gray-800"
: "text-gray-primary dark:text-gray-300"
)}
onClick={goHome}
>
@@ -151,8 +151,8 @@ const LeftSection = ({
<div
className={classNames(
"pl-2 mr-5 py-2 hover:bg-white-hover rounded-md cursor-pointer animate flex flex-row items-center mt-1 w-full",
isMedia ? "text-primary bg-white-hover" : "text-gray-primary"
"pl-2 mr-5 py-2 hover:bg-white-hover dark:hover:bg-gray-800 rounded-md cursor-pointer animate flex flex-row items-center mt-1 w-full",
isMedia ? "text-primary bg-white-hover dark:bg-gray-800" : "text-gray-primary dark:text-gray-300"
)}
onClick={goMedia}
>
@@ -162,8 +162,8 @@ const LeftSection = ({
<div
className={classNames(
"pl-2 mr-5 py-2 hover:bg-white-hover rounded-md cursor-pointer animate flex flex-row items-center desktopMode:hidden mt-1 w-full",
isSettings ? "text-primary bg-white-hover" : "text-gray-primary"
"pl-2 mr-5 py-2 hover:bg-white-hover dark:hover:bg-gray-800 rounded-md cursor-pointer animate flex flex-row items-center desktopMode:hidden mt-1 w-full",
isSettings ? "text-primary bg-white-hover dark:bg-gray-800" : "text-gray-primary dark:text-gray-300"
)}
onClick={goSettings}
>
@@ -173,8 +173,8 @@ const LeftSection = ({
<div
className={classNames(
"pl-2 mr-5 py-2 hover:bg-white-hover rounded-md cursor-pointer animate flex flex-row items-center mt-1 w-full",
isTrash ? "text-red-500 bg-white-hover" : "text-gray-primary"
"pl-2 mr-5 py-2 hover:bg-white-hover dark:hover:bg-gray-800 rounded-md cursor-pointer animate flex flex-row items-center mt-1 w-full",
isTrash ? "text-red-500 bg-white-hover dark:bg-gray-800" : "text-gray-primary dark:text-gray-300"
)}
onClick={goTrash}
>
+5 -5
View File
@@ -234,9 +234,9 @@ const LoginPage = () => {
return (
<div>
<div className="bg-[#F4F4F6] w-screen dynamic-height flex justify-center items-center">
<div className="rounded-md shadow-lg bg-white p-10 relative w-[90%] sm:w-[500px] animate-height">
<div className="rounded-md shadow-lg bg-white dark:bg-gray-900 p-10 relative w-[90%] sm:w-[500px] animate-height">
<div className="absolute -top-10 left-0 right-0 flex justify-center items-center">
<div className="flex items-center justify-center rounded-full bg-white p-3 shadow-md">
<div className="flex items-center justify-center rounded-full bg-white dark:bg-gray-900 p-3 shadow-md">
{!loadingLogin && (
<img src="/images/icon.png" alt="logo" className="w-[45px]" />
)}
@@ -251,7 +251,7 @@ const LoginPage = () => {
<input
type="text"
placeholder="Email address"
className="w-full h-[48px] pl-[12px] pr-[12px] text-black border border-[#637381] rounded-[5px] outline-none text-[15px]"
className="w-full h-[48px] pl-[12px] pr-[12px] text-black dark:text-gray-100 border border-[#637381] rounded-[5px] outline-none text-[15px]"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
@@ -262,7 +262,7 @@ const LoginPage = () => {
<input
type="password"
placeholder="Password"
className="w-full h-[48px] pl-[12px] pr-[70px] text-black border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
className="w-full h-[48px] pl-[12px] pr-[70px] text-black dark:text-gray-100 border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
@@ -284,7 +284,7 @@ const LoginPage = () => {
<input
type="password"
placeholder="Verify Password"
className="w-full h-[48px] pl-[12px] pr-[12px] text-black border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
className="w-full h-[48px] pl-[12px] pr-[12px] text-black dark:text-gray-100 border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
onChange={(e) => setVerifyPassword(e.target.value)}
value={verifyPassword}
/>
+1 -1
View File
@@ -123,7 +123,7 @@ const Medias = memo(
</svg>
</a>
<select
className="text-sm font-medium appearance-none bg-white"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900"
onChange={mediaFilterOnChange}
value={mediaFilter}
>
+4 -4
View File
@@ -203,7 +203,7 @@ const MoverPopup = () => {
</div>
</div>
<div
className="bg-white w-full max-w-[500px] p-4 rounded-md animate-easy"
className="bg-white dark:bg-gray-900 w-full max-w-[500px] p-4 rounded-md animate-easy"
style={{ marginTop: !animate ? "calc(100vh + 480px" : 0 }}
>
<div
@@ -219,7 +219,7 @@ const MoverPopup = () => {
/>
{!multiSelectMode && (
<input
className="w-full py-2 px-3 text-black border border-gray-primary rounded-md text-sm outline-none"
className="w-full py-2 px-3 text-black dark:text-gray-100 border border-gray-primary rounded-md text-sm outline-none"
placeholder="Search"
value={search}
onChange={(e) => 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 = () => {
</div>
)}
</div>
<div className="mt-4 border border-gray-secondary rounded-md p-4">
<div className="mt-4 border border-gray-secondary dark:border-gray-700 rounded-md p-4">
<p className="text-sm overflow-hidden text-ellipsis whitespace-nowrap select-none">
{moveText}
</p>
+5 -5
View File
@@ -46,7 +46,7 @@ const ParentBar = memo(() => {
};
return (
<div className="w-full items-center flex border border-gray-third rounded-md">
<div className="w-full items-center flex border border-gray-third dark:border-gray-700 rounded-md">
{contextMenuState.selected && (
<div onClick={clickStopPropagation}>
<ContextMenu
@@ -60,22 +60,22 @@ const ParentBar = memo(() => {
)}
<div className="flex items-center">
<div className="flex items-center justify-center h-full border-r p-2 mr-2 hover:bg-gray-third">
<div className="flex items-center justify-center h-full border-r p-2 mr-2 hover:bg-gray-third dark:hover:bg-gray-800">
<ArrowBackIcon
className="w-5 h-5 cursor-pointer"
onClick={goBackAFolder}
/>
</div>
<a
className="text-[#637381] text-md leading-[21px] font-medium m-0 no-underline animate cursor-pointer rounded-md p-1 hover:bg-gray-third"
className="text-[#637381] text-md leading-[21px] font-medium m-0 no-underline animate cursor-pointer rounded-md p-1 hover:bg-gray-third dark:hover:bg-gray-800"
onClick={goHomeOrTrash}
>
{!isTrash ? "Home" : "Trash"}
</a>
<SpacerIcon className="text-black mx-2 w-2.5 h-2.5" />
<SpacerIcon className="text-black dark:text-gray-100 mx-2 w-2.5 h-2.5" />
<p
onClick={onContextMenu}
className="text-primary text-md leading-[21px] font-medium m-0 whitespace-nowrap max-w-[170px] sm:max-w-[300px] overflow-hidden text-ellipsis cursor-pointer rounded-md p-1 hover:bg-gray-third "
className="text-primary text-md leading-[21px] font-medium m-0 whitespace-nowrap max-w-[170px] sm:max-w-[300px] overflow-hidden text-ellipsis cursor-pointer rounded-md p-1 hover:bg-gray-third dark:hover:bg-gray-800 "
onContextMenu={onContextMenu}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
@@ -358,7 +358,7 @@ const PhotoViewerPopup: React.FC<PhotoViewerPopupProps> = memo((props) => {
/>
)}
{thumbnailError && (
<div className="bg-white p-6 rounded-md">
<div className="bg-white dark:bg-gray-900 p-6 rounded-md">
<p className="text-center text-sm">Error loading image</p>
</div>
)}
@@ -99,10 +99,10 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
return (
<div
className={classNames(
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[150px] animiate hover:border-[#3c85ee] overflow-hidden bg-white",
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[150px] animiate hover:border-[#3c85ee] overflow-hidden bg-white dark:bg-gray-900",
elementSelected || elementMultiSelected
? "border-[#3c85ee]"
: "border-gray-third"
: "border-gray-third dark:border-gray-700"
)}
onClick={quickItemClick}
onContextMenu={onContextMenu}
@@ -122,7 +122,7 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
)}
<div
className={classNames(
"inline-flex items-center w-full bg-white relative",
"inline-flex items-center w-full bg-white dark:bg-gray-900 relative",
{
"mt-2": !thumbnailLoaded,
}
@@ -176,7 +176,7 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
"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]"
)}
>
<p
@@ -196,7 +196,7 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
"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}
@@ -64,9 +64,9 @@ const ResetPasswordPage = () => {
return (
<div>
<div className="bg-[#F4F4F6] w-screen dynamic-height flex justify-center items-center">
<div className="rounded-md shadow-lg bg-white p-10 relative w-[90%] sm:w-[500px] animate-height">
<div className="rounded-md shadow-lg bg-white dark:bg-gray-900 p-10 relative w-[90%] sm:w-[500px] animate-height">
<div className="absolute -top-10 left-0 right-0 flex justify-center items-center">
<div className="flex items-center justify-center rounded-full bg-white p-3 shadow-md">
<div className="flex items-center justify-center rounded-full bg-white dark:bg-gray-900 p-3 shadow-md">
{!loadingLogin && (
<img src="/images/icon.png" alt="logo" className="w-[45px]" />
)}
@@ -81,14 +81,14 @@ const ResetPasswordPage = () => {
<input
type="password"
placeholder="Password"
className="w-full h-[48px] pl-[12px] pr-[70px] text-black border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
className="w-full h-[48px] pl-[12px] pr-[70px] text-black dark:text-gray-100 border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<input
type="password"
placeholder="Verify Password"
className="w-full h-[48px] pl-[12px] pr-[12px] text-black border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
className="w-full h-[48px] pl-[12px] pr-[12px] text-black dark:text-gray-100 border border-[#637381] rounded-[5px] outline-none text-[15px] mt-4"
onChange={(e) => setVerifyPassword(e.target.value)}
value={verifyPassword}
/>
+2 -2
View File
@@ -121,7 +121,7 @@ const RightSection = memo(() => {
return (
<div
className={classNames(
"!hidden desktopMode:!flex min-w-[260px] max-w-[260px] border-l border-gray-secondary bg-white right-0 justify-center relative mt-1.5",
"!hidden desktopMode:!flex min-w-[260px] max-w-[260px] border-l border-gray-secondary dark:border-gray-700 bg-white dark:bg-gray-900 right-0 justify-center relative mt-1.5",
selectedItem.id === "" ? "flex justify-center items-center" : ""
)}
>
@@ -145,7 +145,7 @@ const RightSection = memo(() => {
<p className="text-sm ml-6 z-10">{bannerText}</p>
</div>
<CloseIcon
className="w-5 h-5 p-1 cursor-pointer absolute right-3 top-5 bg-white rounded-full shadow-lg z-10"
className="w-5 h-5 p-1 cursor-pointer absolute right-3 top-5 bg-white dark:bg-gray-900 rounded-full shadow-lg z-10"
onClick={reset}
/>
</div>
+3 -3
View File
@@ -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"
/>
<div
className={classNames(
"absolute left-0 top-8 bg-white shadow-xl rounded-md w-full max-h-[400px] overflow-y-scroll animate-movement",
"absolute left-0 top-8 bg-white dark:bg-gray-900 shadow-xl rounded-md w-full max-h-[400px] overflow-y-scroll animate-movement",
{
"border border-gray-secondary":
"border border-gray-secondary dark:border-gray-700":
showSuggestions && debouncedSearchText.length,
}
)}
@@ -110,17 +110,17 @@ const SettingsPageAccount: React.FC<SettingsPageAccountProps> = ({
/>
)}
<div className="bg-white-hover p-3 flex items-center w-full rounded-md">
<div className="bg-white-hover dark:bg-gray-800 p-3 flex items-center w-full rounded-md">
<p className="text-base">Account</p>
</div>
<div>
<div className="p-3 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Email</p>
<div className="p-3 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Email</p>
<p>{user.email}</p>
</div>
{"emailVerified" in user && !user.emailVerified && (
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Email not verified</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Email not verified</p>
{!user.emailVerified && (
<button
className="text-primary hover:text-primary-hover cursor-pointer"
@@ -131,8 +131,8 @@ const SettingsPageAccount: React.FC<SettingsPageAccountProps> = ({
)}
</div>
)}
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Change password</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Change password</p>
<button
className="text-primary hover:text-primary-hover cursor-pointer"
onClick={() => setShowChangePasswordPopup(true)}
@@ -140,8 +140,8 @@ const SettingsPageAccount: React.FC<SettingsPageAccountProps> = ({
Change
</button>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Logout account</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Logout account</p>
<button
className="text-primary hover:text-primary-hover cursor-pointer"
onClick={logoutClick}
@@ -149,8 +149,8 @@ const SettingsPageAccount: React.FC<SettingsPageAccountProps> = ({
Logout
</button>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Logout all sessions</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Logout all sessions</p>
<button
className="text-primary hover:text-primary-hover cursor-pointer"
onClick={logoutAllClick}
@@ -92,16 +92,16 @@ const SettingsChangePasswordPopup: React.FC<
className="w-screen dynamic-height bg-black bg-opacity-80 absolute top-0 left-0 right-0 bottom-0 z-50 flex justify-center items-center flex-col"
onClick={outterWrapperClick}
>
<div className="w-[300px] sm:w-[440px] bg-white rounded-md animate">
<div className="flex justify-between p-4 border-b border-gray-secondary">
<div className="w-[300px] sm:w-[440px] bg-white dark:bg-gray-900 rounded-md animate">
<div className="flex justify-between p-4 border-b border-gray-secondary dark:border-gray-700">
<p className="text-md ">Change password</p>
<CloseIcon className="w-6 h-6 cursor-pointer" onClick={closePopup} />
</div>
<form className="mt-2 p-4" onSubmit={submitPasswordChange}>
<label>
<p className="text-sm text-gray-primary mb-1">Current password</p>
<p className="text-sm text-gray-primary dark:text-gray-300 mb-1">Current password</p>
<input
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black w-full text-sm mb-3"
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black dark:text-gray-100 w-full text-sm mb-3"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
@@ -109,9 +109,9 @@ const SettingsChangePasswordPopup: React.FC<
</label>
<label>
<p className="text-sm text-gray-primary mb-1">New password</p>
<p className="text-sm text-gray-primary dark:text-gray-300 mb-1">New password</p>
<input
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black w-full mb-3"
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black dark:text-gray-100 w-full mb-3"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
@@ -119,11 +119,11 @@ const SettingsChangePasswordPopup: React.FC<
</label>
<label>
<p className="text-sm text-gray-primary mb-1">
<p className="text-sm text-gray-primary dark:text-gray-300 mb-1">
Verify new password
</p>
<input
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black w-full mb-3"
className="border border-[#BEC9D3] rounded-md py-2 px-3 text-black dark:text-gray-100 w-full mb-3"
type="password"
value={verifyNewPassword}
onChange={(e) => setVerifyNewPassword(e.target.value)}
@@ -1,7 +1,10 @@
import { useEffect, useState } from "react";
import { usePreferenceSetter } from "../../hooks/preferenceSetter";
import { useTheme } from "../../hooks/theme";
import { ThemeMode } from "../../reducers/theme";
const SettingsPageGeneral = () => {
const { mode, setMode } = useTheme();
const [listViewStyle, setListViewStyle] = useState("list");
const [sortBy, setSortBy] = useState("date");
const [orderBy, setOrderBy] = useState("descending");
@@ -98,60 +101,72 @@ const SettingsPageGeneral = () => {
return (
<div>
<div className="bg-white-hover p-3 flex items-center w-full rounded-md">
<div className="bg-white-hover dark:bg-gray-800 p-3 flex items-center w-full rounded-md">
<p className="text-base">General settings</p>
</div>
<div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">File list style</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Theme</p>
<select
value={mode}
onChange={(e) => setMode(e.target.value as ThemeMode)}
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900 text-primary"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">File list style</p>
<select
value={listViewStyle}
onChange={fileListStyleChange}
className="text-sm font-medium appearance-none bg-white text-primary"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900 text-primary"
>
<option value="grid">Grid</option>
<option value="list">List</option>
</select>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Sort by</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Sort by</p>
<select
value={sortBy}
onChange={sortByChange}
className="text-sm font-medium appearance-none bg-white text-primary"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900 text-primary"
>
<option value="date">Date</option>
<option value="name">Name</option>
</select>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Order by</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Order by</p>
<select
value={orderBy}
onChange={orderByChange}
className="text-sm font-medium appearance-none bg-white text-primary"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900 text-primary"
>
<option value="descending">Descending</option>
<option value="ascending">Ascending</option>
</select>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Single click to enter folders</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Single click to enter folders</p>
<select
value={singleClickFolders}
onChange={singleClickFoldersChange}
className="text-sm font-medium appearance-none bg-white text-primary"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900 text-primary"
>
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
</div>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary">
<p className="text-gray-primary">Load thumbnails</p>
<div className="px-3 py-4 flex flex-row justify-between items-center border-b border-gray-secondary dark:border-gray-700">
<p className="text-gray-primary dark:text-gray-300">Load thumbnails</p>
<select
value={loadThumbnails}
onChange={loadThumbnailsChange}
className="text-sm font-medium appearance-none bg-white text-primary"
className="text-sm font-medium appearance-none bg-white dark:bg-gray-900 text-primary"
>
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
+7 -7
View File
@@ -72,7 +72,7 @@ const SettingsPage = () => {
<div
ref={wrapperRef}
className={classNames(
"fixed sm:relative px-4 border-r border-gray-secondary w-72 dynamic-height animate-movement bg-white",
"fixed sm:relative px-4 border-r border-gray-secondary dark:border-gray-700 w-72 dynamic-height animate-movement bg-white dark:bg-gray-900",
{
"-ml-72 sm:ml-0": !showSidebarMobile,
"ml-0": showSidebarMobile,
@@ -89,10 +89,10 @@ const SettingsPage = () => {
<div className="mt-8 space-y-2">
<div
className={classNames(
"pl-2 mr-5 py-2 hover:bg-white-hover rounded-md cursor-pointer animate flex flex-row items-center w-full",
"pl-2 mr-5 py-2 hover:bg-white-hover dark:hover:bg-gray-800 rounded-md cursor-pointer animate flex flex-row items-center w-full",
tab === "account"
? "text-primary bg-white-hover"
: "text-gray-primary"
? "text-primary bg-white-hover dark:bg-gray-800"
: "text-gray-primary dark:text-gray-300"
)}
onClick={() => changeTab("account")}
>
@@ -101,10 +101,10 @@ const SettingsPage = () => {
</div>
<div
className={classNames(
"pl-2 mr-5 py-2 hover:bg-white-hover rounded-md cursor-pointer animate flex flex-row items-center w-full",
"pl-2 mr-5 py-2 hover:bg-white-hover dark:hover:bg-gray-800 rounded-md cursor-pointer animate flex flex-row items-center w-full",
tab === "general"
? "text-primary bg-white-hover"
: "text-gray-primary"
? "text-primary bg-white-hover dark:bg-gray-800"
: "text-gray-primary dark:text-gray-300"
)}
onClick={() => changeTab("general")}
>
+1 -1
View File
@@ -214,7 +214,7 @@ const SharePopup = memo(() => {
</div>
</div>
<div
className="w-[90%] sm:w-[500px] p-6 bg-white rounded-md animate-easy"
className="w-[90%] sm:w-[500px] p-6 bg-white dark:bg-gray-900 rounded-md animate-easy"
style={{ marginTop: !animate ? "calc(100vh + 340px" : 0 }}
>
<p>Share file</p>
+1 -1
View File
@@ -36,7 +36,7 @@ const Uploader = memo(() => {
};
return (
<div className="fixed bottom-0 sm:right-[20px] z-10 bg-white shadow-lg rounded-t-md w-full sm:w-[315px]">
<div className="fixed bottom-0 sm:right-[20px] z-10 bg-white dark:bg-gray-900 shadow-lg rounded-t-md w-full sm:w-[315px]">
<div className="flex flex-row bg-[#3c85ee] justify-between p-4 rounded-t-md">
<p className="text-white">{uploadTitle}</p>
<div className="flex flex-row items-center justify-center">
+44
View File
@@ -0,0 +1,44 @@
import { useEffect } from "react";
import { useAppDispatch, useAppSelector } from "./store";
import { setTheme, ThemeMode } from "../reducers/theme";
const resolveIsDark = (mode: ThemeMode) => {
if (mode === "dark") return true;
if (mode === "light") return false;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
};
export const useTheme = () => {
const dispatch = useAppDispatch();
const mode = useAppSelector((state) => state.theme.mode);
useEffect(() => {
const stored = window.localStorage.getItem("theme");
if (stored === "light" || stored === "dark" || stored === "system") {
dispatch(setTheme(stored));
}
}, [dispatch]);
useEffect(() => {
document.documentElement.classList.toggle("dark", resolveIsDark(mode));
window.localStorage.setItem("theme", mode);
}, [mode]);
useEffect(() => {
if (mode !== "system") return undefined;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => {
document.documentElement.classList.toggle("dark", mediaQuery.matches);
};
mediaQuery.addEventListener("change", onChange);
return () => mediaQuery.removeEventListener("change", onChange);
}, [mode]);
const setMode = (nextMode: ThemeMode) => {
dispatch(setTheme(nextMode));
};
return { mode, setMode };
};
+15
View File
@@ -0,0 +1,15 @@
type MoonIconType = React.SVGAttributes<SVGSVGElement>;
const MoonIcon: React.FC<MoonIconType> = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<title>dark mode</title>
<path
d="M12.3,4.9C10.9,4.9 9.5,5.3 8.4,6.2C10.4,6.8 12,8.7 12,11C12,13.3 10.4,15.2 8.4,15.8C9.5,16.7 10.9,17.1 12.3,17.1C15.6,17.1 18.3,14.4 18.3,11C18.3,7.6 15.6,4.9 12.3,4.9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"
fill="currentColor"
/>
</svg>
);
};
export default MoonIcon;
+15
View File
@@ -0,0 +1,15 @@
type SunIconType = React.SVGAttributes<SVGSVGElement>;
const SunIcon: React.FC<SunIconType> = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<title>light mode</title>
<path
d="M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z"
fill="currentColor"
/>
</svg>
);
};
export default SunIcon;
+21
View File
@@ -0,0 +1,21 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export type ThemeMode = "light" | "dark" | "system";
const initialState = {
mode: "system" as ThemeMode,
};
const themeSlice = createSlice({
name: "theme",
initialState,
reducers: {
setTheme: (state, action: PayloadAction<ThemeMode>) => {
state.mode = action.payload;
},
},
});
export const { setTheme } = themeSlice.actions;
export default themeSlice.reducer;
+2
View File
@@ -18,12 +18,14 @@ import SettingsPage from "../components/SettingsPage/SettingsPage";
import Homepage from "../components/Homepage/Homepage";
import { usePreferenceSetter } from "../hooks/preferenceSetter";
import useAccessTokenHandler from "../hooks/user";
import { useTheme } from "../hooks/theme";
// export const history = createHistory();
const AppRouter = () => {
usePreferenceSetter();
useAccessTokenHandler();
useTheme();
return (
<BrowserRouter>
+2
View File
@@ -5,6 +5,7 @@ import leftSectionReducer from "../reducers/leftSection";
import userReducer from "../reducers/user";
import uploaderReducer from "../reducers/uploader";
import generalReducer from "../reducers/general";
import themeReducer from "../reducers/theme";
const store = configureStore({
reducer: {
@@ -14,6 +15,7 @@ const store = configureStore({
leftSection: leftSectionReducer,
user: userReducer,
uploader: uploaderReducer,
theme: themeReducer,
},
});
+32
View File
@@ -2,6 +2,36 @@
box-sizing: border-box;
}
:root {
--color-bg: #ffffff;
--color-bg-secondary: #f6f5fd;
--color-bg-tertiary: #ebe9f9;
--color-text: #1c2029;
--color-text-secondary: #637381;
--color-border: #e8eef2;
--color-primary: #3c85ee;
--color-primary-hover: #326bcc;
--color-danger: #e53e3e;
--color-success: #16a34a;
--color-spinner-track: #f3f3f3;
--color-spinner-spin: #3498db;
}
.dark {
--color-bg: #16181d;
--color-bg-secondary: #1f2229;
--color-bg-tertiary: #262a33;
--color-text: #e5e7eb;
--color-text-secondary: #9aa4b2;
--color-border: #2d323c;
--color-primary: #3c85ee;
--color-primary-hover: #5b9bf5;
--color-danger: #f87171;
--color-success: #4ade80;
--color-spinner-track: #2d323c;
--color-spinner-spin: #3c85ee;
}
html {
}
@@ -16,6 +46,8 @@ body {
height: 100dvh;
width: 100vw;
overflow: hidden;
background-color: var(--color-bg);
color: var(--color-text);
}
button {
+15 -10
View File
@@ -1,6 +1,6 @@
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border: 3px solid var(--color-spinner-track);
border-top: 3px solid var(--color-spinner-spin);
border-radius: 50%;
width: 50px;
height: 50px;
@@ -9,8 +9,8 @@
}
.spinner-small {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border: 3px solid var(--color-spinner-track);
border-top: 3px solid var(--color-spinner-spin);
border-radius: 50%;
width: 20px;
height: 20px;
@@ -47,20 +47,20 @@
/* Style the background of the progress bar */
.custom-progress::-webkit-progress-bar {
background-color: #e0dcf3; /* Light gray for the background */
background-color: var(--color-bg-tertiary);
border-radius: 3px;
height: 5px;
}
/* Style the filled portion of the progress bar */
.custom-progress::-webkit-progress-value {
background-color: #3c85ee; /* Blue for progress */
background-color: var(--color-primary);
border-radius: 3px;
}
/* For Firefox */
.custom-progress::-moz-progress-bar {
background-color: #3c85ee; /* Blue for progress */
background-color: var(--color-primary);
border-radius: 3px;
}
@@ -78,7 +78,7 @@
appearance: none;
border-radius: 3px;
border: none;
background-color: red;
background-color: var(--color-danger);
margin-top: 15px;
}
@@ -89,7 +89,7 @@
border-radius: 3px;
border: none;
margin-top: 15px;
@apply bg-green-600;
background-color: var(--color-success);
}
.custom-progress.indeterminate {
@@ -105,7 +105,12 @@
left: 0;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, #3c85ee, transparent);
background: linear-gradient(
90deg,
transparent,
var(--color-primary),
transparent
);
animation: loading 1.5s infinite;
}
+7 -2
View File
@@ -6,10 +6,15 @@
}
}
.swal2-popup {
background-color: var(--color-bg) !important;
color: var(--color-text) !important;
}
.swal2-confirm {
background-color: #3c85ee !important;
background-color: var(--color-primary) !important;
}
.swal2-cancel {
background-color: red !important;
background-color: var(--color-danger) !important;
}
+8 -3
View File
@@ -27,21 +27,26 @@
}
.Toastify__progress-bar--success {
background-color: #3c85ee !important;
background-color: var(--color-primary) !important;
}
.--toastify-color-transparent .Toastify__toast-icon {
background-color: #3c85ee !important;
background-color: var(--color-primary) !important;
}
.Toastify__toast--success .Toastify__icon svg path {
fill: #3c85ee !important;
fill: var(--color-primary) !important;
}
:root {
--toastify-icon-color-success: #3c85ee !important;
}
.Toastify__toast {
background-color: var(--color-bg) !important;
color: var(--color-text) !important;
}
.disable-force-touch {
-webkit-touch-callout: none;
}
+1
View File
@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
+2
View File
@@ -19,6 +19,8 @@ const envFileFix = (env) => {
env.root = process.env.ROOT;
env.url = process.env.URL;
env.mongoURL = process.env.MONGODB_URL;
env.dbEngine = process.env.DB_ENGINE || "mongo";
env.sqliteDBPath = process.env.SQLITE_DB_PATH || "./data/mydrive.db";
env.dbType = process.env.DB_TYPE;
env.fsDirectory = process.env.FS_DIRECTORY;
env.s3ID = process.env.S3_ID;