Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 17 additions & 8 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
POSTGRES_DATABASE=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DATABASE_URL=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
SES_EMAIL_FROM=
ENV=development # set ENV=production in prod

POSTGRES_DATABASE=llsc
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/llsc
POSTGRES_TEST_DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/llsc_test

FIREBASE_WEB_API_KEY=

AWS_ACCESS_KEY=
AWS_SECRET_KEY=
AWS_REGION=ca-central-1
SES_SOURCE_EMAIL=
SES_SOURCE_EMAIL_FR=

FRONTEND_URL=http://localhost:3000
17 changes: 17 additions & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ENV=development # set ENV=production in prod

POSTGRES_DATABASE=llsc
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/llsc
POSTGRES_TEST_DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/llsc_test

FIREBASE_WEB_API_KEY=

AWS_ACCESS_KEY=
AWS_SECRET_KEY=
AWS_REGION=ca-central-1
SES_SOURCE_EMAIL=
SES_SOURCE_EMAIL_FR=

FRONTEND_URL=http://localhost:3000
2 changes: 1 addition & 1 deletion backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create_app():
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
os.getenv("FRONTEND_URL", "http://localhost:3000"),
"https://uw-blueprint-starter-code.firebaseapp.com",
"https://uw-blueprint-starter-code.web.app",
# TODO: create a separate middleware function to dynamically
Expand Down
12 changes: 9 additions & 3 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@
# TODO: ADD RATE LIMITING
@router.post("/register", response_model=UserCreateResponse)
async def register_user(user: UserCreateRequest, user_service: UserService = Depends(get_user_service)):
allowed_Admins = [
allowed_admins = {
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
}
if user.role == UserRole.ADMIN:
if user.email not in allowed_Admins:
normalized_email = user.email.lower() if user.email else ""
if normalized_email not in allowed_admins:
raise HTTPException(status_code=403, detail="Access denied. Admin privileges required for admin portal")

try:
Expand Down
20 changes: 18 additions & 2 deletions backend/app/seeds/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,40 @@ def seed_database(verbose: bool = True) -> None:
Args:
verbose: Whether to print detailed output
"""
# Check environment to determine if we should seed test data
env = os.getenv("ENV", "development").lower()
is_production = env == "production"

if verbose:
print("🌱 Starting database seeding...")
if is_production:
print("⚠️ Production mode: Skipping test data (Users, Ranking Preferences)")
else:
print(f"🔧 {env.capitalize()} mode: Including test data")

session = get_database_session()

try:
# Run all seed functions in dependency order
# Reference data - always seed these
seed_functions = [
("Roles", seed_roles),
("Treatments", seed_treatments),
("Experiences", seed_experiences),
("Qualities", seed_qualities),
("Forms", seed_forms),
("Users", seed_users),
("Ranking Preferences", seed_ranking_preferences),
("Match Status", seed_match_status),
]

# Test data - only seed in non-production environments
if not is_production:
seed_functions.extend(
[
("Users", seed_users),
("Ranking Preferences", seed_ranking_preferences),
]
)

for name, seed_func in seed_functions:
if verbose:
print(f"\n📦 Seeding {name}...")
Expand Down
6 changes: 3 additions & 3 deletions backend/app/server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
from contextlib import asynccontextmanager
from typing import Union

Expand Down Expand Up @@ -87,7 +88,7 @@ async def lifespan(_: FastAPI):

# Shutdown scheduler gracefully
log.info("Shutting down scheduler...")
scheduler.shutdown()
scheduler.shutdown(wait=False) # Don't wait for running jobs to prevent interpreter shutdown race condition

# Dispose database engine to close all connection pools
# This prevents async generator cleanup errors during shutdown
Expand All @@ -104,8 +105,7 @@ async def lifespan(_: FastAPI):
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:3002",
os.getenv("FRONTEND_URL", "http://localhost:3000"),
"https://uw-blueprint-starter-code.firebaseapp.com",
"https://uw-blueprint-starter-code.web.app",
# TODO: create a separate middleware function to dynamically
Expand Down
4 changes: 2 additions & 2 deletions backend/app/services/implementations/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def reset_password(self, email: str) -> None:

# Use Firebase Admin SDK to generate password reset link
action_code_settings = firebase_admin.auth.ActionCodeSettings(
url="http://localhost:3000/set-new-password",
url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/set-new-password",
handle_code_in_app=True,
)

Expand Down Expand Up @@ -152,7 +152,7 @@ def send_email_verification_link(self, email: str, language: str = None) -> None

# Use Firebase Admin SDK to generate email verification link
action_code_settings = firebase_admin.auth.ActionCodeSettings(
url="http://localhost:3000/action", # URL to redirect after verification
url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/action", # URL to redirect after verification
handle_code_in_app=True,
)

Expand Down
15 changes: 8 additions & 7 deletions backend/app/services/implementations/match_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
from datetime import date, datetime, timedelta, timezone
from typing import List, Optional
from uuid import UUID
Expand Down Expand Up @@ -106,7 +107,7 @@ async def create_matches(self, req: MatchCreateRequest) -> MatchCreateResponse:
language = volunteer.language.value if volunteer.language else "en"

first_name = volunteer.first_name if volunteer.first_name else None
matches_url = "http://localhost:3000/volunteer/dashboard"
matches_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/volunteer/dashboard"

ses_service.send_matches_available_email(
to_email=volunteer.email,
Expand Down Expand Up @@ -342,7 +343,7 @@ async def schedule_match(
time=participant_time_str,
timezone=participant_tz_abbr,
first_name=participant.first_name,
scheduled_calls_url="http://localhost:3000/participant/dashboard",
scheduled_calls_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard",
language=participant_language,
)

Expand All @@ -357,7 +358,7 @@ async def schedule_match(
time=volunteer_time_str,
timezone=volunteer_tz_abbr,
first_name=volunteer.first_name,
scheduled_calls_url="http://localhost:3000/volunteer/dashboard",
scheduled_calls_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/volunteer/dashboard",
language=volunteer_language,
)

Expand Down Expand Up @@ -446,7 +447,7 @@ async def request_new_times(
to_email=volunteer.email,
participant_name=participant_name,
first_name=volunteer.first_name,
matches_url="http://localhost:3000/volunteer/dashboard",
matches_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/volunteer/dashboard",
language=volunteer_language,
)
except Exception as e:
Expand Down Expand Up @@ -532,7 +533,7 @@ async def cancel_match_by_participant(
time=volunteer_time_str,
timezone=volunteer_tz_abbr,
first_name=volunteer.first_name,
dashboard_url="http://localhost:3000/volunteer/dashboard",
dashboard_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/volunteer/dashboard",
language=volunteer_language,
)
except Exception as e:
Expand Down Expand Up @@ -623,7 +624,7 @@ async def cancel_match_by_volunteer(
time=participant_time_str,
timezone=participant_tz_abbr,
first_name=participant.first_name,
request_matches_url="http://localhost:3000/participant/dashboard",
request_matches_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard",
language=participant_language,
)
except Exception as e:
Expand Down Expand Up @@ -829,7 +830,7 @@ async def volunteer_accept_match(
language = participant.language.value if participant.language else "en"

first_name = participant.first_name if participant.first_name else None
matches_url = "http://localhost:3000/participant/dashboard"
matches_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard"

ses_service = SESEmailService()
ses_service.send_matches_available_email(
Expand Down
12 changes: 6 additions & 6 deletions backend/app/utilities/ses_email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def send_matches_available_email(

# Default to dashboard if no specific URL provided
if not matches_url:
matches_url = "http://localhost:3000/participant/dashboard"
matches_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard"

template_data = {"first_name": first_name if first_name else "there", "matches_url": matches_url}

Expand Down Expand Up @@ -260,7 +260,7 @@ def send_call_scheduled_email(

# Default to dashboard if no specific URL provided
if not scheduled_calls_url:
scheduled_calls_url = "http://localhost:3000/participant/dashboard"
scheduled_calls_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard"

template_data = {
"first_name": first_name if first_name else "there",
Expand Down Expand Up @@ -304,7 +304,7 @@ def send_participant_requested_new_times_email(

# Default to dashboard if no specific URL provided
if not matches_url:
matches_url = "http://localhost:3000/volunteer/dashboard"
matches_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/volunteer/dashboard"

template_data = {
"first_name": first_name if first_name else "there",
Expand Down Expand Up @@ -351,7 +351,7 @@ def send_volunteer_accepted_new_times_email(

# Default to dashboard if no specific URL provided
if not scheduled_calls_url:
scheduled_calls_url = "http://localhost:3000/participant/dashboard"
scheduled_calls_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard"

template_data = {
"first_name": first_name if first_name else "there",
Expand Down Expand Up @@ -401,7 +401,7 @@ def send_participant_cancelled_email(

# Default to dashboard if no specific URL provided
if not dashboard_url:
dashboard_url = "http://localhost:3000/volunteer/dashboard"
dashboard_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/volunteer/dashboard"

template_data = {
"first_name": first_name if first_name else "there",
Expand Down Expand Up @@ -451,7 +451,7 @@ def send_volunteer_cancelled_email(

# Default to dashboard if no specific URL provided
if not request_matches_url:
request_matches_url = "http://localhost:3000/participant/dashboard"
request_matches_url = f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/participant/dashboard"

template_data = {
"first_name": first_name if first_name else "there",
Expand Down
11 changes: 11 additions & 0 deletions frontend/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
POSTGRES_DATABASE=llsc
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/llsc

NEXT_PUBLIC_FIREBASE_WEB_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=

REACT_APP_BACKEND_URL=http://localhost:8080
23 changes: 3 additions & 20 deletions frontend/src/hooks/useEmailVerification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import baseAPIClient from '@/APIClients/baseAPIClient';
import { detectUserLanguage } from '@/utils/languageDetection';

export const useEmailVerification = () => {
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -12,26 +13,8 @@ export const useEmailVerification = () => {
setSuccess(false);

try {
// Get browser language from navigator - check all languages in preference order
let detectedLang = 'en';

// Check navigator.languages array first (user's preferred languages in order)
if (navigator.languages && navigator.languages.length > 0) {
for (const lang of navigator.languages) {
const langCode = lang.split('-')[0].toLowerCase();
if (langCode === 'fr') {
detectedLang = 'fr';
break;
} else if (langCode === 'en') {
detectedLang = 'en';
// Continue checking in case French comes later
}
}
} else if (navigator.language) {
// Fallback to navigator.language
const langCode = navigator.language.split('-')[0].toLowerCase();
detectedLang = langCode === 'fr' ? 'fr' : 'en';
}
// Detect user's preferred language from browser settings
const detectedLang = detectUserLanguage();

await baseAPIClient.post(
`/auth/send-email-verification/${encodeURIComponent(email)}?language=${detectedLang}`,
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/pages/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import { AuthenticatedUser, FormStatus, UserRole } from '@/types/authTypes';
import { roleIdToUserRole } from '@/utils/roleUtils';
import { getRedirectRoute } from '@/constants/formStatusRoutes';
import { AuthPageLayout } from '@/components/layout';
import { detectUserLanguage, getProgramInfoUrl } from '@/utils/languageDetection';

export default function WelcomePage() {
const router = useRouter();
const [currentUser, setCurrentUser] = useState<AuthenticatedUser>(null);
const [loading, setLoading] = useState(true);
const [userLanguage, setUserLanguage] = useState<'en' | 'fr'>('en');

// Detect user's preferred language on client-side only
useEffect(() => {
setUserLanguage(detectUserLanguage());
}, []);

useEffect(() => {
const evaluate = async () => {
Expand Down Expand Up @@ -107,7 +114,9 @@ export default function WelcomePage() {
<Text color="brand.navy" fontSize="md">
You can learn more about the program{' '}
<a
href="#"
href={getProgramInfoUrl(userLanguage)}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--chakra-colors-brand-primary)', textDecoration: 'underline' }}
>
here
Expand Down
Loading