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:
@@ -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;
|
||||
Reference in New Issue
Block a user