From 86f85761275f33059554d0f2536e315eec6d829a Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Thu, 20 Jul 2023 15:23:04 -0700 Subject: [PATCH 1/2] Enable the system owner to be able to update the system wide password and secret --- frontend/src/components/Modals/Password.jsx | 3 +- .../Settings/PasswordProtection/index.jsx | 308 ++++++++++++++++++ .../src/components/Modals/Settings/index.jsx | 13 +- frontend/src/models/system.js | 12 + server/endpoints/system.js | 15 + server/utils/helpers/updateENV.js | 10 +- server/utils/http/index.js | 10 +- server/utils/middleware/validatedRequest.js | 2 +- 8 files changed, 362 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/Modals/Settings/PasswordProtection/index.jsx diff --git a/frontend/src/components/Modals/Password.jsx b/frontend/src/components/Modals/Password.jsx index 30c628fbb6b..aab38edcaec 100644 --- a/frontend/src/components/Modals/Password.jsx +++ b/frontend/src/components/Modals/Password.jsx @@ -85,7 +85,8 @@ export function usePasswordModal() { useEffect(() => { async function checkAuthReq() { if (!window) return; - if (import.meta.env.DEV) { + if (import.meta.env.DEV && false) { + // TODO: REMOVE setRequiresAuth(false); } else { const currentToken = window.localStorage.getItem( 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 00000000000..1d7b7ba1d55 --- /dev/null +++ b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx @@ -0,0 +1,308 @@ +import React, { useState, useEffect } from "react"; +import { Loader } from "react-feather"; +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 && ( +
+ + +
+ )} + +
+
+ + {/*
+
+ + + +
+
*/} +
+ )} +
+
+ +
+
+
+ ); +} + +function ShowKey({ name, env, value, valid, allowDebug = true }) { + const [isValid, setIsValid] = useState(valid); + const [debug, setDebug] = useState(false); + const [saving, setSaving] = useState(false); + const handleSubmit = async (e) => { + e.preventDefault(); + setSaving(true); + const data = {}; + const form = new FormData(e.target); + for (var [key, value] of form.entries()) data[key] = value; + const { newValues, error } = await System.updateSystem(data); + if (!!error) { + alert(error); + setSaving(false); + setIsValid(false); + return; + } + + setSaving(false); + setDebug(false); + setIsValid(true); + }; + + if (!isValid) { + return ( +
+
+ + +
+

+ Need setup in .env file. +

+ {allowDebug && ( + <> + {debug ? ( +
+ {saving ? ( + <> + + + ) : ( + <> + + + + )} +
+ ) : ( + + )} + + )} +
+
+
+ ); + } + + return ( +
+
+ + + {allowDebug && ( +
+ {debug ? ( +
+ {saving ? ( + <> + + + ) : ( + <> + + + + )} +
+ ) : ( + + )} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index baa46df4a44..b22ae01ffdf 100644 --- a/frontend/src/components/Modals/Settings/index.jsx +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -1,16 +1,18 @@ 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; export default function SystemSettingsModal({ hideModal = noop }) { - const [selectedTab, setSelectedTab] = useState("keys"); + const [selectedTab, setSelectedTab] = useState("password"); const Component = TABS[selectedTab || "keys"]; return ( @@ -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 06ca2b92c18..7e5f61bfdd4 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 5193539ca6d..a39ef3a3b14 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 4161aec13af..54eec1e5bce 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 af42f5de519..9fd643b7563 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 = { diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js index 4e7c519a81d..89733b6ccad 100644 --- a/server/utils/middleware/validatedRequest.js +++ b/server/utils/middleware/validatedRequest.js @@ -4,7 +4,7 @@ function validatedRequest(request, response, next) { // When in development passthrough auth token for ease of development. // Or if the user simply did not set an Auth token or JWT Secret if ( - process.env.NODE_ENV === "development" || + // process.env.NODE_ENV === "development" || // TODO: UNCOMMENT !process.env.AUTH_TOKEN || !process.env.JWT_SECRET ) { From 5c697be0c5885eec7c04217e1dc79fe6613506f2 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Thu, 20 Jul 2023 15:25:18 -0700 Subject: [PATCH 2/2] lint and cleanup --- frontend/src/components/Modals/Password.jsx | 3 +- .../Settings/PasswordProtection/index.jsx | 167 ------------------ .../src/components/Modals/Settings/index.jsx | 2 +- server/utils/middleware/validatedRequest.js | 2 +- 4 files changed, 3 insertions(+), 171 deletions(-) diff --git a/frontend/src/components/Modals/Password.jsx b/frontend/src/components/Modals/Password.jsx index aab38edcaec..30c628fbb6b 100644 --- a/frontend/src/components/Modals/Password.jsx +++ b/frontend/src/components/Modals/Password.jsx @@ -85,8 +85,7 @@ export function usePasswordModal() { useEffect(() => { async function checkAuthReq() { if (!window) return; - if (import.meta.env.DEV && false) { - // TODO: REMOVE + if (import.meta.env.DEV) { setRequiresAuth(false); } else { const currentToken = window.localStorage.getItem( diff --git a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx index 1d7b7ba1d55..2b6444edba7 100644 --- a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx +++ b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import { Loader } from "react-feather"; import System from "../../../../models/system"; const noop = () => false; @@ -124,25 +123,6 @@ export default function PasswordProtection({ hideModal = noop }) { - - {/*
-
- - - -
-
*/} )} @@ -159,150 +139,3 @@ export default function PasswordProtection({ hideModal = noop }) { ); } - -function ShowKey({ name, env, value, valid, allowDebug = true }) { - const [isValid, setIsValid] = useState(valid); - const [debug, setDebug] = useState(false); - const [saving, setSaving] = useState(false); - const handleSubmit = async (e) => { - e.preventDefault(); - setSaving(true); - const data = {}; - const form = new FormData(e.target); - for (var [key, value] of form.entries()) data[key] = value; - const { newValues, error } = await System.updateSystem(data); - if (!!error) { - alert(error); - setSaving(false); - setIsValid(false); - return; - } - - setSaving(false); - setDebug(false); - setIsValid(true); - }; - - if (!isValid) { - return ( -
-
- - -
-

- Need setup in .env file. -

- {allowDebug && ( - <> - {debug ? ( -
- {saving ? ( - <> - - - ) : ( - <> - - - - )} -
- ) : ( - - )} - - )} -
-
-
- ); - } - - return ( -
-
- - - {allowDebug && ( -
- {debug ? ( -
- {saving ? ( - <> - - - ) : ( - <> - - - - )} -
- ) : ( - - )} -
- )} -
-
- ); -} diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index b22ae01ffdf..e898a7afa0e 100644 --- a/frontend/src/components/Modals/Settings/index.jsx +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -12,7 +12,7 @@ const TABS = { const noop = () => false; export default function SystemSettingsModal({ hideModal = noop }) { - const [selectedTab, setSelectedTab] = useState("password"); + const [selectedTab, setSelectedTab] = useState("keys"); const Component = TABS[selectedTab || "keys"]; return ( diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js index 89733b6ccad..4e7c519a81d 100644 --- a/server/utils/middleware/validatedRequest.js +++ b/server/utils/middleware/validatedRequest.js @@ -4,7 +4,7 @@ function validatedRequest(request, response, next) { // When in development passthrough auth token for ease of development. // Or if the user simply did not set an Auth token or JWT Secret if ( - // process.env.NODE_ENV === "development" || // TODO: UNCOMMENT + process.env.NODE_ENV === "development" || !process.env.AUTH_TOKEN || !process.env.JWT_SECRET ) {