Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
9 changes: 7 additions & 2 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,19 @@ async def logout(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
auth_service: AuthService = Depends(get_auth_service),
user_service: UserService = Depends(get_user_service),
):
try:
user_id = request.state.user_id
if not user_id:
auth_id = request.state.user_id # This is actually the Firebase auth_id
if not auth_id:
raise HTTPException(status_code=401, detail="Authentication required")

# Convert Firebase auth_id to database user_id (UUID)
user_id = await user_service.get_user_id_by_auth_id(auth_id)
auth_service.revoke_tokens(user_id)
return {"message": "Successfully logged out"}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

Expand Down
70 changes: 70 additions & 0 deletions backend/app/routes/contact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging

from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session

from app.middleware.auth import has_roles
from app.models import User
from app.schemas.contact import ContactRequest, ContactResponse
from app.schemas.user import UserRole
from app.utilities.constants import LOGGER_NAME
from app.utilities.db_utils import get_db

log = logging.getLogger(LOGGER_NAME("contact"))

router = APIRouter(
prefix="/contact",
tags=["contact"],
)


@router.post("/submit", response_model=ContactResponse)
async def submit_contact_form(
contact_data: ContactRequest,
request: Request,
db: Session = Depends(get_db),
authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.VOLUNTEER, UserRole.ADMIN]),
):
"""
Submit a contact form message from a user.

This endpoint receives contact form submissions from participants or volunteers
and sends the message to the admin team.

Args:
contact_data: The contact form data (name, email, message)
request: The FastAPI request object (contains user_id from auth middleware)
db: Database session

Returns:
ContactResponse with success status and message
"""
try:
# Get current user from auth middleware
current_user_auth_id = request.state.user_id
current_user = db.query(User).filter(User.auth_id == current_user_auth_id).first()

if not current_user:
raise HTTPException(status_code=401, detail="User not found")

# Log the contact form submission
log.info(
f"Contact form submission from user {current_user.id} "
f"(name: {contact_data.name}, email: {contact_data.email})"
)
log.info(f"Message: {contact_data.message}")

# TODO: Send email to admin team
# This will be implemented in a future update
# For now, we just log the message and return success

return ContactResponse(
success=True,
message="Your message has been sent successfully. A staff member will get back to you as soon as possible.",
)

except HTTPException:
raise
except Exception as e:
log.error(f"Error submitting contact form: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to submit contact form")
17 changes: 17 additions & 0 deletions backend/app/routes/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from app.middleware.auth import has_roles
from app.models import Experience, Treatment, User, UserData
from app.models.User import Language
from app.schemas.user import UserRole
from app.utilities.db_utils import get_db

Expand Down Expand Up @@ -62,6 +63,7 @@ class UserDataResponse(BaseModel):
has_kids: Optional[str] = None
other_ethnic_group: Optional[str] = None
gender_identity_custom: Optional[str] = None
timezone: Optional[str] = None

