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 0000000000000000000000000000000000000000..2b6444edba782db4af1fa979dd0b37f1df4ba47b --- /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 ( + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex items-start justify-between px-6 py-4"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + Protect your AnythingLLM instance with a password. If you forget + this there is no recovery method so ensure you save this password. + </p> + </div> + {(error || success) && ( + <div className="w-full flex px-6"> + {error && ( + <div className="w-full bg-red-300 text-red-800 font-semibold px-4 py-2 rounded-lg"> + {error} + </div> + )} + {success && ( + <div className="w-full bg-green-300 text-green-800 font-semibold px-4 py-2 rounded-lg"> + Your page will refresh in a few seconds. + </div> + )} + </div> + )} + <div className="p-6 space-y-6 flex h-full w-full"> + {loading ? ( + <div className="w-full h-full flex items-center justify-center"> + <p className="text-gray-800 dark:text-gray-200 text-base"> + loading system settings + </p> + </div> + ) : ( + <div className="w-full flex flex-col gap-y-4"> + <form onSubmit={handleSubmit}> + <div className=""> + <label className="mb-2.5 block font-medium text-black dark:text-white"> + Password Protect Instance + </label> + + <label className="relative inline-flex cursor-pointer items-center"> + <input + type="checkbox" + name="use_password" + onClick={() => setUsePassword(!usePassword)} + checked={usePassword} + className="peer sr-only pointer-events-none" + /> + <div className="pointer-events-none peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-green-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-stone-400 dark:peer-focus:ring-blue-800"></div> + </label> + </div> + <div className="w-full flex flex-col gap-y-2 my-2"> + {usePassword && ( + <div> + <label + htmlFor="password" + className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" + > + New Password + </label> + <input + name="password" + type="text" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + placeholder="Your Instance Password" + minLength={8} + required={true} + autoComplete="off" + /> + </div> + )} + <button + disabled={saving} + type="submit" + className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600" + > + {saving ? "Saving..." : "Save Changes"} + </button> + </div> + </form> + </div> + )} + </div> + <div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> + <button + onClick={hideModal} + type="button" + className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600" + > + Close + </button> + </div> + </div> + </div> + ); +} diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index baa46df4a44ff4df6bb7dc14315b458246b17f68..e898a7afa0ec7fccabe4f95b4941fe2093210d23 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={<Archive className="h-4 w-4 flex-shrink-0" />} onClick={changeTab} /> + <SettingTab + active={selectedTab === "password"} + displayName="Password Protection" + tabName="password" + icon={<Lock className="h-4 w-4 flex-shrink-0" />} + onClick={changeTab} + /> </ul> </div> ); diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 06ca2b92c1810170db51edf6b6e60bb0bec0e011..7e5f61bfdd4a638e53af7cf234ff3b00ac84f2e8 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 5193539ca6dc3432d1f55996b32eb158d64aa742..a39ef3a3b142edff67a68a33c04e501da4b25cf0 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 4161aec13af3ca941effe6d7f8a1b32c31dae60d..54eec1e5bcee1e79d4d7bd7b58eca10a7b4373ff 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 af42f5de51907916ba678d92588825560f58322b..9fd643b75634c67295d892eea153fd4eb4f9949f 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 = {