Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9c23c25
Add macOS Desktop Services Store file to gitignore
sahilds1 Feb 5, 2026
2d2ccd1
Fix duplicate healthcheck key for db service build
amahuli03 Feb 16, 2026
01ccf9a
Enhance input sanitization and normalize pronouns
AkhilRB0204 Feb 17, 2026
aa3efcd
Merge remote-tracking branch 'upstream/develop' into bugfix-duplicate…
amahuli03 Feb 17, 2026
b08152f
fix: changed link to direct to balancer github page
amahuli03 Feb 18, 2026
b94e998
Fix error 1, added unit tests and more logging
amahuli03 Feb 20, 2026
530b90a
Changed button text from "donate" to "Support Developoment"
amahuli03 Feb 23, 2026
c409689
Merge pull request #462 from amahuli03/bugfix-duplicate-healthchecks-…
sahilds1 Feb 23, 2026
98599e5
Merge pull request #465 from amahuli03/445-update-donate-button
sahilds1 Feb 23, 2026
f96606d
Fix 401 by using adminApi instead of raw axios
amahuli03 Feb 23, 2026
662f29d
Merge pull request #468 from amahuli03/467/unauthorized-pdf-upload
sahilds1 Feb 26, 2026
bbf1034
Fixed wrong API url path in handleDownload
amahuli03 Feb 26, 2026
128418b
Fixed API URL in handleOpen as well
amahuli03 Feb 26, 2026
332af9f
drf-spectacular configuration
amahuli03 Feb 26, 2026
a34a9f8
Added URL routes for API docs generation
amahuli03 Feb 26, 2026
fe660d2
Added OpenAPI security scheme
amahuli03 Feb 26, 2026
3c83abd
Added extend_schema and serializer_class to endpoints that drf-specta…
amahuli03 Feb 26, 2026
7085aa0
Requested changes: fix patch decorators to point to where openAI is u…
amahuli03 Mar 2, 2026
669f939
Merge remote-tracking branch 'upstream/develop' into 390-bugfix-uploa…
amahuli03 Mar 2, 2026
e6754df
Requested changes: added comments explaining title truncation
amahuli03 Mar 2, 2026
4b4d727
Fix mock setups to match how generate_title accesses title
amahuli03 Mar 2, 2026
a8f5d90
Merge pull request #466 from amahuli03/390-bugfix-upload-file-endpoint
sahilds1 Mar 5, 2026
f2f4274
Merge pull request #471 from amahuli03/file-download-error
sahilds1 Mar 5, 2026
e8b0fc1
fix: treat openAIServices.openAI() return value as string
amahuli03 Mar 5, 2026
e0b7c23
fix mock test setup to return string instead of mocked response object
amahuli03 Mar 6, 2026
d68fa62
fix to make test_falls_back_to_chatgpt_if_no_title_found more robust
amahuli03 Mar 6, 2026
4bae746
update documentation to include instructions about how to use the API…
amahuli03 Mar 10, 2026
6f0deed
update site links on README
amahuli03 Mar 10, 2026
6ae14fe
Merge pull request #479 from amahuli03/update-readme-site-link
sahilds1 Mar 10, 2026
9fa287b
Merge pull request #472 from amahuli03/460/generate-api-documentation
sahilds1 Mar 11, 2026
d0542df
Merge pull request #457 from sahilds1/456-update-gitignore
sahilds1 Mar 11, 2026
75c1a14
Merge pull request #474 from amahuli03/473/openai-title-generation-at…
sahilds1 Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
config/env/*
!config/env/*.example
.idea/
.idea/
.DS_Store
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ Each module contains:
- Auth endpoints via Djoser: `/auth/`
- JWT token lifetime: 60 minutes (access), 1 day (refresh)

#### API Documentation
- Auto-generated using **drf-spectacular** (OpenAPI 3.0)
- **Swagger UI**: `http://localhost:8000/api/docs/` — interactive API explorer
- **ReDoc**: `http://localhost:8000/api/redoc/` — readable reference docs
- **Raw schema**: `http://localhost:8000/api/schema/`
- Configuration in `SPECTACULAR_SETTINGS` in `settings.py`
- Views use `@extend_schema` decorators and `serializer_class` attributes for schema generation
- JWT auth is configured in the schema — use `JWT <token>` (not `Bearer`) in Swagger UI's Authorize dialog
- To document a new endpoint: add `serializer_class` to the view if it has one, or add `@extend_schema` with `inline_serializer` for views returning raw dicts

#### Key Data Models
- **Medication** (`api.views.listMeds.models`) - Medication catalog with benefits/risks
- **MedRule** (`api.models.model_medRule`) - Include/Exclude rules for medications based on patient history
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ for patients with bipolar disorder, helping them shorten their journey to stabil

## Usage

You can view the current build of the website here: [https://balancertestsite.com](https://balancertestsite.com/)
You can view the current build of the website here: [https://balancerproject.org/](https://balancerproject.org/)

## Contributing

Expand Down Expand Up @@ -53,7 +53,7 @@ The application supports connecting to PostgreSQL databases via:
See [Database Connection Documentation](./docs/DATABASE_CONNECTION.md) for detailed configuration.

**Local Development:**
- Download a sample of papers to upload from [https://balancertestsite.com](https://balancertestsite.com/)
- Download a sample of papers to upload from [https://balancerproject.org/](https://balancerproject.org/)
- The email and password of `pgAdmin` are specified in `balancer-main/docker-compose.yml`
- The first time you use `pgAdmin` after building the Docker containers you will need to register the server.
- The `Host name/address` is the Postgres server service name in the Docker Compose file
Expand All @@ -74,6 +74,23 @@ df = pd.read_sql(query, engine)
#### Django REST
- The email and password are set in `server/api/management/commands/createsu.py`

## API Documentation

Interactive API docs are auto-generated using [drf-spectacular](https://drf-spectacular.readthedocs.io/) and available at:

- **Swagger UI**: [http://localhost:8000/api/docs/](http://localhost:8000/api/docs/) — interactive explorer with "Try it out" functionality
- **ReDoc**: [http://localhost:8000/api/redoc/](http://localhost:8000/api/redoc/) — clean, readable reference docs
- **Raw schema**: [http://localhost:8000/api/schema/](http://localhost:8000/api/schema/) — OpenAPI 3.0 JSON/YAML

### Testing authenticated endpoints

Most endpoints require JWT authentication. To test them in Swagger UI:

1. **Get a token**: Find the `POST /auth/jwt/create/` endpoint in Swagger UI, click **Try it out**, enter an authorized `email` and `password`, and click **Execute**. Copy the `access` token from the response.
2. **Authorize**: Click the **Authorize** button (lock icon) at the top of the page. Enter `JWT <your-access-token>` in the value field. The prefix must be `JWT`, not `Bearer`.
3. **Test endpoints**: All subsequent requests will include your token. Use **Try it out** on any protected endpoint.
4. **Token refresh**: Access tokens expire after 60 minutes. Use `POST /auth/jwt/refresh/` with your `refresh` token, or repeat step 1.

## Architecture

The Balancer website is a Postgres, Django REST, and React project. The source code layout is:
Expand Down
5 changes: 0 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ services:
networks:
app_net:
ipv4_address: 192.168.0.2
healthcheck:
test: ["CMD-SHELL", "pg_isready -U balancer -d balancer_dev"]
interval: 5s
timeout: 5s
retries: 5

pgadmin:
image: dpage/pgadmin4
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ function Footer() {
>
Leave feedback
</Link>
<a href="https://www.flipcause.com/secure/cause_pdetails/MjMyMTIw"
<a href="https://github.com/CodeForPhilly/balancer-main"
target="_blank"
className="flex justify-center text-black hover:border-blue-600 hover:text-blue-600 hover:no-underline"
className="flex justify-center text-center text-black hover:border-blue-600 hover:text-blue-600 hover:no-underline"
>
Donate
Support Development
</a>
<Link
to="/help"
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ const Header: React.FC<LoginFormProps> = ({ isAuthenticated, isSuperuser }) => {
Leave Feedback
</Link>
<a
href="https://www.flipcause.com/secure/cause_pdetails/MjMyMTIw"
href="https://github.com/CodeForPhilly/balancer-main"
target="_blank"
className="header-nav-item"
>
Donate
Support Development
</a>
{isAuthenticated && isSuperuser && (
<div
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Header/MdNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ const MdNavBar = (props: LoginFormProps) => {
</Link>
</li>
<li className="border-b border-gray-300 p-4">
<a href="https://www.flipcause.com/secure/cause_pdetails/MjMyMTIw"
<a href="https://github.com/CodeForPhilly/balancer-main"
target="_blank"
className="mr-9 text-black hover:border-b-2 hover:border-blue-600 hover:text-black hover:no-underline"
>
Donate
Support Development
</a>
</li>
{isAuthenticated &&
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/About/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ function About() {
</div>
</div>
<div className="mb-20 mt-5 flex flex-row flex-wrap justify-center gap-4">
<a href="https://www.flipcause.com/secure/cause_pdetails/MjMyMTIw" target="_blank">
<a href="https://github.com/CodeForPhilly/balancer-main" target="_blank">
<button className="btnBlue transition-transform focus:outline-none focus:ring focus:ring-blue-200">
Donate
Support Development
</button>
</a>

Expand Down
9 changes: 2 additions & 7 deletions frontend/src/pages/DocumentManager/UploadFile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useRef } from "react";
import axios from "axios";
import { adminApi } from "../../api/apiClient";
import TypingAnimation from "../../components/Header/components/TypingAnimation.tsx";
import Layout from "../Layout/Layout.tsx";

Expand All @@ -22,14 +22,9 @@ const UploadFile: React.FC = () => {
formData.append("file", file);

try {
const response = await axios.post(
const response = await adminApi.post(
`/api/v1/api/uploadFile`,
formData,
{
headers: {
"Content-Type": "multipart/form-data"
},
}
);
console.log("File uploaded successfully", response.data);
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Files/ListOfFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({
const handleDownload = async (guid: string, fileName: string) => {
try {
setDownloading(guid);
const { data } = await publicApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'blob' });
const { data } = await publicApi.get(`/api/v1/api/uploadFile/${guid}`, { responseType: 'blob' });

const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
Expand All @@ -82,7 +82,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({
const handleOpen = async (guid: string) => {
try {
setOpening(guid);
const { data } = await publicApi.get(`/v1/api/uploadFile/${guid}`, { responseType: 'arraybuffer' });
const { data } = await publicApi.get(`/api/v1/api/uploadFile/${guid}`, { responseType: 'arraybuffer' });

const file = new Blob([data], { type: 'application/pdf' });
const fileURL = window.URL.createObjectURL(file);
Expand Down
3 changes: 3 additions & 0 deletions server/api/views/ai_promptStorage/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema
from .models import AI_PromptStorage
from .serializers import AI_PromptStorageSerializer


@extend_schema(request=AI_PromptStorageSerializer, responses={201: AI_PromptStorageSerializer})
@api_view(['POST'])
# @permission_classes([IsAuthenticated])
def store_prompt(request):
Expand All @@ -21,6 +23,7 @@ def store_prompt(request):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@extend_schema(responses={200: AI_PromptStorageSerializer(many=True)})
@api_view(['GET'])
def get_all_prompts(request):
"""
Expand Down
2 changes: 2 additions & 0 deletions server/api/views/ai_settings/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema
from .models import AI_Settings
from .serializers import AISettingsSerializer


@extend_schema(request=AISettingsSerializer, responses={200: AISettingsSerializer(many=True), 201: AISettingsSerializer})
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def settings_view(request):
Expand Down
62 changes: 56 additions & 6 deletions server/api/views/assistant/sanitizer.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,76 @@
import re
import logging

logger = logging.getLogger(__name__)
def sanitize_input(user_input:str) -> str:
"""
Sanitize user input to prevent injection attacks and remove unwanted characters.

Args:
user_input (str): The raw input string from the user.

Returns:
str: The sanitized input string.
"""
try:
# Remove any script tags
sanitized = re.sub(r'<script.*?>.*?</script>', '', user_input, flags=re.IGNORECASE)
# Remove any HTML tags
sanitized = user_input

# Remove any style tags
sanitized = re.sub(r'<style.*?>.*?</style>', '', sanitized, flags=re.IGNORECASE)

# Remove any HTML/script tags
sanitized = re.sub(r'<.*?>', '', sanitized)

# Remove Phone Numbers
sanitized = re.sub(r'\+?\d[\d -]{8,}\d', '[Phone Number]', sanitized)

# Remove Email Addresses
sanitized = re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '[Email Address]', sanitized)

# Remove Medical Record Numbers (simple pattern)
sanitized = re.sub(r'\bMRN[:\s]*\d+\b', '[Medical Record Number]', sanitized, flags=re.IGNORECASE)

# Normalize pronouns
sanitized = normalize_pronouns(sanitized)

# Escape special characters
sanitized = re.sub(r'["\'\\]', '', sanitized)
sanitized = re.sub(r'\s+', '', sanitized)

# Limit length to prevent buffer overflow attacks
max_length = 1000
max_length = 5000
if len(sanitized) > max_length:
sanitized = sanitized[:max_length]

return sanitized.strip()
except Exception as e:
logger.error(f"Error sanitizing input: {e}")
return ""
return ""

def normalize_pronouns(text:str) -> str:
"""
Normalize first and second person pronouns to third person clinical language.

Converts patient centric pronouns to a more neutral form.
Args:
text (str): The input text containing pronouns.
Returns:
str: The text with normalized pronouns.
"""
# Normalize first person possessives: I, me, my, mine -> the patient
text = re.sub(r'\bMy\b', 'The patient\'s', text)
text = re.sub(r'\bmy\b', 'the patient\'s', text)

# First person subject: I -> the patient
text = re.sub(r'\bI\b', 'the patient', text)

# First person object: me -> the patient
text = re.sub(r'\bme\b', 'the patient', text)

# First person reflexive: myself -> the patient
text = re.sub(r'\bmyself\b', 'the patient', text)

# Second person: you, your -> the clinician
text = re.sub(r'\bYour\b', 'the clinician', text)
return text


17 changes: 17 additions & 0 deletions server/api/views/assistant/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from rest_framework.permissions import AllowAny
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework import serializers as drf_serializers

from openai import OpenAI

Expand Down Expand Up @@ -113,6 +115,21 @@ def invoke_functions_from_response(
class Assistant(APIView):
permission_classes = [AllowAny]

@extend_schema(
request=inline_serializer(name='AssistantRequest', fields={
'message': drf_serializers.CharField(help_text='User message to send to the assistant'),
'previous_response_id': drf_serializers.CharField(required=False, allow_null=True, help_text='ID of previous response for conversation continuity'),
}),
responses={
200: inline_serializer(name='AssistantResponse', fields={
'response_output_text': drf_serializers.CharField(),
'final_response_id': drf_serializers.CharField(),
}),
500: inline_serializer(name='AssistantError', fields={
'error': drf_serializers.CharField(),
}),
}
)
def post(self, request):
try:
user = request.user
Expand Down
31 changes: 31 additions & 0 deletions server/api/views/conversations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from .models import Conversation, Message
from .serializers import ConversationSerializer
from ...services.tools.tools import tools, execute_tool
from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework import serializers as drf_serializers


@csrf_exempt
Expand Down Expand Up @@ -95,6 +97,21 @@ def destroy(self, request, *args, **kwargs):
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)

@extend_schema(
request=inline_serializer(name='ContinueConversationRequest', fields={
'message': drf_serializers.CharField(help_text='User message to continue the conversation'),
'page_context': drf_serializers.CharField(required=False, help_text='Optional page context'),
}),
responses={
200: inline_serializer(name='ContinueConversationResponse', fields={
'response': drf_serializers.CharField(),
'title': drf_serializers.CharField(),
}),
400: inline_serializer(name='ContinueConversationBadRequest', fields={
'error': drf_serializers.CharField(),
}),
}
)
@action(detail=True, methods=['post'])
def continue_conversation(self, request, pk=None):
conversation = self.get_object()
Expand Down Expand Up @@ -123,6 +140,20 @@ def continue_conversation(self, request, pk=None):

return Response({"response": chatgpt_response, "title": conversation.title})

@extend_schema(
request=inline_serializer(name='UpdateTitleRequest', fields={
'title': drf_serializers.CharField(help_text='New conversation title'),
}),
responses={
200: inline_serializer(name='UpdateTitleResponse', fields={
'status': drf_serializers.CharField(),
'title': drf_serializers.CharField(),
}),
400: inline_serializer(name='UpdateTitleBadRequest', fields={
'error': drf_serializers.CharField(),
}),
}
)
@action(detail=True, methods=['patch'])
def update_title(self, request, pk=None):
conversation = self.get_object()
Expand Down
Loading
Loading