# Cancer Experience
diagnosis: Optional[str] = None
Expand Down Expand Up @@ -147,6 +149,7 @@ async def get_my_user_data(
has_kids=user_data.has_kids,
other_ethnic_group=user_data.other_ethnic_group,
gender_identity_custom=user_data.gender_identity_custom,
timezone=user_data.timezone,
# Cancer Experience
diagnosis=user_data.diagnosis,
date_of_diagnosis=user_data.date_of_diagnosis.isoformat() if user_data.date_of_diagnosis else None,
Expand Down Expand Up @@ -300,6 +303,19 @@ async def update_my_user_data(
if experience:
user_data.loved_one_experiences.append(experience)

# Update user language (stored on User model, not UserData)
if "language" in update_data:
try:
language_value = update_data["language"]
if language_value in ["en", "fr"]:
current_user.language = Language(language_value)
except (ValueError, AttributeError):
pass # Invalid language value, skip

# Update user timezone (stored on UserData model)
if "timezone" in update_data:
user_data.timezone = update_data["timezone"]

db.commit()
db.refresh(user_data)

Expand Down Expand Up @@ -330,6 +346,7 @@ async def update_my_user_data(
has_kids=user_data.has_kids,
other_ethnic_group=user_data.other_ethnic_group,
gender_identity_custom=user_data.gender_identity_custom,
timezone=user_data.timezone,
diagnosis=user_data.diagnosis,
date_of_diagnosis=user_data.date_of_diagnosis.isoformat() if user_data.date_of_diagnosis else None,
treatments=[treatment.name for treatment in user_data.treatments],
Expand Down
16 changes: 16 additions & 0 deletions backend/app/schemas/contact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pydantic import BaseModel


class ContactRequest(BaseModel):
"""Schema for contact form submission"""

name: str
email: str
message: str


class ContactResponse(BaseModel):
"""Schema for contact form response"""

success: bool
message: str
1 change: 1 addition & 0 deletions backend/app/schemas/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class MatchVolunteerSummary(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
email: str
phone: Optional[str] = None
pronouns: Optional[List[str]] = None
diagnosis: Optional[str] = None
age: Optional[int] = None
Expand Down
58 changes: 57 additions & 1 deletion backend/app/seeds/users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Seed users data for testing matching functionality."""

import uuid
from datetime import date
from datetime import date, time

from sqlalchemy import delete
from sqlalchemy.orm import Session
Expand Down Expand Up @@ -131,6 +131,7 @@ def seed_users(session: Session) -> None:
"date_of_diagnosis": date(2018, 4, 20), # Survivor
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"timezone": "EST",
},
"treatments": [
TreatmentId.CHEMOTHERAPY,
Expand All @@ -143,6 +144,12 @@ def seed_users(session: Session) -> None:
ExperienceId.FATIGUE,
ExperienceId.RETURNING_TO_WORK,
],
"volunteer_experience": "My journey with blood cancer started when I was about twelve years old and getting treatment for the first time was extremely stress-inducing. My journey with blood cancer started when I was about twelve years old and getting treatment for the first time was extremely stress-inducing.",
"availability_templates": [
{"day_of_week": 1, "start_time": time(14, 0), "end_time": time(16, 0)}, # Tuesday 2-4pm
{"day_of_week": 3, "start_time": time(14, 0), "end_time": time(17, 0)}, # Thursday 2-5pm
{"day_of_week": 4, "start_time": time(10, 0), "end_time": time(12, 0)}, # Friday 10am-12pm
],
},
{
"role": "volunteer",
Expand All @@ -165,9 +172,16 @@ def seed_users(session: Session) -> None:
"date_of_diagnosis": date(2020, 8, 15), # Survivor
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"timezone": "PST",
},
"treatments": [3, 6], # Chemotherapy, Radiation (matching Sarah's preferences)
"experiences": [1, 3, 4], # Brain Fog, Feeling Overwhelmed, Fatigue (same as Sarah!)
"volunteer_experience": "I was diagnosed with ALL at 34 and underwent intensive chemotherapy and radiation. The hardest part was balancing treatment with being a mother. I want to help others navigate this difficult journey and share what I learned.",
"availability_templates": [
{"day_of_week": 0, "start_time": time(16, 0), "end_time": time(18, 0)}, # Monday 4-6pm
{"day_of_week": 2, "start_time": time(11, 0), "end_time": time(13, 0)}, # Wednesday 11am-1pm
{"day_of_week": 4, "start_time": time(13, 0), "end_time": time(15, 0)}, # Friday 1-3pm
],
},
{
"role": "volunteer",
Expand All @@ -190,9 +204,16 @@ def seed_users(session: Session) -> None:
"date_of_diagnosis": date(2020, 2, 14),
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"timezone": "EST",
},
"treatments": [3, 6], # Chemotherapy, Radiation
"experiences": [10, 11, 7], # Anxiety/Depression, PTSD, Returning to work
"volunteer_experience": "Fighting Hodgkin Lymphoma taught me resilience I never knew I had. The mental health challenges were just as tough as the physical ones. I'm here to listen and support anyone going through similar struggles.",
"availability_templates": [
{"day_of_week": 1, "start_time": time(9, 0), "end_time": time(11, 0)}, # Tuesday 9-11am
{"day_of_week": 3, "start_time": time(14, 0), "end_time": time(16, 0)}, # Thursday 2-4pm
{"day_of_week": 5, "start_time": time(10, 0), "end_time": time(12, 0)}, # Saturday 10am-12pm
],
},
# High-matching volunteers for Sarah Johnson
{
Expand All @@ -216,9 +237,15 @@ def seed_users(session: Session) -> None:
"date_of_diagnosis": date(2019, 5, 10), # Survivor
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"timezone": "EST",
},
"treatments": [3, 6], # Chemotherapy, Radiation (matching Sarah's preferences)
"experiences": [1, 3, 4], # Brain Fog, Feeling Overwhelmed, Fatigue (same as Sarah!)
"volunteer_experience": "As a working mother of two, being diagnosed with leukemia turned my world upside down. Brain fog and fatigue made everyday tasks feel impossible. Now in remission, I'm passionate about helping others find their strength during treatment.",
"availability_templates": [
{"day_of_week": 2, "start_time": time(14, 0), "end_time": time(17, 0)}, # Wednesday 2-5pm
{"day_of_week": 3, "start_time": time(16, 0), "end_time": time(18, 0)}, # Thursday 4-6pm
],
},
{
"role": "volunteer",
Expand All @@ -241,9 +268,15 @@ def seed_users(session: Session) -> None:
"date_of_diagnosis": date(2021, 3, 18), # Survivor
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"timezone": "MST",
},
"treatments": [3, 6], # Chemotherapy, Radiation (matching Sarah's preferences)
"experiences": [1, 3, 4, 10], # Brain Fog, Feeling Overwhelmed, Fatigue, Anxiety/Depression
"volunteer_experience": "My cancer journey came with intense anxiety and depression alongside the physical symptoms. Through therapy and support groups, I learned to cope with the overwhelming emotions. I want to be that supportive voice for others facing the same battles.",
"availability_templates": [
{"day_of_week": 0, "start_time": time(10, 0), "end_time": time(12, 0)}, # Monday 10am-12pm
{"day_of_week": 4, "start_time": time(14, 30), "end_time": time(16, 30)}, # Friday 2:30-4:30pm
],
},
{
"role": "volunteer",
Expand All @@ -266,9 +299,16 @@ def seed_users(session: Session) -> None:
"date_of_diagnosis": date(2018, 9, 25), # Survivor
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"timezone": "CST",
},
"treatments": [3, 6, 7], # Chemotherapy, Radiation, Maintenance Chemo
"experiences": [1, 4, 5], # Brain Fog, Feeling Overwhelmed, Fatigue (same as Sarah!)
"volunteer_experience": "Seven years ago, ALL changed everything. The fatigue was relentless, and brain fog made me feel like a stranger in my own mind. But I made it through, and now I want to offer hope and practical advice to anyone starting this journey.",
"availability_templates": [
{"day_of_week": 1, "start_time": time(15, 0), "end_time": time(17, 0)}, # Tuesday 3-5pm
{"day_of_week": 3, "start_time": time(10, 0), "end_time": time(12, 0)}, # Thursday 10am-12pm
{"day_of_week": 5, "start_time": time(14, 0), "end_time": time(16, 0)}, # Saturday 2-4pm
],
},
# Test Case 3: Participant who is a caregiver wanting caregiver volunteers
{
Expand Down Expand Up @@ -434,6 +474,22 @@ def seed_users(session: Session) -> None:
)
session.add(volunteer_data)

# Add availability templates for volunteers (if specified)
# Skip [email protected] so they can set their own availability
if (
user_info.get("availability_templates")
and user_info["user_data"]["email"] != "[email protected]"
):
for template_info in user_info["availability_templates"]:
availability_template = AvailabilityTemplate(
user_id=user.id,
day_of_week=template_info["day_of_week"],
start_time=template_info["start_time"],
end_time=template_info["end_time"],
is_active=True,
)
session.add(availability_template)

created_users.append((user, user_info["role"]))
print(f"Added {user_info['role']}: {user.first_name} {user.last_name}")

Expand Down
2 changes: 2 additions & 0 deletions backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .routes import (
auth,
availability,
contact,
intake,
match,
matching,
Expand Down Expand Up @@ -90,6 +91,7 @@ async def lifespan(_: FastAPI):
app.include_router(send_email.router)
app.include_router(task.router)
app.include_router(test.router)
app.include_router(contact.router)


@app.get("/")
Expand Down
1 change: 1 addition & 0 deletions backend/app/services/email/amazon_ses_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ def get_email_service_provider() -> IEmailServiceProvider:
aws_secret_key=os.getenv("AWS_SECRET_KEY"),
region=os.getenv("AWS_REGION"),
source_email=os.getenv("SES_SOURCE_EMAIL"),
is_sandbox=os.getenv("IS_SANDBOX", "True").lower() == "true",
)
7 changes: 5 additions & 2 deletions backend/app/services/implementations/match_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ async def cancel_match_by_participant(
if acting_participant_id and match.participant_id != acting_participant_id:
raise HTTPException(status_code=403, detail="Cannot modify another participant's match")

self._clear_confirmed_time(match)
self._set_match_status(match, "cancelled_by_participant")
# Soft-delete the match when cancelled (cleans up time blocks and sets deleted_at)
self._delete_match(match)

self.db.flush()
self.db.commit()
Expand Down Expand Up @@ -665,6 +665,7 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
diagnosis = None
age: Optional[int] = None
timezone: Optional[str] = None
phone: Optional[str] = None
treatments: List[str] = []
experiences: List[str] = []
overview: Optional[str] = None
Expand All @@ -674,6 +675,7 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
pronouns = volunteer_data.pronouns
diagnosis = volunteer_data.diagnosis
timezone = volunteer_data.timezone
phone = volunteer_data.phone

if volunteer_data.date_of_birth:
age = self._calculate_age(volunteer_data.date_of_birth)
Expand All @@ -693,6 +695,7 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
first_name=volunteer.first_name,
last_name=volunteer.last_name,
email=volunteer.email,
phone=phone,
pronouns=pronouns,
diagnosis=diagnosis,
age=age,
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/APIClients/authAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { signOut as firebaseSignOut } from 'firebase/auth';
import {
AuthenticatedUser,
UserCreateResponse,
Expand Down Expand Up @@ -149,12 +150,20 @@ export const logout = async (): Promise<boolean> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(AUTHENTICATED_USER_KEY, 'accessToken')}`;

try {
// Call backend to revoke tokens
await baseAPIClient.post('/auth/logout', {}, { headers: { Authorization: bearerToken } });
localStorage.removeItem(AUTHENTICATED_USER_KEY);
return true;
} catch (error) {
console.error('Logout error:', error);
console.error('Backend logout error:', error);
return false;
} finally {
// Always clear localStorage and sign out from Firebase regardless of backend API success/failure
localStorage.removeItem(AUTHENTICATED_USER_KEY);
try {
await firebaseSignOut(auth);
} catch (firebaseError) {
console.error('Firebase sign out error:', firebaseError);
}
}
};

Expand Down
Loading
Loading