diff --git a/server/models/user.js b/server/models/user.js index c447950c..ecb620ee 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -2,6 +2,23 @@ const prisma = require("../utils/prisma"); const { EventLogs } = require("./eventLogs"); const User = { + writable: [ + // Used for generic updates so we can validate keys in request body + "username", + "password", + "pfpFilename", + "role", + "suspended", + ], + // validations for the above writable fields. + castColumnValue: function (key, value) { + switch (key) { + case "suspended": + return Number(Boolean(value)); + default: + return String(value); + } + }, create: async function ({ username, password, role = "default" }) { const passwordCheck = this.checkPasswordComplexity(password); if (!passwordCheck.checkedOK) { @@ -42,13 +59,26 @@ const User = { update: async function (userId, updates = {}) { try { + if (!userId) throw new Error("No user id provided for update"); const currentUser = await prisma.users.findUnique({ where: { id: parseInt(userId) }, }); - if (!currentUser) { - return { success: false, error: "User not found" }; - } + if (!currentUser) return { success: false, error: "User not found" }; + // Removes non-writable fields for generic updates + // and force-casts to the proper type; + Object.entries(updates).forEach(([key, value]) => { + if (this.writable.includes(key)) { + updates[key] = this.castColumnValue(key, value); + return; + } + delete updates[key]; + }); + + if (Object.keys(updates).length === 0) + return { success: false, error: "No valid updates applied." }; + + // Handle password specific updates if (updates.hasOwnProperty("password")) { const passwordCheck = this.checkPasswordComplexity(updates.password); if (!passwordCheck.checkedOK) { @@ -78,6 +108,24 @@ const User = { } }, + // Explicit direct update of user object. + // Only use this method when directly setting a key value + // that takes no user input for the keys being modified. + _update: async function (id = null, data = {}) { + if (!id) throw new Error("No user id provided for update"); + + try { + const user = await prisma.users.update({ + where: { id }, + data, + }); + return { user, message: null }; + } catch (error) { + console.error(error.message); + return { user: null, message: error.message }; + } + }, + get: async function (clause = {}) { try { const user = await prisma.users.findFirst({ where: clause }); diff --git a/server/utils/AiProviders/openRouter/index.js b/server/utils/AiProviders/openRouter/index.js index ac72b167..655839dc 100644 --- a/server/utils/AiProviders/openRouter/index.js +++ b/server/utils/AiProviders/openRouter/index.js @@ -296,7 +296,7 @@ class OpenRouterLLM { try { JSON.parse(message); validJSON = true; - } catch { } + } catch {} if (!validJSON) { // It can be possible that the chunk decoding is running away diff --git a/server/utils/PasswordRecovery/index.js b/server/utils/PasswordRecovery/index.js index fbcbe579..2383dd2c 100644 --- a/server/utils/PasswordRecovery/index.js +++ b/server/utils/PasswordRecovery/index.js @@ -22,7 +22,7 @@ async function generateRecoveryCodes(userId) { const { error } = await RecoveryCode.createMany(newRecoveryCodes); if (!!error) throw new Error(error); - const { success } = await User.update(userId, { + const { user: success } = await User._update(userId, { seen_recovery_codes: true, }); if (!success) throw new Error("Failed to generate user recovery codes!"); @@ -80,6 +80,11 @@ async function resetPassword(token, _newPassword = "", confirmPassword = "") { // JOI password rules will be enforced inside .update. const { error } = await User.update(resetToken.user_id, { password: newPassword, + }); + + // seen_recovery_codes is not publicly writable + // so we have to do direct update here + await User._update(resetToken.user_id, { seen_recovery_codes: false, });