feat: add dark mode UI and SQLite as alternate database backend
Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage, with a theme toggle in the Header and Settings page. Foundation didn't exist yet despite prior assumption, so it was built from scratch. SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail bytes continue to use the existing fs/S3 storage path. Implemented via a shared DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations, selected through dbFactory.ts, with JWT/encryption logic extracted into userCrypto.ts so both engines share it.
This commit is contained in:
+11
-1
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user