From 8c9c48ef1d5313e751eea1d83d85b0f6c5197cf3 Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Sun, 24 Aug 2025 13:53:38 -0400 Subject: [PATCH 01/10] feat: #309 - Add admin-only notice to login page --- frontend/src/pages/Login/LoginForm.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Login/LoginForm.tsx b/frontend/src/pages/Login/LoginForm.tsx index d4579ead..3c1bec01 100644 --- a/frontend/src/pages/Login/LoginForm.tsx +++ b/frontend/src/pages/Login/LoginForm.tsx @@ -1,6 +1,5 @@ import { useFormik } from "formik"; -// import { Link, useNavigate } from "react-router-dom"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { login, AppDispatch } from "../../services/actions/auth"; import { connect, useDispatch } from "react-redux"; import { RootState } from "../../services/actions/types"; @@ -59,11 +58,16 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) { onSubmit={handleSubmit} className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12" > -
+
{/* {errorMessage &&
{errorMessage}
} */}

Welcome

+ +
+

This login is for Code for Philly administrators. Providers can use all site features without logging in.

+ Return to Medication Suggester +
From a5965999280f94a6f3135ede2760607cb1b62738 Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Sun, 24 Aug 2025 14:06:13 -0400 Subject: [PATCH 02/10] feat: #309 - add icon to login notice; add line break to return link --- frontend/src/pages/Login/LoginForm.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Login/LoginForm.tsx b/frontend/src/pages/Login/LoginForm.tsx index 3c1bec01..7639b937 100644 --- a/frontend/src/pages/Login/LoginForm.tsx +++ b/frontend/src/pages/Login/LoginForm.tsx @@ -6,6 +6,7 @@ import { RootState } from "../../services/actions/types"; import { useState, useEffect } from "react"; import ErrorMessage from "../../components/ErrorMessage"; import LoadingSpinner from "../../components/LoadingSpinner/LoadingSpinner"; +import { FaExclamationTriangle } from "react-icons/fa"; interface LoginFormProps { isAuthenticated: boolean; @@ -64,9 +65,13 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) { Welcome -
-

This login is for Code for Philly administrators. Providers can use all site features without logging in.

- Return to Medication Suggester +
+
+ +
+
+

This login is for Code for Philly administrators. Providers can use all site features without logging in. Return to Homepage

