From 6b6007f9ad1a4ccb6429f9b8de622104cb8b12af Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Thu, 20 Jul 2023 15:25:47 -0700 Subject: [PATCH] Enable the system owner to be able to update the system wide password and secret (#156) * Enable the system owner to be able to update the system wide password and secret * lint and cleanup --- .../Settings/PasswordProtection/index.jsx | 141 ++++++++++++++++++ .../src/components/Modals/Settings/index.jsx | 11 +- frontend/src/models/system.js | 12 ++ server/endpoints/system.js | 15 ++ server/utils/helpers/updateENV.js | 10 +- server/utils/http/index.js | 10 +- 6 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/Modals/Settings/PasswordProtection/index.jsx diff --git a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx new file mode 100644 index 00000000..2b6444ed --- /dev/null +++ b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx @@ -0,0 +1,141 @@ +import React, { useState, useEffect } from "react"; +import System from "../../../../models/system"; + +const noop = () => false; +export default function PasswordProtection({ hideModal = noop }) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(null); + const [usePassword, setUsePassword] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setSaving(true); + setSuccess(false); + setError(null); + + const form = new FormData(e.target); + const data = { + usePassword, + newPassword: form.get("password"), + }; + + const { success, error } = await System.updateSystemPassword(data); + if (success) { + setSuccess(true); + setSaving(false); + setTimeout(() => { + window.localStorage.removeItem("anythingllm_authToken"); + window.location.reload(); + }, 2_000); + return; + } + + setError(error); + setSaving(false); + }; + + useEffect(() => { + async function fetchKeys() { + const settings = await System.keys(); + setUsePassword(settings?.RequiresAuth); + setLoading(false); + } + fetchKeys(); + }, []); + + return ( +
+
+
+

+ Protect your AnythingLLM instance with a password. If you forget + this there is no recovery method so ensure you save this password. +

+
+ {(error || success) && ( +
+ {error && ( +
+ {error} +
+ )} + {success && ( +
+ Your page will refresh in a few seconds. +
+ )} +
+ )} +
+ {loading ? ( +
+

+ loading system settings +

+
+ ) : ( +
+
+
+ + + +
+
+ {usePassword && ( +
+ + +
+ )} + +
+
+
+ )} +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index baa46df4..e898a7af 100644 --- a/frontend/src/components/Modals/Settings/index.jsx +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -1,11 +1,13 @@ import React, { useState } from "react"; -import { Archive, Cloud, Key, X } from "react-feather"; +import { Archive, Lock, Key, X } from "react-feather"; import SystemKeys from "./Keys"; import ExportOrImportData from "./ExportImport"; +import PasswordProtection from "./PasswordProtection"; const TABS = { keys: SystemKeys, exportimport: ExportOrImportData, + password: PasswordProtection, }; const noop = () => false; @@ -62,6 +64,13 @@ function SettingTabs({ selectedTab, changeTab }) { icon={} onClick={changeTab} /> + } + onClick={changeTab} + /> ); diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 06ca2b92..7e5f61bf 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -86,6 +86,18 @@ const System = { return { newValues: null, error: e.message }; }); }, + updateSystemPassword: async (data) => { + return await fetch(`${API_BASE}/system/update-password`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify(data), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, deleteDocument: async (name, meta) => { return await fetch(`${API_BASE}/system/remove-document`, { method: "DELETE", diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 5193539c..a39ef3a3 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -13,6 +13,7 @@ const { getVectorDbClass } = require("../utils/helpers"); const { updateENV } = require("../utils/helpers/updateENV"); const { reqBody, makeJWT } = require("../utils/http"); const { setupDataImports } = require("../utils/files/multer"); +const { v4 } = require("uuid"); const { handleImports } = setupDataImports(); function systemEndpoints(app) { @@ -155,6 +156,20 @@ function systemEndpoints(app) { } }); + app.post("/system/update-password", async (request, response) => { + try { + const { usePassword, newPassword } = reqBody(request); + const { error } = updateENV({ + AuthToken: usePassword ? newPassword : "", + JWTSecret: usePassword ? v4() : "", + }); + response.status(200).json({ success: !error, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + app.get("/system/data-export", async (_, response) => { try { const { filename, error } = await exportData(); diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 4161aec1..54eec1e5 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -27,9 +27,15 @@ const KEY_MAPPING = { envKey: "PINECONE_INDEX", checks: [], }, + AuthToken: { + envKey: "AUTH_TOKEN", + checks: [], + }, + JWTSecret: { + envKey: "JWT_SECRET", + checks: [], + }, // Not supported yet. - // 'AuthToken': 'AUTH_TOKEN', - // 'JWTSecret': 'JWT_SECRET', // 'StorageDir': 'STORAGE_DIR', }; diff --git a/server/utils/http/index.js b/server/utils/http/index.js index af42f5de..9fd643b7 100644 --- a/server/utils/http/index.js +++ b/server/utils/http/index.js @@ -2,7 +2,6 @@ process.env.NODE_ENV === "development" ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) : require("dotenv").config(); const JWT = require("jsonwebtoken"); -const SECRET = process.env.JWT_SECRET; function reqBody(request) { return typeof request.body === "string" @@ -15,15 +14,16 @@ function queryParams(request) { } function makeJWT(info = {}, expiry = "30d") { - if (!SECRET) throw new Error("Cannot create JWT as JWT_SECRET is unset."); - return JWT.sign(info, SECRET, { expiresIn: expiry }); + if (!process.env.JWT_SECRET) + throw new Error("Cannot create JWT as JWT_SECRET is unset."); + return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry }); } function decodeJWT(jwtToken) { try { - return JWT.verify(jwtToken, SECRET); + return JWT.verify(jwtToken, process.env.JWT_SECRET); } catch {} - return null; + return { p: null }; } module.exports = {