diff --git a/collector/utils/files/index.js b/collector/utils/files/index.js index 1263a59d0..89b33ad8e 100644 --- a/collector/utils/files/index.js +++ b/collector/utils/files/index.js @@ -124,7 +124,7 @@ function isWithin(outer, inner) { function normalizePath(filepath = "") { const result = path - .normalize(filepath.trim()) + .normalize(filepath.replace(/\s/g, "-").trim()) .replace(/^(\.\.(\/|\\|$))+/, "") .trim(); if (["..", ".", "/"].includes(result)) throw new Error("Invalid path."); diff --git a/frontend/src/components/Footer/index.jsx b/frontend/src/components/Footer/index.jsx index 6e80f0dfe..2dfccc3f9 100644 --- a/frontend/src/components/Footer/index.jsx +++ b/frontend/src/components/Footer/index.jsx @@ -108,10 +108,13 @@ export default function Footer() { rel="noreferrer" className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" > - {React.createElement(ICON_COMPONENTS[item.icon], { - weight: "fill", - className: "h-5 w-5", - })} + {React.createElement( + ICON_COMPONENTS?.[item.icon] ?? ICON_COMPONENTS.Info, + { + weight: "fill", + className: "h-5 w-5", + } + )} ))} {!isMobile && } diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 959e023ff..9b836b19a 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -33,10 +33,7 @@ function adminEndpoints(app) { [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (_request, response) => { try { - const users = (await User.where()).map((user) => { - const { password, ...rest } = user; - return rest; - }); + const users = await User.where(); response.status(200).json({ users }); } catch (e) { console.error(e); diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js index 228777ab5..95b8e7916 100644 --- a/server/endpoints/api/admin/index.js +++ b/server/endpoints/api/admin/index.js @@ -73,10 +73,7 @@ function apiAdminEndpoints(app) { return; } - const users = (await User.where()).map((user) => { - const { password, ...rest } = user; - return rest; - }); + const users = await User.where(); response.status(200).json({ users }); } catch (e) { console.error(e); diff --git a/server/endpoints/system.js b/server/endpoints/system.js index e197da43f..43053a5a6 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -115,7 +115,7 @@ function systemEndpoints(app) { if (await SystemSettings.isMultiUserMode()) { const { username, password } = reqBody(request); - const existingUser = await User.get({ username: String(username) }); + const existingUser = await User._get({ username: String(username) }); if (!existingUser) { await EventLogs.logEvent( @@ -193,7 +193,7 @@ function systemEndpoints(app) { // Return recovery codes to frontend response.status(200).json({ valid: true, - user: existingUser, + user: User.filterFields(existingUser), token: makeJWT( { id: existingUser.id, username: existingUser.username }, "30d" @@ -206,7 +206,7 @@ function systemEndpoints(app) { response.status(200).json({ valid: true, - user: existingUser, + user: User.filterFields(existingUser), token: makeJWT( { id: existingUser.id, username: existingUser.username }, "30d" @@ -1029,7 +1029,7 @@ function systemEndpoints(app) { const updates = {}; if (username) { - updates.username = String(username); + updates.username = User.validations.username(String(username)); } if (password) { updates.password = String(password); diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 81cbd6154..615716597 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -111,39 +111,45 @@ function workspaceEndpoints(app) { handleFileUpload, ], async function (request, response) { - const Collector = new CollectorApi(); - const { originalname } = request.file; - const processingOnline = await Collector.online(); + try { + const Collector = new CollectorApi(); + const { originalname } = request.file; + const processingOnline = await Collector.online(); - if (!processingOnline) { - response - .status(500) - .json({ - success: false, - error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`, - }) - .end(); - return; + if (!processingOnline) { + response + .status(500) + .json({ + success: false, + error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`, + }) + .end(); + return; + } + + const { success, reason } = + await Collector.processDocument(originalname); + if (!success) { + response.status(500).json({ success: false, error: reason }).end(); + return; + } + + Collector.log( + `Document ${originalname} uploaded processed and successfully. It is now available in documents.` + ); + await Telemetry.sendTelemetry("document_uploaded"); + await EventLogs.logEvent( + "document_uploaded", + { + documentName: originalname, + }, + response.locals?.user?.id + ); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); } - - const { success, reason } = await Collector.processDocument(originalname); - if (!success) { - response.status(500).json({ success: false, error: reason }).end(); - return; - } - - Collector.log( - `Document ${originalname} uploaded processed and successfully. It is now available in documents.` - ); - await Telemetry.sendTelemetry("document_uploaded"); - await EventLogs.logEvent( - "document_uploaded", - { - documentName: originalname, - }, - response.locals?.user?.id - ); - response.status(200).json({ success: true, error: null }); } ); @@ -151,37 +157,42 @@ function workspaceEndpoints(app) { "/workspace/:slug/upload-link", [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { - const Collector = new CollectorApi(); - const { link = "" } = reqBody(request); - const processingOnline = await Collector.online(); + try { + const Collector = new CollectorApi(); + const { link = "" } = reqBody(request); + const processingOnline = await Collector.online(); - if (!processingOnline) { - response - .status(500) - .json({ - success: false, - error: `Document processing API is not online. Link ${link} will not be processed automatically.`, - }) - .end(); - return; + if (!processingOnline) { + response + .status(500) + .json({ + success: false, + error: `Document processing API is not online. Link ${link} will not be processed automatically.`, + }) + .end(); + return; + } + + const { success, reason } = await Collector.processLink(link); + if (!success) { + response.status(500).json({ success: false, error: reason }).end(); + return; + } + + Collector.log( + `Link ${link} uploaded processed and successfully. It is now available in documents.` + ); + await Telemetry.sendTelemetry("link_uploaded"); + await EventLogs.logEvent( + "link_uploaded", + { link }, + response.locals?.user?.id + ); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); } - - const { success, reason } = await Collector.processLink(link); - if (!success) { - response.status(500).json({ success: false, error: reason }).end(); - return; - } - - Collector.log( - `Link ${link} uploaded processed and successfully. It is now available in documents.` - ); - await Telemetry.sendTelemetry("link_uploaded"); - await EventLogs.logEvent( - "link_uploaded", - { link }, - response.locals?.user?.id - ); - response.status(200).json({ success: true, error: null }); } ); diff --git a/server/models/user.js b/server/models/user.js index ecb620ee4..f08548afb 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -10,6 +10,20 @@ const User = { "role", "suspended", ], + validations: { + username: (newValue = "") => { + try { + if (String(newValue).length > 100) + throw new Error("Username cannot be longer than 100 characters"); + if (String(newValue).length < 2) + throw new Error("Username must be at least 2 characters"); + return String(newValue); + } catch (e) { + throw new Error(e.message); + } + }, + }, + // validations for the above writable fields. castColumnValue: function (key, value) { switch (key) { @@ -19,6 +33,12 @@ const User = { return String(value); } }, + + filterFields: function (user = {}) { + const { password, ...rest } = user; + return { ...rest }; + }, + create: async function ({ username, password, role = "default" }) { const passwordCheck = this.checkPasswordComplexity(password); if (!passwordCheck.checkedOK) { @@ -30,12 +50,12 @@ const User = { const hashedPassword = bcrypt.hashSync(password, 10); const user = await prisma.users.create({ data: { - username, + username: this.validations.username(username), password: hashedPassword, - role, + role: String(role), }, }); - return { user, error: null }; + return { user: this.filterFields(user), error: null }; } catch (error) { console.error("FAILED TO CREATE USER.", error.message); return { user: null, error: error.message }; @@ -69,7 +89,13 @@ const User = { // and force-casts to the proper type; Object.entries(updates).forEach(([key, value]) => { if (this.writable.includes(key)) { - updates[key] = this.castColumnValue(key, value); + if (this.validations.hasOwnProperty(key)) { + updates[key] = this.validations[key]( + this.castColumnValue(key, value) + ); + } else { + updates[key] = this.castColumnValue(key, value); + } return; } delete updates[key]; @@ -127,6 +153,17 @@ const User = { }, get: async function (clause = {}) { + try { + const user = await prisma.users.findFirst({ where: clause }); + return user ? this.filterFields({ ...user }) : null; + } catch (error) { + console.error(error.message); + return null; + } + }, + + // Returns user object with all fields + _get: async function (clause = {}) { try { const user = await prisma.users.findFirst({ where: clause }); return user ? { ...user } : null; @@ -162,7 +199,7 @@ const User = { where: clause, ...(limit !== null ? { take: limit } : {}), }); - return users; + return users.map((usr) => this.filterFields(usr)); } catch (error) { console.error(error.message); return []; diff --git a/server/utils/EmbeddingEngines/voyageAi/index.js b/server/utils/EmbeddingEngines/voyageAi/index.js index fe2a39643..65126613b 100644 --- a/server/utils/EmbeddingEngines/voyageAi/index.js +++ b/server/utils/EmbeddingEngines/voyageAi/index.js @@ -38,7 +38,10 @@ class VoyageAiEmbedder { Array.isArray(textInput) ? textInput : [textInput], { modelName: this.model } ); - return result || []; + + // If given an array return the native Array[Array] format since that should be the outcome. + // But if given a single string, we need to flatten it so that we have a 1D array. + return (Array.isArray(textInput) ? result : result.flat()) || []; } async embedChunks(textChunks = []) { @@ -50,6 +53,12 @@ class VoyageAiEmbedder { return embeddings; } catch (error) { console.error("Voyage AI Failed to embed:", error); + if ( + error.message.includes( + "Cannot read properties of undefined (reading '0')" + ) + ) + throw new Error("Voyage AI failed to embed: Rate limit reached"); throw error; } }