+
From f81704fc0858f2da932ee44affd2007321dcaf57 Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Sun, 24 Aug 2025 14:08:26 -0400 Subject: [PATCH 03/10] feat: #309 - Remove extra padding above notice --- frontend/src/pages/Login/LoginForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Login/LoginForm.tsx b/frontend/src/pages/Login/LoginForm.tsx index 7639b937..3e3fd0f1 100644 --- a/frontend/src/pages/Login/LoginForm.tsx +++ b/frontend/src/pages/Login/LoginForm.tsx @@ -65,7 +65,7 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) { Welcome -
+
From 757e712ebd7daca39cf102abef537cb1d5ef03bd Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Sun, 24 Aug 2025 14:31:37 -0400 Subject: [PATCH 04/10] feat: #309 - Remove forced login; update UI elements to reflect public vs. admin login --- frontend/src/components/Header/Header.tsx | 34 ++------ .../components/Header/LoginMenuDropDown.tsx | 78 ------------------- frontend/src/pages/Layout/Layout.tsx | 7 -- .../src/pages/Layout/Layout_V2_Header.tsx | 9 --- frontend/src/pages/Login/LoginForm.tsx | 5 -- 5 files changed, 6 insertions(+), 127 deletions(-) delete mode 100644 frontend/src/components/Header/LoginMenuDropDown.tsx diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 32039605..50b14091 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -2,7 +2,6 @@ import { useState, useRef, useEffect, Fragment } from "react"; // import { useState, Fragment } from "react"; import accountLogo from "../../assets/account.svg"; import { Link, useNavigate, useLocation } from "react-router-dom"; -import LoginMenuDropDown from "./LoginMenuDropDown"; import "../../components/Header/header.css"; import Chat from "./Chat"; import { FeatureMenuDropDown } from "./FeatureMenuDropDown"; @@ -24,7 +23,6 @@ const Header: React.FC = ({ isAuthenticated, isSuperuser }) => { const dropdownRef = useRef(null); let delayTimeout: number | null = null; const [showChat, setShowChat] = useState(false); - const [showLoginMenu, setShowLoginMenu] = useState(false); const [redirect, setRedirect] = useState(false); const { setShowSummary, setEnterNewPatient, triggerFormReset, setIsEditing } = useGlobalContext(); @@ -36,19 +34,6 @@ const Header: React.FC = ({ isAuthenticated, isSuperuser }) => { setRedirect(false); }; - const guestLinks = () => ( - - ); - const authLinks = () => ( ); - const handleLoginMenu = () => { - setShowLoginMenu(!showLoginMenu); - }; - const handleMouseEnter = () => { if (delayTimeout !== null) { clearTimeout(delayTimeout); @@ -136,7 +117,7 @@ const Header: React.FC = ({ isAuthenticated, isSuperuser }) => { Balancer -
diff --git a/frontend/src/components/Header/LoginMenuDropDown.tsx b/frontend/src/components/Header/LoginMenuDropDown.tsx deleted file mode 100644 index 427fdf07..00000000 --- a/frontend/src/components/Header/LoginMenuDropDown.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import { classNames } from "../../utils/classNames"; - -interface LoginMenuDropDownProps { - showLoginMenu: boolean; - handleLoginMenu: () => void; -} - -const LoginMenuDropDown: React.FC = ({ - showLoginMenu, -}) => { - return ( - <> - - -
- - Balancer - - - -

- Balancer is an interactive and user-friendly research tool for bipolar - medications, powered by Code for Philly volunteers. -

-

- We built Balancer{" "} - - to improve the health and well-being of people with bipolar - disorder. - -

-

- Balancer is currently still being developed, so do not take any - information on the test site as actual medical advice. -

- - {/*

- You can log in or sign up for a Balancer account using your email, - gmail or Facebook account. -

*/} - - - - - {/* - - */} -
- - ); -}; - -const LoginMenu = ({ show }: { show: boolean }) => { - if (!show) return null; - - return
; -}; - -export default LoginMenuDropDown; diff --git a/frontend/src/pages/Layout/Layout.tsx b/frontend/src/pages/Layout/Layout.tsx index 3c12358b..afe880b8 100644 --- a/frontend/src/pages/Layout/Layout.tsx +++ b/frontend/src/pages/Layout/Layout.tsx @@ -2,7 +2,6 @@ import {ReactNode, useState, useEffect} from "react"; import Header from "../../components/Header/Header"; import Footer from "../../components/Footer/Footer"; -import LoginMenuDropDown from "../../components/Header/LoginMenuDropDown"; import {connect} from "react-redux"; import {useAuth} from "./authHooks.ts"; import {RootState} from "../../services/actions/types"; @@ -50,12 +49,6 @@ export const Layout = ({
- {!isAuthenticated && showLoginMenu && ( - - )}
{children}
diff --git a/frontend/src/pages/Layout/Layout_V2_Header.tsx b/frontend/src/pages/Layout/Layout_V2_Header.tsx index b510c62d..3c5b7318 100644 --- a/frontend/src/pages/Layout/Layout_V2_Header.tsx +++ b/frontend/src/pages/Layout/Layout_V2_Header.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import { Link, useLocation } from "react-router-dom"; -import LoginMenuDropDown from "../../components/Header/LoginMenuDropDown.tsx"; import { useAuth } from "./authHooks.ts"; import { useGlobalContext } from "../../../src/contexts/GlobalContext.tsx"; @@ -65,14 +64,6 @@ const Header: React.FC = ({ isAuthenticated }) => { )}
- {!isAuthenticated && showLoginMenu && ( -
- -
- )} ); }; diff --git a/frontend/src/pages/Login/LoginForm.tsx b/frontend/src/pages/Login/LoginForm.tsx index 3e3fd0f1..97bcdbe5 100644 --- a/frontend/src/pages/Login/LoginForm.tsx +++ b/frontend/src/pages/Login/LoginForm.tsx @@ -109,11 +109,6 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) {
- {/* - - */} From c9f3a319dbd514b29c1aa33a7a94e46db624e13c Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Sun, 24 Aug 2025 19:33:11 -0400 Subject: [PATCH 05/10] feat: #309 - Add new logout page; restyle admin dropdown; restyle logout button; fix css of mobile nav links; remove login sidenav --- .../components/Header/FeatureMenuDropDown.tsx | 10 ++--- frontend/src/components/Header/Header.tsx | 31 ++++---------- frontend/src/components/Header/MdNavBar.tsx | 26 ++++-------- frontend/src/components/Header/header.css | 4 +- frontend/src/pages/Layout/Layout.tsx | 31 ++------------ .../src/pages/Layout/Layout_V2_Header.tsx | 22 +--------- frontend/src/pages/Login/LoginForm.tsx | 2 +- frontend/src/pages/Logout/Logout.tsx | 42 +++++++++++++++++++ frontend/src/routes/routes.tsx | 5 +++ frontend/tailwind.config.js | 9 +++- 10 files changed, 83 insertions(+), 99 deletions(-) create mode 100644 frontend/src/pages/Logout/Logout.tsx diff --git a/frontend/src/components/Header/FeatureMenuDropDown.tsx b/frontend/src/components/Header/FeatureMenuDropDown.tsx index b1bbf03e..36d72792 100644 --- a/frontend/src/components/Header/FeatureMenuDropDown.tsx +++ b/frontend/src/components/Header/FeatureMenuDropDown.tsx @@ -4,13 +4,13 @@ export const FeatureMenuDropDown = () => { const location = useLocation(); const currentPath = location.pathname; return ( -
-
+
+
    Manage files -
    +
    Manage and chat with files
@@ -19,7 +19,7 @@ export const FeatureMenuDropDown = () => {
    Manage rules -
    +
    Manage list of rules
@@ -28,7 +28,7 @@ export const FeatureMenuDropDown = () => {
    Manage meds -
    +
    Manage list of meds
diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 50b14091..f8c40028 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,15 +1,12 @@ -import { useState, useRef, useEffect, Fragment } from "react"; -// import { useState, Fragment } from "react"; -import accountLogo from "../../assets/account.svg"; +import { useState, useRef, useEffect } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import "../../components/Header/header.css"; import Chat from "./Chat"; import { FeatureMenuDropDown } from "./FeatureMenuDropDown"; import MdNavBar from "./MdNavBar"; -import { connect, useDispatch } from "react-redux"; +import { connect } from "react-redux"; import { RootState } from "../../services/actions/types"; -import { logout, AppDispatch } from "../../services/actions/auth"; -import { HiChevronDown } from "react-icons/hi"; +import { FaChevronDown, FaSignOutAlt } from "react-icons/fa"; import { useGlobalContext } from "../../contexts/GlobalContext.tsx"; interface LoginFormProps { @@ -23,24 +20,14 @@ const Header: React.FC = ({ isAuthenticated, isSuperuser }) => { const dropdownRef = useRef(null); let delayTimeout: number | null = null; const [showChat, setShowChat] = useState(false); - const [redirect, setRedirect] = useState(false); const { setShowSummary, setEnterNewPatient, triggerFormReset, setIsEditing } = useGlobalContext(); - const dispatch = useDispatch(); - - const logout_user = () => { - dispatch(logout()); - setRedirect(false); - }; - const authLinks = () => ( - + + Sign Out + + ); const handleMouseEnter = () => { @@ -201,14 +188,12 @@ const Header: React.FC = ({ isAuthenticated, isSuperuser }) => { : "absolute ml-1.5 " }`} > - + {showFeaturesMenu && }
)} - - {redirect ? navigate("/") : } {isAuthenticated && ( diff --git a/frontend/src/components/Header/MdNavBar.tsx b/frontend/src/components/Header/MdNavBar.tsx index 926794cf..1ed2cd43 100644 --- a/frontend/src/components/Header/MdNavBar.tsx +++ b/frontend/src/components/Header/MdNavBar.tsx @@ -5,8 +5,6 @@ import Chat from "./Chat"; // import logo from "../../assets/balancer.png"; import closeLogo from "../../assets/close.svg"; import hamburgerLogo from "../../assets/hamburger.svg"; -import {useDispatch} from "react-redux"; -import {logout, AppDispatch} from "../../services/actions/auth"; interface LoginFormProps { isAuthenticated: boolean; @@ -22,13 +20,6 @@ const MdNavBar = (props: LoginFormProps) => { setNav(!nav); }; - const dispatch = useDispatch(); - - const logout_user = () => { - dispatch(logout()); - }; - - return (
{
  • Donate
  • {isAuthenticated && -
  • - - Sign Out - -
  • +
  • + Sign Out + +
  • }
    diff --git a/frontend/src/components/Header/header.css b/frontend/src/components/Header/header.css index 4b0f4a2c..c7e807b9 100644 --- a/frontend/src/components/Header/header.css +++ b/frontend/src/components/Header/header.css @@ -23,7 +23,7 @@ } .header-nav-item { - @apply text-black border-transparent border-b-2 hover:border-blue-600 hover:text-blue-600 hover:border-b-2 hover:border-blue-600; + @apply text-black border-transparent border-b-2 hover:cursor-pointer hover:border-blue-600 hover:text-blue-600 hover:border-b-2 hover:border-blue-600; } .header-nav-item.header-nav-item-selected { @@ -31,7 +31,7 @@ } .subheader-nav-item { - @apply cursor-pointer rounded-lg p-3 transition duration-300 hover:bg-gray-100; + @apply cursor-pointer p-3 transition duration-300 hover:bg-gray-200 border-b border-gray-200; } .subheader-nav-item.subheader-nav-item-selected { diff --git a/frontend/src/pages/Layout/Layout.tsx b/frontend/src/pages/Layout/Layout.tsx index afe880b8..84f9c215 100644 --- a/frontend/src/pages/Layout/Layout.tsx +++ b/frontend/src/pages/Layout/Layout.tsx @@ -1,11 +1,10 @@ // Layout.tsx -import {ReactNode, useState, useEffect} from "react"; +import {ReactNode} from "react"; import Header from "../../components/Header/Header"; import Footer from "../../components/Footer/Footer"; import {connect} from "react-redux"; import {useAuth} from "./authHooks.ts"; import {RootState} from "../../services/actions/types"; -import {useLocation} from "react-router-dom"; interface LayoutProps { children: ReactNode; @@ -16,32 +15,8 @@ interface LoginFormProps { } export const Layout = ({ - children, - isAuthenticated, - }: LayoutProps & LoginFormProps): JSX.Element => { - const [showLoginMenu, setShowLoginMenu] = useState(false); - const location = useLocation(); - - - useEffect(() => { - if (!isAuthenticated) { - if ( - location.pathname === "/login" || - location.pathname === "/resetpassword" || - location.pathname.includes("password") || - location.pathname.includes("reset") - ) { - setShowLoginMenu(false); - } else { - setShowLoginMenu(true); - } - } - }, [isAuthenticated, location.pathname]); - - const handleLoginMenu = () => { - setShowLoginMenu(!showLoginMenu); - }; - + children +}: LayoutProps & LoginFormProps): JSX.Element => { useAuth(); return (
    diff --git a/frontend/src/pages/Layout/Layout_V2_Header.tsx b/frontend/src/pages/Layout/Layout_V2_Header.tsx index 3c5b7318..3371cef5 100644 --- a/frontend/src/pages/Layout/Layout_V2_Header.tsx +++ b/frontend/src/pages/Layout/Layout_V2_Header.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from "react"; import { Link, useLocation } from "react-router-dom"; import { useAuth } from "./authHooks.ts"; import { useGlobalContext } from "../../../src/contexts/GlobalContext.tsx"; @@ -7,31 +6,12 @@ interface LoginFormProps { isAuthenticated: boolean; } -const Header: React.FC = ({ isAuthenticated }) => { - const [showLoginMenu, setShowLoginMenu] = useState(false); +const Header: React.FC = () => { const location = useLocation(); const { setShowMetaPanel } = useGlobalContext(); const isOnDrugSummaryPage = location.pathname.includes("/drugsummary"); - useEffect(() => { - // only show the login menu on non‑auth pages - if (!isAuthenticated) { - const path = location.pathname; - const isAuthPage = - path === "/login" || - path === "/resetpassword" || - path.includes("password") || - path.includes("reset"); - - setShowLoginMenu(!isAuthPage); - } - }, [isAuthenticated, location.pathname]); - - const handleLoginMenu = () => { - setShowLoginMenu((prev) => !prev); - }; - useAuth(); return ( diff --git a/frontend/src/pages/Login/LoginForm.tsx b/frontend/src/pages/Login/LoginForm.tsx index 97bcdbe5..ce28c62c 100644 --- a/frontend/src/pages/Login/LoginForm.tsx +++ b/frontend/src/pages/Login/LoginForm.tsx @@ -70,7 +70,7 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) {
    -

    This login is for Code for Philly administrators. Providers can use all site features without logging in. Return to Homepage

    +

    This login is for Code for Philly administrators. Providers can use all site features without logging in. Return to Homepage

    diff --git a/frontend/src/pages/Logout/Logout.tsx b/frontend/src/pages/Logout/Logout.tsx new file mode 100644 index 00000000..b09f0ca3 --- /dev/null +++ b/frontend/src/pages/Logout/Logout.tsx @@ -0,0 +1,42 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch } from "react-redux"; +import { logout, AppDispatch } from "../../services/actions/auth"; + +const LogoutPage = () => { + const navigate = useNavigate(); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(logout()); + + const timer = setTimeout(() => { + navigate('/'); + }, 3000); // Redirect after 3 seconds + + // Cleanup the timer on component unmount + return () => clearTimeout(timer); + }, [dispatch, navigate]); + + return ( +
    +
    +

    You’ve been logged out

    +
    +
    +
    +

    + Thank you for using Balancer. You'll be redirected to the homepage shortly. +

    + +
    +
    + ); +}; + +export default LogoutPage; diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx index 2e6273d4..f96f2574 100644 --- a/frontend/src/routes/routes.tsx +++ b/frontend/src/routes/routes.tsx @@ -1,6 +1,7 @@ import App from "../App"; import RouteError from "../pages/404/404.tsx"; import LoginForm from "../pages/Login/Login.tsx"; +import Logout from "../pages/Logout/Logout.tsx"; import AdminPortal from "../pages/AdminPortal/AdminPortal.tsx"; import ResetPassword from "../pages/Login/ResetPassword.tsx"; import ResetPasswordConfirm from "../pages/Login/ResetPasswordConfirm.tsx"; @@ -50,6 +51,10 @@ const routes = [ path: "login", element: , }, + { + path: "logout", + element: , + }, { path: "resetPassword", element: , diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index bcc1e693..4161a741 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -10,8 +10,15 @@ export default { lora: "'Lora', serif", 'quicksand': ['Quicksand', 'sans-serif'] }, + keyframes: { + 'loading': { + '0%': { left: '-40%' }, + '100%': { left: '100%' }, + }, + }, animation: { - 'pulse-bounce': 'pulse-bounce 2s infinite', // Adjust duration and iteration as needed + 'pulse-bounce': 'pulse-bounce 2s infinite', + 'loading': 'loading 3s infinite', }, plugins: [], }, From 28834711023046af4d938c6a7c9bd477e77b097f Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Sun, 24 Aug 2025 20:48:35 -0400 Subject: [PATCH 06/10] feat: #309 - Separate public and admin api calls --- frontend/src/api/apiClient.ts | 28 +++++++++++-------- frontend/src/pages/Files/ListOfFiles.tsx | 8 +++--- .../src/pages/ListMeds/useMedications.tsx | 4 +-- frontend/src/pages/ManageMeds/ManageMeds.tsx | 8 +++--- .../pages/PatientManager/NewPatientForm.tsx | 4 +-- .../src/pages/RulesManager/RulesManager.tsx | 4 +-- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 73b74caf..0b48496b 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -3,7 +3,11 @@ import { FormValues } from "../pages/Feedback/FeedbackForm"; import { Conversation } from "../components/Header/Chat"; const baseURL = import.meta.env.VITE_API_BASE_URL; -export const api = axios.create({ +export const publicApi = axios.create({ + baseURL +}); + +export const adminApi = axios.create({ baseURL, headers: { Authorization: `JWT ${localStorage.getItem("access")}`, @@ -11,7 +15,7 @@ export const api = axios.create({ }); // Request interceptor to set the Authorization header -api.interceptors.request.use( +adminApi.interceptors.request.use( (configuration) => { const token = localStorage.getItem("access"); if (token) { @@ -29,7 +33,7 @@ const handleSubmitFeedback = async ( message: FormValues["message"], ) => { try { - const response = await api.post(`/v1/api/feedback/`, { + const response = await publicApi.post(`/v1/api/feedback/`, { feedbacktype: feedbackType, name, email, @@ -45,7 +49,7 @@ const handleSubmitFeedback = async ( const handleSendDrugSummary = async (message: FormValues["message"], guid: string) => { try { const endpoint = guid ? `/v1/api/embeddings/ask_embeddings?guid=${guid}` : '/v1/api/embeddings/ask_embeddings'; - const response = await api.post(endpoint, { + const response = await adminApi.post(endpoint, { message, }); console.log("Response data:", JSON.stringify(response.data, null, 2)); @@ -58,7 +62,7 @@ const handleSendDrugSummary = async (message: FormValues["message"], guid: strin const handleRuleExtraction = async (guid: string) => { try { - const response = await api.get(`/v1/api/rule_extraction_openai?guid=${guid}`); + const response = await adminApi.get(`/v1/api/rule_extraction_openai?guid=${guid}`); // console.log("Rule extraction response:", JSON.stringify(response.data, null, 2)); return response.data; } catch (error) { @@ -69,7 +73,7 @@ const handleRuleExtraction = async (guid: string) => { const fetchRiskDataWithSources = async (medication: string, source: "include" | "diagnosis" = "include") => { try { - const response = await api.post(`/v1/api/riskWithSources`, { + const response = await adminApi.post(`/v1/api/riskWithSources`, { drug: medication, source: source, }); @@ -192,7 +196,7 @@ const handleSendDrugSummaryStreamLegacy = async ( const fetchConversations = async (): Promise => { try { - const response = await api.get(`/chatgpt/conversations/`); + const response = await publicApi.get(`/chatgpt/conversations/`); return response.data; } catch (error) { console.error("Error(s) during getConversations: ", error); @@ -202,7 +206,7 @@ const fetchConversations = async (): Promise => { const fetchConversation = async (id: string): Promise => { try { - const response = await api.get(`/chatgpt/conversations/${id}/`); + const response = await publicApi.get(`/chatgpt/conversations/${id}/`); return response.data; } catch (error) { console.error("Error(s) during getConversation: ", error); @@ -212,7 +216,7 @@ const fetchConversation = async (id: string): Promise => { const newConversation = async (): Promise => { try { - const response = await api.post(`/chatgpt/conversations/`, { + const response = await publicApi.post(`/chatgpt/conversations/`, { messages: [], }); return response.data; @@ -228,7 +232,7 @@ const continueConversation = async ( page_context?: string, ): Promise<{ response: string; title: Conversation["title"] }> => { try { - const response = await api.post( + const response = await publicApi.post( `/chatgpt/conversations/${id}/continue_conversation/`, { message, @@ -244,7 +248,7 @@ const continueConversation = async ( const deleteConversation = async (id: string) => { try { - const response = await api.delete(`/chatgpt/conversations/${id}/`); + const response = await publicApi.delete(`/chatgpt/conversations/${id}/`); return response.data; } catch (error) { console.error("Error(s) during deleteConversation: ", error); @@ -257,7 +261,7 @@ const updateConversationTitle = async ( newTitle: Conversation["title"], ): Promise<{status: string, title: Conversation["title"]} | {error: string}> => { try { - const response = await api.patch(`/chatgpt/conversations/${id}/update_title/`, { + const response = await publicApi.patch(`/chatgpt/conversations/${id}/update_title/`, { title: newTitle, }); return response.data; diff --git a/frontend/src/pages/Files/ListOfFiles.tsx b/frontend/src/pages/Files/ListOfFiles.tsx index b53874bf..2a579bdc 100644 --- a/frontend/src/pages/Files/ListOfFiles.tsx +++ b/frontend/src/pages/Files/ListOfFiles.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { api } from "../../api/apiClient"; +import { adminApi } from "../../api/apiClient"; import Layout from "../Layout/Layout"; import FileRow from "./FileRow"; import Table from "../../components/Table/Table"; @@ -37,7 +37,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ try { const url = `${baseUrl}/v1/api/uploadFile`; - const { data } = await api.get(url); + const { data } = await adminApi.get(url); if (Array.isArray(data)) { setFiles(data); @@ -63,7 +63,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ const handleDownload = async (guid: string, fileName: string) => { try { setDownloading(guid); - const { data } = await api.get(`/v1/api/uploadFile/${guid}`, { responseType: 'blob' }); + const { data } = await adminApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([data])); const link = document.createElement("a"); @@ -84,7 +84,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ const handleOpen = async (guid: string) => { try { setOpening(guid); - const { data } = await api.get(`/v1/api/uploadFile/${guid}`, { responseType: 'arraybuffer' }); + const { data } = await adminApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'arraybuffer' }); const file = new Blob([data], { type: 'application/pdf' }); const fileURL = window.URL.createObjectURL(file); diff --git a/frontend/src/pages/ListMeds/useMedications.tsx b/frontend/src/pages/ListMeds/useMedications.tsx index e15cc758..022eb07a 100644 --- a/frontend/src/pages/ListMeds/useMedications.tsx +++ b/frontend/src/pages/ListMeds/useMedications.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { api } from "../../api/apiClient"; +import { publicApi } from "../../api/apiClient"; export interface MedData { name: string; @@ -18,7 +18,7 @@ export function useMedications() { try { const url = `${baseUrl}/v1/api/get_full_list_med`; - const { data } = await api.get(url); + const { data } = await publicApi.get(url); data.sort((a: MedData, b: MedData) => { const nameA = a.name.toUpperCase(); diff --git a/frontend/src/pages/ManageMeds/ManageMeds.tsx b/frontend/src/pages/ManageMeds/ManageMeds.tsx index 071a2690..23493f7e 100644 --- a/frontend/src/pages/ManageMeds/ManageMeds.tsx +++ b/frontend/src/pages/ManageMeds/ManageMeds.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import Layout from "../Layout/Layout"; import Welcome from "../../components/Welcome/Welcome"; import ErrorMessage from "../../components/ErrorMessage"; -import { api } from "../../api/apiClient"; +import { adminApi } from "../../api/apiClient"; function ManageMedications() { interface MedData { id: string; @@ -23,7 +23,7 @@ function ManageMedications() { const fetchMedications = async () => { try { const url = `${baseUrl}/v1/api/get_full_list_med`; - const { data } = await api.get(url); + const { data } = await adminApi.get(url); data.sort((a: MedData, b: MedData) => a.name.localeCompare(b.name)); setMedications(data); } catch (e: unknown) { @@ -36,7 +36,7 @@ function ManageMedications() { // Handle Delete Medication const handleDelete = async (name: string) => { try { - await api.delete(`${baseUrl}/v1/api/delete_med`, { data: { name } }); + await adminApi.delete(`${baseUrl}/v1/api/delete_med`, { data: { name } }); setMedications((prev) => prev.filter((med) => med.name !== name)); setConfirmDelete(null); } catch (e: unknown) { @@ -56,7 +56,7 @@ function ManageMedications() { return; } try { - await api.post(`${baseUrl}/v1/api/add_medication`, { + await adminApi.post(`${baseUrl}/v1/api/add_medication`, { name: newMedName, benefits: newMedBenefits, risks: newMedRisks, diff --git a/frontend/src/pages/PatientManager/NewPatientForm.tsx b/frontend/src/pages/PatientManager/NewPatientForm.tsx index 774ebcb3..16143fdb 100644 --- a/frontend/src/pages/PatientManager/NewPatientForm.tsx +++ b/frontend/src/pages/PatientManager/NewPatientForm.tsx @@ -4,7 +4,7 @@ import { PatientInfo, Diagnosis } from "./PatientTypes"; import { useMedications } from "../ListMeds/useMedications"; import ChipsInput from "../../components/ChipsInput/ChipsInput"; import Tooltip from "../../components/Tooltip"; -import { api } from "../../api/apiClient"; +import { publicApi } from "../../api/apiClient"; import { useGlobalContext } from "../../contexts/GlobalContext.tsx"; // import ErrorMessage from "../../components/ErrorMessage"; @@ -155,7 +155,7 @@ const NewPatientForm = ({ const baseUrl = import.meta.env.VITE_API_BASE_URL; const url = `${baseUrl}/v1/api/get_med_recommend`; - const { data } = await api.post(url, payload); + const { data } = await publicApi.post(url, payload); const categorizedMedications = { first: data.first ?? [], diff --git a/frontend/src/pages/RulesManager/RulesManager.tsx b/frontend/src/pages/RulesManager/RulesManager.tsx index be4980d4..0268a4c8 100644 --- a/frontend/src/pages/RulesManager/RulesManager.tsx +++ b/frontend/src/pages/RulesManager/RulesManager.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import Layout from "../Layout/Layout"; import Welcome from "../../components/Welcome/Welcome"; import ErrorMessage from "../../components/ErrorMessage"; -import { api } from "../../api/apiClient"; +import { adminApi } from "../../api/apiClient"; import { ChevronDown, ChevronUp } from "lucide-react"; interface Medication { @@ -69,7 +69,7 @@ function RulesManager() { const fetchMedRules = async () => { try { const url = `${baseUrl}/v1/api/medRules`; - const { data } = await api.get(url); + const { data } = await adminApi.get(url); if (!data || !Array.isArray(data.results)) { throw new Error("Invalid response format"); From 8c4036eb3df48d6d30e6e34ac06503d5582a867a Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Mon, 25 Aug 2025 20:34:02 -0400 Subject: [PATCH 07/10] feat: #309 - Disable JWT auth for newly public endpoints --- frontend/src/api/apiClient.ts | 4 +--- server/api/views/conversations/views.py | 4 ++-- server/api/views/feedback/views.py | 4 +++- server/api/views/listMeds/views.py | 13 +++++++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 0b48496b..5e4a5eb6 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -3,9 +3,7 @@ import { FormValues } from "../pages/Feedback/FeedbackForm"; import { Conversation } from "../components/Header/Chat"; const baseURL = import.meta.env.VITE_API_BASE_URL; -export const publicApi = axios.create({ - baseURL -}); +export const publicApi = axios.create({ baseURL }); export const adminApi = axios.create({ baseURL, diff --git a/server/api/views/conversations/views.py b/server/api/views/conversations/views.py index d5921eaf..eeb68809 100644 --- a/server/api/views/conversations/views.py +++ b/server/api/views/conversations/views.py @@ -1,7 +1,7 @@ from rest_framework.response import Response from rest_framework import viewsets, status from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import AllowAny from rest_framework.exceptions import APIException from django.http import JsonResponse from bs4 import BeautifulSoup @@ -81,7 +81,7 @@ def __init__(self, detail=None, code=None): class ConversationViewSet(viewsets.ModelViewSet): serializer_class = ConversationSerializer - permission_classes = [IsAuthenticated] + permission_classes = [AllowAny] def get_queryset(self): return Conversation.objects.filter(user=self.request.user) diff --git a/server/api/views/feedback/views.py b/server/api/views/feedback/views.py index dcbef992..d0f0e1da 100644 --- a/server/api/views/feedback/views.py +++ b/server/api/views/feedback/views.py @@ -1,4 +1,4 @@ - +from rest_framework.permissions import AllowAny from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -8,6 +8,8 @@ class FeedbackView(APIView): + permission_classes = [AllowAny] + def post(self, request, *args, **kwargs): serializer = FeedbackSerializer(data=request.data) if serializer.is_valid(): diff --git a/server/api/views/listMeds/views.py b/server/api/views/listMeds/views.py index 796d9b17..d10a385a 100644 --- a/server/api/views/listMeds/views.py +++ b/server/api/views/listMeds/views.py @@ -1,4 +1,5 @@ from rest_framework import status +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView @@ -21,6 +22,8 @@ class GetMedication(APIView): + permission_classes = [AllowAny] + def post(self, request): data = request.data state_query = data.get('state', '') @@ -67,6 +70,8 @@ def post(self, request): class ListOrDetailMedication(APIView): + permission_classes = [AllowAny] + def get(self, request): name_query = request.query_params.get('name', None) if name_query: @@ -95,7 +100,7 @@ def post(self, request): name = data.get('name', '').strip() benefits = data.get('benefits', '').strip() risks = data.get('risks', '').strip() - + # Validate required fields if not name: return Response({'error': 'Medication name is required'}, status=status.HTTP_400_BAD_REQUEST) @@ -103,7 +108,7 @@ def post(self, request): return Response({'error': 'Medication benefits are required'}, status=status.HTTP_400_BAD_REQUEST) if not risks: return Response({'error': 'Medication risks are required'}, status=status.HTTP_400_BAD_REQUEST) - + # Check if medication already exists if Medication.objects.filter(name=name).exists(): return Response({'error': f'Medication "{name}" already exists'}, status=status.HTTP_400_BAD_REQUEST) @@ -123,11 +128,11 @@ class DeleteMedication(APIView): def delete(self, request): data = request.data name = data.get('name', '').strip() - + # Validate required fields if not name: return Response({'error': 'Medication name is required'}, status=status.HTTP_400_BAD_REQUEST) - + # Check if medication exists and delete try: medication = Medication.objects.get(name=name) From 3be42d380baf6a6de26dea9be9ccb028bdf4d3ec Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Mon, 25 Aug 2025 20:39:54 -0400 Subject: [PATCH 08/10] feat: #309 - Make riskWithSources endpoint public --- frontend/src/api/apiClient.ts | 2 +- server/api/views/risk/views_riskWithSources.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 5e4a5eb6..a1d32318 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -71,7 +71,7 @@ const handleRuleExtraction = async (guid: string) => { const fetchRiskDataWithSources = async (medication: string, source: "include" | "diagnosis" = "include") => { try { - const response = await adminApi.post(`/v1/api/riskWithSources`, { + const response = await publicApi.post(`/v1/api/riskWithSources`, { drug: medication, source: source, }); diff --git a/server/api/views/risk/views_riskWithSources.py b/server/api/views/risk/views_riskWithSources.py index d1c01615..94076c5c 100644 --- a/server/api/views/risk/views_riskWithSources.py +++ b/server/api/views/risk/views_riskWithSources.py @@ -1,6 +1,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status +from rest_framework.permissions import AllowAny from api.views.listMeds.models import Medication from api.models.model_medRule import MedRule, MedRuleSource import openai @@ -8,6 +9,8 @@ class RiskWithSourcesView(APIView): + permission_classes = [AllowAny] + def post(self, request): openai.api_key = os.environ.get("OPENAI_API_KEY") From 789442bc2ae3ff25954a44652e30d299cda62244 Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Tue, 26 Aug 2025 19:44:58 -0400 Subject: [PATCH 09/10] feat: #309 - Temporarily restrict chatbot to admins only --- frontend/src/api/apiClient.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index a1d32318..a594f921 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -214,7 +214,7 @@ const fetchConversation = async (id: string): Promise => { const newConversation = async (): Promise => { try { - const response = await publicApi.post(`/chatgpt/conversations/`, { + const response = await adminApi.post(`/chatgpt/conversations/`, { messages: [], }); return response.data; @@ -230,7 +230,7 @@ const continueConversation = async ( page_context?: string, ): Promise<{ response: string; title: Conversation["title"] }> => { try { - const response = await publicApi.post( + const response = await adminApi.post( `/chatgpt/conversations/${id}/continue_conversation/`, { message, @@ -246,7 +246,7 @@ const continueConversation = async ( const deleteConversation = async (id: string) => { try { - const response = await publicApi.delete(`/chatgpt/conversations/${id}/`); + const response = await adminApi.delete(`/chatgpt/conversations/${id}/`); return response.data; } catch (error) { console.error("Error(s) during deleteConversation: ", error); @@ -259,7 +259,7 @@ const updateConversationTitle = async ( newTitle: Conversation["title"], ): Promise<{status: string, title: Conversation["title"]} | {error: string}> => { try { - const response = await publicApi.patch(`/chatgpt/conversations/${id}/update_title/`, { + const response = await adminApi.patch(`/chatgpt/conversations/${id}/update_title/`, { title: newTitle, }); return response.data; From 0986a172df7fc3e0c72573d5edf5cd8384829f69 Mon Sep 17 00:00:00 2001 From: Gregg Stubberfield Date: Tue, 26 Aug 2025 20:43:18 -0400 Subject: [PATCH 10/10] feat: #309 - Allow viewing and downloading of uploads to be public; edits remain admin only --- .../src/pages/DocumentManager/UploadFile.tsx | 3 +-- frontend/src/pages/Files/ListOfFiles.tsx | 8 ++++---- .../src/pages/Layout/Layout_V2_Sidebar.tsx | 6 +----- server/api/views/uploadFile/views.py | 19 ++++++++----------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/frontend/src/pages/DocumentManager/UploadFile.tsx b/frontend/src/pages/DocumentManager/UploadFile.tsx index 35c4b84f..f3d0f477 100644 --- a/frontend/src/pages/DocumentManager/UploadFile.tsx +++ b/frontend/src/pages/DocumentManager/UploadFile.tsx @@ -28,8 +28,7 @@ const UploadFile: React.FC = () => { formData, { headers: { - "Content-Type": "multipart/form-data", - Authorization: `JWT ${localStorage.getItem("access")}`, // Assuming JWT is used for auth + "Content-Type": "multipart/form-data" }, } ); diff --git a/frontend/src/pages/Files/ListOfFiles.tsx b/frontend/src/pages/Files/ListOfFiles.tsx index 2a579bdc..efed19e5 100644 --- a/frontend/src/pages/Files/ListOfFiles.tsx +++ b/frontend/src/pages/Files/ListOfFiles.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { adminApi } from "../../api/apiClient"; +import { publicApi } from "../../api/apiClient"; import Layout from "../Layout/Layout"; import FileRow from "./FileRow"; import Table from "../../components/Table/Table"; @@ -37,7 +37,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ try { const url = `${baseUrl}/v1/api/uploadFile`; - const { data } = await adminApi.get(url); + const { data } = await publicApi.get(url); if (Array.isArray(data)) { setFiles(data); @@ -63,7 +63,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ const handleDownload = async (guid: string, fileName: string) => { try { setDownloading(guid); - const { data } = await adminApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'blob' }); + const { data } = await publicApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([data])); const link = document.createElement("a"); @@ -84,7 +84,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ const handleOpen = async (guid: string) => { try { setOpening(guid); - const { data } = await adminApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'arraybuffer' }); + const { data } = await publicApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'arraybuffer' }); const file = new Blob([data], { type: 'application/pdf' }); const fileURL = window.URL.createObjectURL(file); diff --git a/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx b/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx index 19163290..bec32d50 100644 --- a/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx +++ b/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx @@ -25,11 +25,7 @@ const Sidebar: React.FC = () => { const fetchFiles = async () => { try { const baseUrl = import.meta.env.VITE_API_BASE_URL; - const response = await axios.get(`${baseUrl}/v1/api/uploadFile`, { - headers: { - Authorization: `JWT ${localStorage.getItem("access")}`, - }, - }); + const response = await axios.get(`${baseUrl}/v1/api/uploadFile`); if (Array.isArray(response.data)) { setFiles(response.data); } diff --git a/server/api/views/uploadFile/views.py b/server/api/views/uploadFile/views.py index 8989dbc3..003e171e 100644 --- a/server/api/views/uploadFile/views.py +++ b/server/api/views/uploadFile/views.py @@ -1,5 +1,5 @@ from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework import status from rest_framework.generics import UpdateAPIView @@ -18,17 +18,15 @@ @method_decorator(csrf_exempt, name='dispatch') class UploadFileView(APIView): - permission_classes = [IsAuthenticated] + def get_permissions(self): + if self.request.method == 'GET': + return [AllowAny()] # Public access + return [IsAuthenticated()] # Auth required for other methods def get(self, request, format=None): print("UploadFileView, get list") - # Get the authenticated user - user = request.user - - # Filter the files uploaded by the authenticated user - files = UploadFile.objects.filter(uploaded_by=user.id).defer( - 'file').order_by('-date_of_upload') + files = UploadFile.objects.all().defer('file').order_by('-date_of_upload') serializer = UploadFileSerializer(files, many=True) return Response(serializer.data) @@ -160,12 +158,11 @@ def delete(self, request, format=None): @method_decorator(csrf_exempt, name='dispatch') class RetrieveUploadFileView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [AllowAny] def get(self, request, guid, format=None): try: - file = UploadFile.objects.get( - guid=guid, uploaded_by=request.user.id) + file = UploadFile.objects.get(guid=guid) response = HttpResponse(file.file, content_type='application/pdf') # print(file.file[:100]) response['Content-Disposition'] = f'attachment; filename="{file.file_name}"'