From 6b71c034c35b9ce206333776df1fd618ad10439d Mon Sep 17 00:00:00 2001 From: S-Shahla <143126171+S-Shahla@users.noreply.github.com> Date: Mon, 19 May 2025 23:18:55 -0700 Subject: [PATCH 01/33] Revert "Merge branch 'InboxSearchBar' into main" This reverts commit 3fb0ad250a839e88de5b57325d112a2c3ce8bf2c, reversing changes made to 113ed4558fd3f966d863cb684348e83671a7e91d. --- backend/app/routers/emails_router.py | 20 --- .../database/repositories/base_repository.py | 6 +- .../repositories/summary_repository.py | 27 +--- backend/app/services/email_service.py | 45 +----- .../src/components/client/inbox/inbox.jsx | 131 +++++------------- 5 files changed, 38 insertions(+), 191 deletions(-) diff --git a/backend/app/routers/emails_router.py b/backend/app/routers/emails_router.py index 30ffdc3..331042b 100644 --- a/backend/app/routers/emails_router.py +++ b/backend/app/routers/emails_router.py @@ -34,26 +34,6 @@ # Create module-specific logger logger = logging.getLogger(__name__) -@router.get("/search") -async def search_emails_by_keyword( - keyword: str, - email_service: EmailService = Depends(get_email_service), - user: UserSchema = Depends(get_current_user) -): - """ - Search emails using extracted summary keywords. - - Args: - keyword: Keyword to search for - email_service: Injected email service - user: Current user (from token) - - Returns: - List of matched emails based on summary keywords - """ - logger.info(f"Search endpoint hit with keyword: {keyword}") - return await email_service.search_emails_by_keyword(user.google_id, keyword) - @router.get( "/", response_model=EmailResponse, diff --git a/backend/app/services/database/repositories/base_repository.py b/backend/app/services/database/repositories/base_repository.py index a9a6a86..83f7280 100644 --- a/backend/app/services/database/repositories/base_repository.py +++ b/backend/app/services/database/repositories/base_repository.py @@ -131,8 +131,7 @@ async def find_many( query: Dict[str, Any], limit: int = 100, skip: int = 0, - sort: List[tuple] = None, - projection: Optional[Dict[str, int]] = None + sort: List[tuple] = None ) -> List[T]: """ Find multiple documents matching the query with pagination support. @@ -142,13 +141,12 @@ async def find_many( limit: Maximum number of documents to return skip: Number of documents to skip sort: List of (field, direction) tuples for sorting - projection: Dictionary specifying fields to include/exclude (e.g., {"email_id": 1}) Returns: List[T]: List of matching documents """ try: - cursor = self._get_collection().find(query, projection) if projection else self._get_collection().find(query) + cursor = self._get_collection().find(query) if sort: cursor = cursor.sort(sort) diff --git a/backend/app/services/database/repositories/summary_repository.py b/backend/app/services/database/repositories/summary_repository.py index c359749..9b778f5 100644 --- a/backend/app/services/database/repositories/summary_repository.py +++ b/backend/app/services/database/repositories/summary_repository.py @@ -59,28 +59,6 @@ async def find_by_google_id(self, google_id: str) -> List[SummarySchema]: """ return await self.find_many({"google_id": google_id}) - async def find_email_ids_by_keyword(self, google_id: str, keyword: str) -> List[str]: - """ - Search for emails using summary keywords. - - Args: - google_id: Google ID of the user. - keyword: Keyword to search in the summary keywords. - limit: Maximum number of emails to return. - - Returns: - List[EmailSchema]: List of emails whose summaries match the keyword. - """ - - query = { - "google_id": google_id, - "keywords": {"$regex": keyword, "$options": "i"} - } - raw_results = await self._get_collection().find(query, {"email_id": 1}).to_list(length=100) - - return [doc["email_id"] for doc in raw_results if "email_id" in doc] - - async def update_by_email_id( self, email_id: str, @@ -122,8 +100,7 @@ async def find_many( query: Dict[str, Any], limit: int = 100, skip: int = 0, - sort: List[tuple] = None, - projection: Optional[Dict[str, int]] = None + sort: List[tuple] = None ) -> List[SummarySchema]: """ Find multiple summaries matching the query. @@ -144,4 +121,4 @@ async def find_many( if isinstance(value, datetime): query["generated_at"][op] = value - return await super().find_many(query, limit, skip, sort, projection=projection) \ No newline at end of file + return await super().find_many(query, limit, skip, sort) \ No newline at end of file diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index f46d994..7cdccd2 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -16,7 +16,7 @@ # Import from app modules from app.models import EmailSchema, ReaderViewResponse -from app.services.database import EmailRepository, SummaryRepository, get_email_repository, get_summary_repository +from app.services.database import EmailRepository, get_email_repository from app.services.database.factories import get_user_service, get_auth_service from app.services import auth_service @@ -37,16 +37,14 @@ class EmailService: processing, and storage operations. """ - def __init__(self, email_repository: EmailRepository = None, summary_repository: SummaryRepository = None): + def __init__(self, email_repository: EmailRepository = None): """ Initialize the email service. Args: email_repository: Email repository instance - summary_repository: Summary repository instance """ self.email_repository = email_repository or get_email_repository() - self.summary_repository = summary_repository or get_summary_repository() self.imap_host = 'imap.gmail.com' self.default_email_account = os.environ.get("EMAIL_ACCOUNT") @@ -494,45 +492,6 @@ async def delete_email(self, email_id: str, google_id: str) -> bool: except Exception as e: self._handle_email_error(e, "delete", email_id, google_id) - async def search_emails_by_keyword(self, google_id: str, keyword: str, limit: int = 50) -> List[EmailSchema]: - """ - Search for emails using summary keywords. - - Args: - google_id: Google ID of the user. - keyword: Keyword to search in the summary keywords. - limit: Maximum number of emails to return. - - Returns: - List[EmailSchema]: List of emails whose summaries match the keyword and then enriched with corresponding summary. - """ - logger.info(f"[Keyword Search] google_id={google_id}, keyword='{keyword}'") - - try: - #Find all email_ids from summaries that match the keyword for the given user - email_ids = await self.summary_repository.find_email_ids_by_keyword(google_id, keyword) - if not email_ids: - return [] - - #Query emails from the email repository using those email_ids - query = {"google_id": google_id, "email_id": {"$in": [str(eid) for eid in email_ids]}} - emails = await self.email_repository.find_many(query, limit=limit) - - # Retrieve matching summary records to extract summary_text - summaries = await self.summary_repository.find_many(query) - summary_map = {s.email_id: getattr(s, "summary_text", "") for s in summaries} - - # Enrich email records with their corresponding summaries - enriched = [] - for e in emails: - base = e if isinstance(e, dict) else e.model_dump() - base["summary_text"] = summary_map.get(base["email_id"], "") - enriched.append(base) - - return enriched - - except Exception as e: - self._handle_email_error(e, "search by keyword", None, google_id) # ------------------------------------------------------------------------- # Content Processing Methods # ------------------------------------------------------------------------- diff --git a/frontend/src/components/client/inbox/inbox.jsx b/frontend/src/components/client/inbox/inbox.jsx index 9972b39..b2030f5 100644 --- a/frontend/src/components/client/inbox/inbox.jsx +++ b/frontend/src/components/client/inbox/inbox.jsx @@ -6,72 +6,37 @@ import EmailDisplay from "./emailDisplay"; import { getPageSummaries } from "../../../emails/emailHandler"; import "./emailEntry.css"; import "./emailList.css"; -import { baseUrl } from "../../../emails/emailHandler"; // shared API URL base function Inbox({ displaySummaries, emailList, setCurEmail, curEmail }) { - const [searchTerm, setSearchTerm] = useState(""); - const [filteredEmails, setFilteredEmails] = useState(emailList); - const token = localStorage.getItem("auth_token"); - - useEffect(() => { - setFilteredEmails(emailList); - }, [emailList]); - - // Search triggered only on Enter key - const handleSearchKeyDown = async (e) => { - if (e.key !== "Enter") return; - - if (searchTerm.trim() === "") { - setFilteredEmails(emailList); - return; - } - - try { - const res = await fetch(`${baseUrl}/emails/search?keyword=${searchTerm}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - if (res.ok) { - const data = await res.json(); - setFilteredEmails(Array.isArray(data) ? data : []); - } else { - console.error("Search failed", res.status); - } - } catch (err) { - console.error("Search error", err); - } - }; - return (
- +
); } function EmailEntry({ displaySummary, email, onClick, selected }) { const summary = () => { - if (email.summary_text?.length > 0) { - return
{email.summary_text}
; + let returnBlock; + if (email.summary_text.length > 0) { + returnBlock =
{email.summary_text}
; } else { - return
; + returnBlock =
; } + return returnBlock; }; - const date = getDate(email.received_at); return (
@@ -93,24 +58,23 @@ function EmailEntry({ displaySummary, email, onClick, selected }) { ); } -function InboxEmailList({ - displaySummaries, - emailList, - curEmail, - onClick, - searchTerm, - setSearchTerm, - handleSearchKeyDown, -}) { +function InboxEmailList({ displaySummaries, emailList, curEmail, onClick }) { const [pages, setPages] = useState(1); const ref = useRef(null); - const maxEmails = Math.min(pages * emailsPerPage, emailList.length); + const maxEmails = + pages * emailsPerPage < emailList.length + ? pages * emailsPerPage + : emailList.length; const hasUnloadedEmails = maxEmails < emailList.length; const handleScroll = () => { // add external summary call const fullyScrolled = - Math.abs(ref.current.scrollHeight - ref.current.clientHeight - ref.current.scrollTop) <= 1; + Math.abs( + ref.current.scrollHeight - + ref.current.clientHeight - + ref.current.scrollTop + ) <= 1; if (fullyScrolled && hasUnloadedEmails) { setPages(pages + 1); } @@ -142,54 +106,22 @@ function InboxEmailList({ }; return (
-
-
+
+
-
- Inbox -
+
Inbox
- - setSearchTerm(e.target.value)} - onKeyDown={handleSearchKeyDown} - style={{ - padding: "8px", - fontSize: "14px", - border: "1px solid #ccc", - borderRadius: "4px", - width: "280px", - }} - />
-
-
-
- {emails()} - {emailList.length === 0 && ( -
No matching emails found.
- )} -
+
+ {emails()}
); } -// PropTypes Inbox.propTypes = { displaySummaries: PropTypes.bool, emailList: PropTypes.array, @@ -209,13 +141,14 @@ InboxEmailList.propTypes = { emailList: PropTypes.array, curEmail: PropTypes.object, onClick: PropTypes.func, - searchTerm: PropTypes.string, - setSearchTerm: PropTypes.func, - handleSearchKeyDown: PropTypes.func, }; -// Utils -const getDate = (date) => `${date[1]}/${date[2]}/${date[0]}`; -const getSenderName = (sender) => sender.slice(0, sender.indexOf("<")); +const getDate = (date) => { + return `${date[1]}/${date[2]}/${date[0]}`; +}; + +const getSenderName = (sender) => { + return sender.slice(0, sender.indexOf("<")); +}; export default Inbox; From f37a46dbbccda55ac0b40b23ab2505d35b02fa0d Mon Sep 17 00:00:00 2001 From: Joseph Madigan Date: Sun, 25 May 2025 12:44:21 -0700 Subject: [PATCH 02/33] Refactor main API structure for consistent logging and dependency management. - Moved API route handlers to be defined before router inclusions for better organization. - Updated logging to use a consistent logger across various modules, replacing print statements with logger.debug and logger.error. - Removed redundant logging configurations from individual service files, centralizing it in main.py. - Cleaned up imports and improved code readability across multiple router files. --- backend/app/routers/auth_router.py | 49 ++++++------ backend/app/routers/emails_router.py | 17 +--- backend/app/routers/summaries_router.py | 13 +--- backend/app/routers/user_router.py | 78 +++++++++---------- backend/app/services/auth_service.py | 11 +-- backend/app/services/email_service.py | 15 ++-- .../services/summarization/summary_service.py | 12 +-- backend/app/services/user_service.py | 43 +++++----- backend/main.py | 37 ++++----- 9 files changed, 121 insertions(+), 154 deletions(-) diff --git a/backend/app/routers/auth_router.py b/backend/app/routers/auth_router.py index 47fac4b..1b25350 100644 --- a/backend/app/routers/auth_router.py +++ b/backend/app/routers/auth_router.py @@ -10,17 +10,15 @@ import urllib.parse import base64 import uuid -from typing import Dict, Optional, Any -from functools import lru_cache +import logging -from fastapi import APIRouter, HTTPException, status, Depends, Request, Query, Form -from fastapi.security import OAuth2AuthorizationCodeBearer, OAuth2PasswordBearer -from fastapi.responses import RedirectResponse, HTMLResponse, JSONResponse +from fastapi import APIRouter, HTTPException, status, Depends, Query, Form +from fastapi.security import OAuth2PasswordBearer +from fastapi.responses import RedirectResponse, HTMLResponse from google.auth.transport.requests import Request as GoogleRequest from google.oauth2.credentials import Credentials from starlette.concurrency import run_in_threadpool -from pydantic import BaseModel, EmailStr -from google_auth_oauthlib.flow import Flow +# from google_auth_oauthlib.flow import Flow from googleapiclient.discovery import build from app.services.auth_service import AuthService, SCOPES @@ -31,6 +29,7 @@ router = APIRouter() settings = get_settings() +logger = logging.getLogger(__name__) # -- Authentication Schemes -- @@ -71,10 +70,6 @@ async def get_current_user_email( # -- Endpoints -- -# Debugging helper function -def debug(message: str): - print(f"[DEBUG] {message}") - @router.get( "/login", summary="Start Google OAuth login flow", @@ -99,7 +94,7 @@ async def login( Returns: RedirectResponse: Redirects to Google's authentication page """ - debug(f"Login initiated - Redirect URI: {redirect_uri}") + logger.debug(f"Login initiated - Redirect URI: {redirect_uri}") try: # Create a state object that includes the frontend redirect URI @@ -115,13 +110,13 @@ async def login( result = auth_service.create_authorization_url(encoded_custom_state) authorization_url = result["authorization_url"] - debug(f"Generated Google OAuth URL: {authorization_url}") + logger.debug(f"Generated Google OAuth URL: {authorization_url}") # Now redirect to the correct URL return RedirectResponse(authorization_url) except Exception as e: - debug(f"[ERROR] Login failed: {str(e)}") + logger.error(f"[ERROR] Login failed: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create authorization URL: {str(e)}" @@ -146,7 +141,7 @@ async def callback( Returns: RedirectResponse: Redirects to frontend with authentication state """ - debug(f"Received callback with code: {code}") + logger.debug(f"Received callback with code: {code}") try: if not state: @@ -156,13 +151,13 @@ async def callback( decoded_state = json.loads(base64.urlsafe_b64decode(state).decode()) frontend_url = decoded_state.get("redirect_uri") - debug(f"Decoded state - Redirecting to frontend: {frontend_url}") + logger.debug(f"Decoded state - Redirecting to frontend: {frontend_url}") if not frontend_url: raise ValueError("Missing redirect URI in state parameter") # Exchange code for tokens and get user info in one step - debug("Exchanging code for tokens and getting user info...") + logger.debug("Exchanging code for tokens and getting user info...") token_data = await auth_service.get_tokens_from_code(code, None) # First exchange # Get user info using the token @@ -183,7 +178,7 @@ async def callback( ) user_email = user_info.get('email') - debug(f"User email retrieved: {user_email}") + logger.debug(f"User email retrieved: {user_email}") if not user_email: raise ValueError("Could not retrieve user email from Google") @@ -191,7 +186,7 @@ async def callback( # Check if user exists, create if not user = await user_service.get_user_by_email(user_email) if not user: - debug(f"Creating new user: {user_email}") + logger.debug(f"Creating new user: {user_email}") user = await user_service.create_user({ "email": user_email, "name": user_info.get("name", ""), @@ -199,7 +194,7 @@ async def callback( "google_id": user_info.get("id") }) else: - debug(f"Found existing user: {user_email}") + logger.debug(f"Found existing user: {user_email}") # Special handling for Swagger UI testing if "localhost:8000/docs" in frontend_url or "/docs" in frontend_url: @@ -239,7 +234,7 @@ async def exchange_code( Requires the user's email to associate the tokens. """ - debug(f"Exchanging OAuth code for user: {request.user_email}") + logger.debug(f"Exchanging OAuth code for user: {request.user_email}") try: if not request.code or not request.user_email: raise HTTPException( @@ -250,7 +245,7 @@ async def exchange_code( # Exchange auth code for tokens and store them in MongoDB tokens = await auth_service.get_tokens_from_code(request.code, request.user_email) - debug(f"Token exchange successful for {request.user_email}") + logger.debug(f"Token exchange successful for {request.user_email}") return TokenResponse( access_token=tokens.token, token_type="bearer", @@ -259,7 +254,7 @@ async def exchange_code( ) except Exception as e: - debug(f"[ERROR] Code exchange failed: {str(e)}") + logger.error(f"[ERROR] Code exchange failed: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Failed to exchange code for tokens: {str(e)}" @@ -331,14 +326,14 @@ async def auth_status( try: # Extract user info from the token user_data = await auth_service.get_credentials_from_token(token) - user_email = user_data['google_id'] + user_google_id = user_data['google_id'] - debug(f"User google_id extracted from token: {google_id}") + logger.debug(f"User google_id extracted from token: {user_google_id}") # Get detailed credentials from the database using that email try: # Get the token record directly from the database instead of using get_credentials - token_record = await auth_service.get_token_record(google_id) + token_record = await auth_service.get_token_record(user_google_id) if not token_record: return AuthStatusResponse( @@ -364,7 +359,7 @@ async def auth_status( except Exception as e: # Token validation failed - debug(f"[ERROR] Auth status check failed: {str(e)}") + logger.error(f"[ERROR] Auth status check failed: {str(e)}", exc_info=True) return AuthStatusResponse( is_authenticated=False, token_valid=False, diff --git a/backend/app/routers/emails_router.py b/backend/app/routers/emails_router.py index 331042b..a58b949 100644 --- a/backend/app/routers/emails_router.py +++ b/backend/app/routers/emails_router.py @@ -7,30 +7,17 @@ """ from fastapi import APIRouter, HTTPException, Query, Depends, status -from fastapi.security import OAuth2PasswordBearer -from typing import List, Optional -from pydantic import BaseModel +from typing import Optional import logging -from functools import lru_cache from app.models.email_models import EmailSchema, EmailResponse, ReaderViewResponse from app.models.user_models import UserSchema from app.routers.user_router import get_current_user -from app.services.database.factories import get_email_repository, get_email_service +from app.services.database.factories import get_email_service from app.services.email_service import EmailService router = APIRouter() -# Configure logging with format and level -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) - -# Add specific configuration for pymongo's logger -logging.getLogger('pymongo').setLevel(logging.WARNING) - # Create module-specific logger logger = logging.getLogger(__name__) diff --git a/backend/app/routers/summaries_router.py b/backend/app/routers/summaries_router.py index 7b4ac07..866dc50 100644 --- a/backend/app/routers/summaries_router.py +++ b/backend/app/routers/summaries_router.py @@ -7,13 +7,11 @@ """ import logging -from typing import List, Optional, Annotated +from typing import List from fastapi import APIRouter, HTTPException, Depends, Query, Path, status -from contextlib import asynccontextmanager -from app.utils.config import Settings, get_settings, SummarizerProvider from app.models import EmailSchema, SummarySchema, UserSchema -from app.services import EmailService, SummaryService +from app.services import SummaryService from app.services.summarization import get_summarizer from app.services.summarization.base import AdaptiveSummarizer from app.services.summarization import ( @@ -27,13 +25,6 @@ get_email_service ) -# Configure logging with format and level -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) - # Add specific configuration for pymongo's logger logging.getLogger('pymongo').setLevel(logging.WARNING) diff --git a/backend/app/routers/user_router.py b/backend/app/routers/user_router.py index 9363b8c..8aabb85 100644 --- a/backend/app/routers/user_router.py +++ b/backend/app/routers/user_router.py @@ -7,14 +7,7 @@ from fastapi import APIRouter, HTTPException, status, Depends from fastapi.security import OAuth2PasswordBearer -from fastapi.responses import JSONResponse -from functools import lru_cache -from typing import Optional, Dict, Any - -from google.auth.transport.requests import Request as GoogleRequest -from google.oauth2.credentials import Credentials -from googleapiclient.discovery import build -from starlette.concurrency import run_in_threadpool +import logging from app.models import UserSchema, PreferencesSchema from app.services.auth_service import AuthService @@ -22,14 +15,15 @@ from app.services.database.factories import get_user_service, get_auth_service router = APIRouter() +logger = logging.getLogger(__name__) # OAuth authentication scheme oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token", description="Enter the token you received from the login flow (without Bearer prefix)") # Debugging helper function -def debug(message: str): - """Print debug messages with a consistent format""" - print(f"[DEBUG] {message}") +# def debug(message: str): +# """Print debug messages with a consistent format""" +# print(f"[DEBUG] {message}") async def get_current_user_info( token: str = Depends(oauth2_scheme), @@ -48,14 +42,14 @@ async def get_current_user_info( Raises: HTTPException: 401 error if token is invalid """ - debug(f"Validating token for user authentication...") + logger.debug(f"Validating token for user authentication...") try: user_data = await auth_service.get_credentials_from_token(token) - debug(f"User authenticated successfully: {user_data.get('user_info', {}).get('email', 'Unknown')}") + logger.debug(f"User authenticated successfully: {user_data.get('user_info', {}).get('email', 'Unknown')}") return user_data except Exception as e: - debug(f"[ERROR] Authentication failed: {str(e)}") + logger.error(f"[ERROR] Authentication failed: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid authentication: {str(e)}" @@ -84,21 +78,21 @@ async def get_current_user( Raises: HTTPException: If user retrieval fails """ - debug("Retrieving current user...") + logger.debug("Retrieving current user...") try: user_info = user_data['user_info'] user_email = user_info.get('email') google_id = user_info.get('google_id') - debug(f"Fetching user from database or creating new: {user_email}") + logger.debug(f"Fetching user from database or creating new: {user_email}") # Try to get existing user user = await user_service.get_user_by_email(user_email) # If user doesn't exist, create new user if not user: - debug(f"Creating new user: {user_email}") + logger.debug(f"Creating new user: {user_email}") user = await user_service.create_user({ "email": user_email, "name": user_info.get("name", ""), @@ -106,18 +100,18 @@ async def get_current_user( "google_id": google_id }) else: - debug(f"Found existing user: {user_email}") + logger.debug(f"Found existing user: {user_email}") # Convert UserSchema to dict for checking google_id - user_dict = user.dict() + user_dict = user.model_dump() # Update google_id if it's missing if not user_dict.get('google_id'): - debug(f"Updating missing google_id for user: {user_email}") + logger.debug(f"Updating missing google_id for user: {user_email}") await user_service.update_user(user_dict['_id'], {"google_id": google_id}) - debug(f"User retrieval successful: {user_email}") + logger.debug(f"User retrieval successful: {user_email}") return user except Exception as e: - debug(f"[ERROR] Failed to retrieve user: {str(e)}") + logger.error(f"[ERROR] Failed to retrieve user: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve user: {str(e)}" @@ -145,30 +139,30 @@ async def get_user_preferences( Raises: HTTPException: If preferences cannot be retrieved """ - debug("Retrieving user preferences...") + logger.debug("Retrieving user preferences...") try: user_info = user_data['user_info'] user_email = user_info.get('email') - debug(f"Fetching preferences for user: {user_email}") + logger.debug(f"Fetching preferences for user: {user_email}") # Get user first to ensure they exist user = await user_service.get_user_by_email(user_email) if not user: - debug(f"User not found: {user_email}") + logger.debug(f"User not found: {user_email}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) - preferences = user.preferences.dict() - debug(f"Preferences retrieved successfully for user: {user_email}") + preferences = user.preferences.model_dump() + logger.debug(f"Preferences retrieved successfully for user: {user_email}") return {"preferences": preferences} except HTTPException: raise except Exception as e: - debug(f"[ERROR] Failed to retrieve user preferences: {str(e)}") + logger.error(f"[ERROR] Failed to retrieve user preferences: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve user preferences: {str(e)}" @@ -198,36 +192,36 @@ async def update_preferences( Raises: HTTPException: If preference update fails """ - debug("Updating user preferences...") + logger.debug("Updating user preferences...") try: user_info = user_data['user_info'] user_email = user_info.get('email') - debug(f"Updating preferences for user: {user_email}") + logger.debug(f"Updating preferences for user: {user_email}") # Get user first to ensure they exist user = await user_service.get_user_by_email(user_email) if not user: - debug(f"User not found: {user_email}") + logger.debug(f"User not found: {user_email}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) - debug(f"Found user with ID: {user.google_id}") - debug(f"Current user data: {user.dict()}") + logger.debug(f"Found user with ID: {user.google_id}") + logger.debug(f"Current user data: {user.model_dump()}") # Create update data with existing user fields and new preferences update_data = { "google_id": user.google_id, "email": user.email, "name": user.name, - "oauth": user.oauth.dict() if hasattr(user, 'oauth') else {}, - "preferences": preferences.dict() + "oauth": user.oauth.model_dump() if hasattr(user, 'oauth') and user.oauth else {}, + "preferences": preferences.model_dump() } - debug(f"Update data: {update_data}") + logger.debug(f"Update data: {update_data}") # Update user with new preferences try: @@ -236,26 +230,26 @@ async def update_preferences( update_data ) except Exception as e: - debug(f"Error updating user: {str(e)}") + logger.error(f"Error updating user: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update user: {str(e)}" ) if not updated_user: - debug("Update returned None") + logger.debug("Update returned None") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update preferences" ) - debug(f"Updated user data: {updated_user.dict()}") - debug(f"Preferences updated successfully for user: {user_email}") - return {"preferences": updated_user.preferences.dict()} + logger.debug(f"Updated user data: {updated_user.model_dump()}") + logger.debug(f"Preferences updated successfully for user: {user_email}") + return {"preferences": updated_user.preferences.model_dump()} except HTTPException: raise except Exception as e: - debug(f"[ERROR] Failed to update preferences: {str(e)}") + logger.error(f"[ERROR] Failed to update preferences: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update preferences: {str(e)}" diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 622da34..93a9bcf 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -21,13 +21,14 @@ from app.services.database import TokenRepository, UserRepository, get_token_repository, get_user_repository from app.services.user_service import UserService from app.utils.config import Settings, get_settings +from app.models import UserSchema # Configure logging -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) +# logging.basicConfig( +# level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# datefmt='%Y-%m-%d %H:%M:%S' +# ) logger = logging.getLogger(__name__) diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index 7cdccd2..b4a5689 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -19,15 +19,18 @@ from app.services.database import EmailRepository, get_email_repository from app.services.database.factories import get_user_service, get_auth_service from app.services import auth_service +from app.utils.config import get_settings -# Configure logging -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) +# Configure logging : redundant with the logging in the main file +# logging.basicConfig( +# level=logging.INFO, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# datefmt='%Y-%m-%d %H:%M:%S' +# ) +# Create module-specific logger logger = logging.getLogger(__name__) +settings = get_settings() class EmailService: """ diff --git a/backend/app/services/summarization/summary_service.py b/backend/app/services/summarization/summary_service.py index 10654c5..f66b770 100644 --- a/backend/app/services/summarization/summary_service.py +++ b/backend/app/services/summarization/summary_service.py @@ -19,12 +19,12 @@ GeminiEmailSummarizer ) -# Configure logging with format and level -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) +# Configure logging : redundant with the logging in the main file +# logging.basicConfig( +# level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# datefmt='%Y-%m-%d %H:%M:%S' +# ) # Create module-specific logger logger = logging.getLogger(__name__) diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index dc47c51..1764b0d 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -5,19 +5,17 @@ import logging from typing import Optional, Dict, Any from fastapi import HTTPException, status -from bson import ObjectId -from google.oauth2.credentials import Credentials # Import from app modules -from app.models import UserSchema, TokenData, PreferencesSchema -from app.services.database import UserRepository, get_user_repository +from app.models import UserSchema, PreferencesSchema +from app.services.database import get_user_repository, UserRepository -# Configure logging -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) +# Configure logging : redundant with the logging in the main file +# logging.basicConfig( +# level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# datefmt='%Y-%m-%d %H:%M:%S' +# ) logger = logging.getLogger(__name__) @@ -31,14 +29,15 @@ class UserService: - Managing user authentication state """ - def __init__(self, user_repository: UserRepository): + def __init__(self, user_repository: UserRepository = None): """ Initialize the user service. Args: user_repository: User repository instance """ - self.user_repository = user_repository + # TODO Make sure this is a good way to do this.. + self.user_repository = user_repository or get_user_repository() # Note: We can't call ensure_indexes here because it's async # The indexes will be created on first use @@ -126,7 +125,7 @@ async def create_user(self, user_data: Dict[str, Any]) -> UserSchema: detail="Failed to create user" ) - async def update_user(self, user_id: str, user_data: Dict[str, Any]) -> Optional[UserSchema]: + async def update_user(self, google_id: str, user_data: Dict[str, Any]) -> Optional[UserSchema]: """ Update a user. @@ -139,21 +138,21 @@ async def update_user(self, user_id: str, user_data: Dict[str, Any]) -> Optional """ try: # First get the current user to ensure it exists - current_user = await self.user_repository.find_by_id(user_id) + current_user = await self.user_repository.find_by_id(google_id) if not current_user: - logger.error(f"User not found: {user_id}") + logger.error(f"User not found: {google_id}") return None # Update the user - success = await self.user_repository.update_one(user_id, user_data) + success = await self.user_repository.update_one(google_id, user_data) if not success: - logger.error(f"Update failed for user: {user_id}") + logger.error(f"Update failed for user: {google_id}") return None # Get the updated user - updated_user = await self.user_repository.find_by_id(user_id) + updated_user = await self.user_repository.find_by_id(google_id) if not updated_user: - logger.error(f"Failed to fetch updated user: {user_id}") + logger.error(f"Failed to fetch updated user: {google_id}") return None return UserSchema(**updated_user) @@ -164,18 +163,18 @@ async def update_user(self, user_id: str, user_data: Dict[str, Any]) -> Optional detail="Failed to update user" ) - async def delete_user(self, user_id: str) -> bool: + async def delete_user(self, google_id: str) -> bool: """ Delete a user. Args: - user_id: User ID + google_id: User ID Returns: True if deleted, False otherwise """ try: - return await self.user_repository.delete_one(user_id) + return await self.user_repository.delete_one(google_id) except Exception as e: logger.error(f"Failed to delete user: {e}") raise HTTPException( diff --git a/backend/main.py b/backend/main.py index 8806256..acf1034 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,9 +1,8 @@ # uvicorn main:app --reload import os -from fastapi import FastAPI, HTTPException, Depends, status +from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware -from starlette.concurrency import run_in_threadpool from contextlib import asynccontextmanager import logging @@ -12,20 +11,14 @@ # Reduce verbosity of httpx and httpcore logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) +logger = logging.getLogger(__name__) from app.routers import emails_router, summaries_router, auth_router, user_router from app.services.database.connection import DatabaseConnection -from app.models import EmailSchema, SummarySchema, UserSchema # from app.models.user_model import User -@asynccontextmanager -async def lifespan(app: FastAPI): - await startup_db_client() - yield - await shutdown_db_client() - async def startup_db_client(): """ Initializes MongoDB connection and repository indexes on startup. @@ -54,6 +47,12 @@ async def shutdown_db_client(): except Exception as e: raise RuntimeError("Failed to close database connection") from e +@asynccontextmanager +async def lifespan(app: FastAPI): + await startup_db_client() + yield + await shutdown_db_client() + app = FastAPI( title="Email Essence API", description="API for the Email Essence application", @@ -84,15 +83,7 @@ async def shutdown_db_client(): allow_headers=["*"], # Allows all headers ) -logger = logging.getLogger(__name__) - -# Register routers -app.include_router(auth_router, prefix="/auth", tags=["Auth"]) -app.include_router(user_router, prefix="/user", tags=["User"]) -app.include_router(emails_router, prefix="/emails", tags=["Emails"]) -app.include_router(summaries_router, prefix="/summaries", tags=["Summaries"]) - -# Root route handler +# API Route Handlers (definitions moved before router inclusions) @app.get("/", tags=["Root"]) async def root(): """ @@ -114,7 +105,7 @@ async def root(): "status": "online" } -# Serve favicon.ico from root directory +# Serve favicon.ico from root directory - only served to swagger UI @app.get('/favicon.ico') async def favicon(): root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -172,4 +163,10 @@ async def health_check(): health_status["components"]["google_api"] = f"error: {str(e)}" health_status["status"] = "unhealthy" - return health_status \ No newline at end of file + return health_status + +# Register Routers +app.include_router(auth_router, prefix="/auth", tags=["Auth"]) +app.include_router(user_router, prefix="/user", tags=["User"]) +app.include_router(emails_router, prefix="/emails", tags=["Emails"]) +app.include_router(summaries_router, prefix="/summaries", tags=["Summaries"]) \ No newline at end of file From 1c9be25a6fba0d08822113ababaebdbfe204f4bc Mon Sep 17 00:00:00 2001 From: Joseph Madigan Date: Tue, 10 Jun 2025 17:42:40 -0700 Subject: [PATCH 03/33] Refactor API structure and enhance logging and dependencies across services. - Introduced a new `dependencies.py` module for common auth dependencies - Introduced a new `helpers.py` module for shared utilities, including standardized logging and error handling functions. - Updated all service and router files to utilize the new logging and error handling methods, improving consistency and readability. - Cleaned up imports and organized code structure for better maintainability. - Enhanced error responses to provide more context and standardized messages across the application. --- backend/app/dependencies.py | 130 ++++++++++++++ backend/app/routers/auth_router.py | 128 +++++--------- backend/app/routers/emails_router.py | 60 ++++--- backend/app/routers/summaries_router.py | 124 ++++++-------- backend/app/routers/user_router.py | 161 +++++------------- backend/app/services/auth_service.py | 128 +++++++------- backend/app/services/email_service.py | 95 ++++------- .../app/services/summarization/__init__.py | 6 +- backend/app/services/summarization/base.py | 6 +- backend/app/services/summarization/prompts.py | 10 +- .../summarization/providers/google/google.py | 13 +- .../summarization/providers/google/prompts.py | 7 +- .../summarization/providers/openai/openai.py | 13 +- .../summarization/providers/openai/prompts.py | 5 +- .../services/summarization/summary_service.py | 92 +++++----- backend/app/services/summarization/types.py | 5 +- backend/app/services/user_service.py | 93 ++++------ backend/app/utils/helpers.py | 136 +++++++++++++++ backend/main.py | 41 ++++- 19 files changed, 690 insertions(+), 563 deletions(-) create mode 100644 backend/app/dependencies.py create mode 100644 backend/app/utils/helpers.py diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py new file mode 100644 index 0000000..f79f220 --- /dev/null +++ b/backend/app/dependencies.py @@ -0,0 +1,130 @@ +""" +Common dependencies for Email Essence FastAPI application. + +This module centralizes shared dependencies including authentication schemes, +logging configuration, and common helper functions used across routers and services. +""" + +import logging +from typing import Dict, Any + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer + +# Internal imports +from app.models.user_models import UserSchema +from app.services.auth_service import AuthService +from app.services.user_service import UserService +from app.services.database.factories import get_auth_service, get_user_service +from app.utils.helpers import get_logger, configure_module_logging, standardize_error_response, log_operation + +# ------------------------------------------------------------------------- +# Authentication Dependencies +# ------------------------------------------------------------------------- + +# Centralized OAuth2 scheme +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="/auth/token", + description="Enter the token you received from the login flow (without Bearer prefix)" +) + +async def get_current_user_email( + token: str = Depends(oauth2_scheme), + auth_service: AuthService = Depends(get_auth_service) +) -> str: + """ + Dependency to extract user email from valid token. + + Args: + token: JWT token from OAuth2 authentication + auth_service: Auth service instance + + Returns: + str: User's email address + + Raises: + HTTPException: 401 error if token is invalid + """ + try: + user_data = await auth_service.get_credentials_from_token(token) + return user_data['email'] + except Exception as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + +async def get_current_user_info( + token: str = Depends(oauth2_scheme), + auth_service: AuthService = Depends(get_auth_service) +) -> Dict[str, Any]: + """ + Validates token and returns user information. + + Args: + token: JWT token from OAuth2 authentication + auth_service: Auth service instance + + Returns: + dict: User information and credentials + + Raises: + HTTPException: 401 error if token is invalid + """ + try: + user_data = await auth_service.get_credentials_from_token(token) + return user_data + except Exception as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Invalid authentication: {str(e)}" + ) + +async def get_current_user( + user_data: Dict[str, Any] = Depends(get_current_user_info), + user_service: UserService = Depends(get_user_service) +) -> UserSchema: + """ + Retrieve user details or create user if they don't exist. + + Args: + user_data: User information and credentials from token validation + user_service: User service instance + + Returns: + UserSchema: User profile information + + Raises: + HTTPException: If user retrieval fails + """ + try: + user_info = user_data['user_info'] + user_email = user_info.get('email') + google_id = user_info.get('google_id') + + # Try to get existing user + user = await user_service.get_user_by_email(user_email) + + # If user doesn't exist, create new user + if not user: + user = await user_service.create_user({ + "email": user_email, + "name": user_info.get("name", ""), + "picture": user_info.get("picture", ""), + "google_id": google_id + }) + else: + # Update google_id if it's missing + user_dict = user.model_dump() + if not user_dict.get('google_id'): + await user_service.update_user(user_dict['_id'], {"google_id": google_id}) + + return user + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve user: {str(e)}" + ) + + \ No newline at end of file diff --git a/backend/app/routers/auth_router.py b/backend/app/routers/auth_router.py index 1b25350..9ba23d4 100644 --- a/backend/app/routers/auth_router.py +++ b/backend/app/routers/auth_router.py @@ -6,69 +6,47 @@ via OAuth2 to retrieve and process email data. """ +# Standard library imports +import base64 import json import urllib.parse -import base64 import uuid -import logging -from fastapi import APIRouter, HTTPException, status, Depends, Query, Form -from fastapi.security import OAuth2PasswordBearer -from fastapi.responses import RedirectResponse, HTMLResponse +# Third-party imports +from fastapi import APIRouter, Depends, Form, HTTPException, Query, status +from fastapi.responses import HTMLResponse, RedirectResponse from google.auth.transport.requests import Request as GoogleRequest from google.oauth2.credentials import Credentials -from starlette.concurrency import run_in_threadpool -# from google_auth_oauthlib.flow import Flow from googleapiclient.discovery import build +from starlette.concurrency import run_in_threadpool -from app.services.auth_service import AuthService, SCOPES +# Internal imports +from app.dependencies import get_current_user_email, oauth2_scheme +from app.utils.helpers import get_logger, log_operation +from app.models import ( + AuthStatusResponse, + ExchangeCodeRequest, + RefreshTokenRequest, + TokenData, + TokenResponse, + VerifyTokenRequest, +) +from app.services.auth_service import SCOPES, AuthService +from app.services.database.factories import get_auth_service, get_user_service from app.services.user_service import UserService from app.utils.config import get_settings -from app.services.database.factories import get_auth_service, get_user_service -from app.models import TokenData, TokenResponse, AuthStatusResponse, ExchangeCodeRequest, RefreshTokenRequest, VerifyTokenRequest + +# ------------------------------------------------------------------------- +# Router Configuration +# ------------------------------------------------------------------------- router = APIRouter() settings = get_settings() -logger = logging.getLogger(__name__) - -# -- Authentication Schemes -- - -# This is a simpler authentication scheme for Swagger UI -# It only shows a token field without client_id/client_secret -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token", description="Enter the token you received from the login flow (without Bearer prefix)") - -# -- Authentication Utility -- - -async def get_current_user_email( - token: str = Depends(oauth2_scheme), - auth_service: AuthService = Depends(get_auth_service) -): - """ - Dependency to extract user email from valid token. - Will raise 401 automatically if token is invalid. - - Args: - token: JWT token from OAuth2 authentication - auth_service: Auth service instance - - Returns: - str: User's email address - - Raises: - HTTPException: 401 error if token is invalid - """ - try: - # Get user info from token - user_data = await auth_service.get_credentials_from_token(token) - return user_data['email'] - except Exception as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) +logger = get_logger(__name__, 'router') -# -- Endpoints -- +# ------------------------------------------------------------------------- +# Endpoints +# ------------------------------------------------------------------------- @router.get( "/login", @@ -94,7 +72,7 @@ async def login( Returns: RedirectResponse: Redirects to Google's authentication page """ - logger.debug(f"Login initiated - Redirect URI: {redirect_uri}") + log_operation(logger, 'debug', f"Login initiated - Redirect URI: {redirect_uri}") try: # Create a state object that includes the frontend redirect URI @@ -110,17 +88,13 @@ async def login( result = auth_service.create_authorization_url(encoded_custom_state) authorization_url = result["authorization_url"] - logger.debug(f"Generated Google OAuth URL: {authorization_url}") + log_operation(logger, 'debug', f"Generated Google OAuth URL: {authorization_url}") # Now redirect to the correct URL return RedirectResponse(authorization_url) except Exception as e: - logger.error(f"[ERROR] Login failed: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create authorization URL: {str(e)}" - ) + raise standardize_error_response(e, "login") @router.get("/callback") async def callback( @@ -141,7 +115,7 @@ async def callback( Returns: RedirectResponse: Redirects to frontend with authentication state """ - logger.debug(f"Received callback with code: {code}") + log_operation(logger, 'debug', f"Received callback with code: {code}") try: if not state: @@ -151,13 +125,13 @@ async def callback( decoded_state = json.loads(base64.urlsafe_b64decode(state).decode()) frontend_url = decoded_state.get("redirect_uri") - logger.debug(f"Decoded state - Redirecting to frontend: {frontend_url}") + log_operation(logger, 'debug', f"Decoded state - Redirecting to frontend: {frontend_url}") if not frontend_url: raise ValueError("Missing redirect URI in state parameter") # Exchange code for tokens and get user info in one step - logger.debug("Exchanging code for tokens and getting user info...") + log_operation(logger, 'debug', "Exchanging code for tokens and getting user info...") token_data = await auth_service.get_tokens_from_code(code, None) # First exchange # Get user info using the token @@ -178,7 +152,7 @@ async def callback( ) user_email = user_info.get('email') - logger.debug(f"User email retrieved: {user_email}") + log_operation(logger, 'debug', f"User email retrieved: {user_email}") if not user_email: raise ValueError("Could not retrieve user email from Google") @@ -186,7 +160,7 @@ async def callback( # Check if user exists, create if not user = await user_service.get_user_by_email(user_email) if not user: - logger.debug(f"Creating new user: {user_email}") + log_operation(logger, 'info', f"Creating new user: {user_email}") user = await user_service.create_user({ "email": user_email, "name": user_info.get("name", ""), @@ -194,7 +168,7 @@ async def callback( "google_id": user_info.get("id") }) else: - logger.debug(f"Found existing user: {user_email}") + log_operation(logger, 'info', f"Found existing user: {user_email}") # Special handling for Swagger UI testing if "localhost:8000/docs" in frontend_url or "/docs" in frontend_url: @@ -234,7 +208,7 @@ async def exchange_code( Requires the user's email to associate the tokens. """ - logger.debug(f"Exchanging OAuth code for user: {request.user_email}") + log_operation(logger, 'info', f"Exchanging OAuth code for user: {request.user_email}") try: if not request.code or not request.user_email: raise HTTPException( @@ -245,7 +219,7 @@ async def exchange_code( # Exchange auth code for tokens and store them in MongoDB tokens = await auth_service.get_tokens_from_code(request.code, request.user_email) - logger.debug(f"Token exchange successful for {request.user_email}") + log_operation(logger, 'debug', f"Token exchange successful for {request.user_email}") return TokenResponse( access_token=tokens.token, token_type="bearer", @@ -254,11 +228,8 @@ async def exchange_code( ) except Exception as e: - logger.error(f"[ERROR] Code exchange failed: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Failed to exchange code for tokens: {str(e)}" - ) + log_operation(logger, 'error', f"Code exchange failed: {str(e)}") + raise standardize_error_response(e, "exchange code") @router.get("/token", response_model=TokenResponse) async def get_token( @@ -278,10 +249,7 @@ async def get_token( token_type="bearer" ) except Exception as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Token retrieval failed: {str(e)}" - ) + raise standardize_error_response(e, "get token") @router.post("/refresh", response_model=TokenResponse) async def refresh_token( @@ -308,10 +276,7 @@ async def refresh_token( token_type="bearer" ) except Exception as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Token refresh failed: {str(e)}" - ) + raise standardize_error_response(e, "refresh token") @router.get("/status", response_model=AuthStatusResponse) async def auth_status( @@ -328,7 +293,7 @@ async def auth_status( user_data = await auth_service.get_credentials_from_token(token) user_google_id = user_data['google_id'] - logger.debug(f"User google_id extracted from token: {user_google_id}") + log_operation(logger, 'debug', f"User google_id extracted from token: {user_google_id}") # Get detailed credentials from the database using that email try: @@ -359,7 +324,7 @@ async def auth_status( except Exception as e: # Token validation failed - logger.error(f"[ERROR] Auth status check failed: {str(e)}", exc_info=True) + log_operation(logger, 'error', f"Auth status check failed: {str(e)}") return AuthStatusResponse( is_authenticated=False, token_valid=False, @@ -582,7 +547,4 @@ async def token_endpoint( "token_type": "bearer" } except Exception as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Failed to store token: {str(e)}" - ) \ No newline at end of file + raise standardize_error_response(e, "token endpoint") \ No newline at end of file diff --git a/backend/app/routers/emails_router.py b/backend/app/routers/emails_router.py index a58b949..91b8994 100644 --- a/backend/app/routers/emails_router.py +++ b/backend/app/routers/emails_router.py @@ -6,20 +6,30 @@ It provides a set of REST endpoints for interacting with the user's email data. """ -from fastapi import APIRouter, HTTPException, Query, Depends, status +# Standard library imports from typing import Optional -import logging -from app.models.email_models import EmailSchema, EmailResponse, ReaderViewResponse +# Third-party imports +from fastapi import APIRouter, Depends, HTTPException, Query, status + +# Internal imports +from app.dependencies import get_current_user +from app.utils.helpers import get_logger, log_operation, standardize_error_response +from app.models.email_models import EmailResponse, EmailSchema, ReaderViewResponse from app.models.user_models import UserSchema -from app.routers.user_router import get_current_user from app.services.database.factories import get_email_service from app.services.email_service import EmailService +# ------------------------------------------------------------------------- +# Router Configuration +# ------------------------------------------------------------------------- + router = APIRouter() +logger = get_logger(__name__, 'router') -# Create module-specific logger -logger = logging.getLogger(__name__) +# ------------------------------------------------------------------------- +# Endpoints +# ------------------------------------------------------------------------- @router.get( "/", @@ -78,8 +88,8 @@ async def retrieve_emails( try: # Log request parameters - logger.debug(f"Email retrieval request with refresh={refresh}", extra={"params": debug_info["request_params"]}) - logger.debug(f"Google ID for email retrieval: {user.google_id}") + log_operation(logger, 'debug', f"Email retrieval request with refresh={refresh}", extra={"params": debug_info["request_params"]}) + log_operation(logger, 'debug', f"Google ID for email retrieval: {user.google_id}") emails, total, service_debug_info = await email_service.fetch_emails( google_id=user.google_id, @@ -95,7 +105,7 @@ async def retrieve_emails( # Combine debug info debug_info.update(service_debug_info) - logger.info(f"Retrieved {len(emails)} emails out of {total} total") + log_operation(logger, 'info', f"Retrieved {len(emails)} emails out of {total} total") return EmailResponse( emails=emails, @@ -105,9 +115,7 @@ async def retrieve_emails( ) except Exception as e: - error_msg = f"Failed to retrieve emails: {str(e)}" - logger.exception(error_msg) # This logs the full stack trace - raise HTTPException(status_code=500, detail=error_msg) + raise standardize_error_response(e, "retrieve emails") @router.get( "/{email_id}", @@ -136,7 +144,11 @@ async def retrieve_email( """ email = await email_service.get_email(email_id, user.google_id) if not email: - raise HTTPException(status_code=404, detail="Email not found") + raise standardize_error_response( + Exception("Email not found"), + "get email", + email_id + ) return email @router.put( @@ -166,7 +178,11 @@ async def mark_email_as_read( """ updated_email = await email_service.mark_email_as_read(email_id, user.google_id) if not updated_email: - raise HTTPException(status_code=404, detail="Email not found") + raise standardize_error_response( + Exception("Email not found"), + "mark email as read", + email_id + ) return updated_email @router.delete( @@ -196,7 +212,11 @@ async def delete_email( """ success = await email_service.delete_email(email_id, user.google_id) if not success: - raise HTTPException(status_code=404, detail="Email not found") + raise standardize_error_response( + Exception("Email not found"), + "delete email", + email_id + ) return {"message": "Email deleted successfully"} @router.get( @@ -230,13 +250,15 @@ async def get_email_reader_view( reader_content = await email_service.get_email_reader_view(email_id, user.google_id) if not reader_content: - raise HTTPException(status_code=404, detail="Email not found") + raise standardize_error_response( + Exception("Email not found"), + "get email reader view", + email_id + ) return reader_content except Exception as e: if isinstance(e, HTTPException): raise e - error_msg = f"Failed to generate reader view: {str(e)}" - logger.exception(error_msg) - raise HTTPException(status_code=500, detail=error_msg) \ No newline at end of file + raise standardize_error_response(e, "generate reader view", email_id) \ No newline at end of file diff --git a/backend/app/routers/summaries_router.py b/backend/app/routers/summaries_router.py index 866dc50..66ea7c6 100644 --- a/backend/app/routers/summaries_router.py +++ b/backend/app/routers/summaries_router.py @@ -6,32 +6,40 @@ strategies to provide concise representations of emails. """ +# Standard library imports import logging from typing import List -from fastapi import APIRouter, HTTPException, Depends, Query, Path, status +# Third-party imports +from fastapi import APIRouter, Depends, HTTPException, Path, Query, status + +# Internal imports +from app.dependencies import get_current_user +from app.utils.helpers import get_logger, log_operation, standardize_error_response from app.models import EmailSchema, SummarySchema, UserSchema from app.services import SummaryService -from app.services.summarization import get_summarizer -from app.services.summarization.base import AdaptiveSummarizer +from app.services.database.factories import get_email_service, get_summary_service from app.services.summarization import ( - ProcessingStrategy, - OpenAIEmailSummarizer, - GeminiEmailSummarizer -) -from app.routers.user_router import get_current_user -from app.services.database.factories import ( - get_summary_service, - get_email_service + GeminiEmailSummarizer, + OpenAIEmailSummarizer, + ProcessingStrategy, + get_summarizer, ) +from app.services.summarization.base import AdaptiveSummarizer + +# ------------------------------------------------------------------------- +# Router Configuration +# ------------------------------------------------------------------------- + +router = APIRouter() # Add specific configuration for pymongo's logger logging.getLogger('pymongo').setLevel(logging.WARNING) +logger = get_logger(__name__, 'router') -# Create module-specific logger -logger = logging.getLogger(__name__) - -router = APIRouter() +# ------------------------------------------------------------------------- +# Endpoints +# ------------------------------------------------------------------------- @router.get( "/batch", @@ -78,15 +86,17 @@ async def get_summaries_by_ids( if not result['summaries']: if result['missing_emails'] and not result['failed_summaries']: # Only missing emails, no generation failures - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Emails not found: {result['missing_emails']}" + raise standardize_error_response( + Exception("Emails not found"), + "get summaries by ids", + result['missing_emails'] ) elif result['failed_summaries'] and not result['missing_emails']: # Only generation failures, no missing emails - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Failed to generate summaries for emails: {result['failed_summaries']}" + raise standardize_error_response( + Exception("Failed to generate summaries"), + "get summaries by ids", + result['failed_summaries'] ) else: # Both missing emails and generation failures @@ -94,14 +104,15 @@ async def get_summaries_by_ids( "missing_emails": result['missing_emails'], "failed_summaries": result['failed_summaries'] } - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"No summaries could be generated: {error_details}" + raise standardize_error_response( + Exception("No summaries could be generated"), + "get summaries by ids", + error_details ) # If we have some successful summaries but also some failures, log a warning if result['missing_emails'] or result['failed_summaries']: - logger.warning( + log_operation(logger, 'warning', f"Partial success for user {user.google_id}: " f"Partial success for user {user.google_id}: " f"{len(result['summaries'])} successful, " f"{len(result['missing_emails'])} missing, " @@ -114,11 +125,7 @@ async def get_summaries_by_ids( # Re-raise HTTP exceptions as-is raise except Exception as e: - logger.error(f"Error retrieving/generating summaries by IDs: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to retrieve email summaries: {str(e)}" - ) + raise standardize_error_response(e, "retrieve/generate summaries by IDs") @router.get( "/", @@ -233,12 +240,7 @@ async def get_summaries( ) except Exception as e: - # Log the full error for debugging - logger.error(f"Error processing summaries: {str(e)}", exc_info=True) - raise HTTPException( - status_code=500, - detail="Failed to process email summaries" - ) + raise standardize_error_response(e, "process email summaries") @router.get( "/recent/{days}", @@ -265,7 +267,7 @@ async def get_recent_summaries( """ try: # Log request parameters - logger.debug(f"Getting recent summaries for user {user.email} - days: {days}, limit: {limit}") + log_operation(logger, 'debug', f"Getting recent summaries for user {user.email} - days: {days}, limit: {limit}") # Get summaries from service summaries = await summary_service.get_recent_summaries( @@ -274,22 +276,10 @@ async def get_recent_summaries( google_id=user.google_id ) - logger.debug(f"Retrieved {len(summaries)} summaries for user {user.email}") + log_operation(logger, 'debug', f"Retrieved {len(summaries)} summaries for user {user.email}") return summaries except Exception as e: - logger.error( - f"Error retrieving recent summaries for user {user.email}: {str(e)}", - exc_info=True, - extra={ - "user_email": user.email, - "days": days, - "limit": limit - } - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to retrieve recent summaries" - ) + raise standardize_error_response(e, "retrieve recent summaries") @router.get( "/keyword/{keyword}", @@ -322,11 +312,7 @@ async def search_by_keyword( results = await summary_service.search_by_keywords([keyword], limit=limit, google_id=user.google_id) return results except Exception as e: - logging.error(f"Error searching summaries by keyword: {str(e)}", exc_info=True) - raise HTTPException( - status_code=500, - detail="Failed to search summaries" - ) + raise standardize_error_response(e, "search summaries by keyword") @router.get("/{email_id}", response_model=SummarySchema) async def get_summary_by_id( @@ -351,19 +337,16 @@ async def get_summary_by_id( # Get summary from repository summary = await summary_service.get_or_create_summary(email_id, summarizer, user.google_id) if not summary: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Summary not found for email {email_id}" + raise standardize_error_response( + Exception("Summary not found"), + "get summary", + email_id ) return SummarySchema(**summary) except Exception as e: - logger.error(f"Error retrieving/generating summary: {e}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) + raise standardize_error_response(e, "retrieve/generate summary", email_id) @router.post( "/summarize", @@ -415,11 +398,7 @@ async def summarize_single_email( return summary except Exception as e: - logging.error(f"Error summarizing email: {str(e)}", exc_info=True) - raise HTTPException( - status_code=500, - detail="Failed to generate email summary" - ) + raise standardize_error_response(e, "generate email summary") @router.delete( "/{email_id}", @@ -448,8 +427,9 @@ async def delete_summary( """ deleted = await summary_service.delete_summary(email_id, user.google_id) if not deleted: - raise HTTPException( - status_code=404, - detail=f"Summary for email {email_id} not found" + raise standardize_error_response( + Exception("Summary not found"), + "delete summary", + email_id ) return {"message": f"Summary for email {email_id} deleted"} diff --git a/backend/app/routers/user_router.py b/backend/app/routers/user_router.py index 8aabb85..1e1866a 100644 --- a/backend/app/routers/user_router.py +++ b/backend/app/routers/user_router.py @@ -5,55 +5,27 @@ It provides endpoints for retrieving and updating user information and preferences. """ -from fastapi import APIRouter, HTTPException, status, Depends -from fastapi.security import OAuth2PasswordBearer -import logging +# Third-party imports +from fastapi import APIRouter, Depends, HTTPException, status -from app.models import UserSchema, PreferencesSchema +# Internal imports +from app.dependencies import get_current_user, get_current_user_info +from app.utils.helpers import get_logger, log_operation, standardize_error_response +from app.models import PreferencesSchema, UserSchema from app.services.auth_service import AuthService +from app.services.database.factories import get_auth_service, get_user_service from app.services.user_service import UserService -from app.services.database.factories import get_user_service, get_auth_service -router = APIRouter() -logger = logging.getLogger(__name__) - -# OAuth authentication scheme -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token", description="Enter the token you received from the login flow (without Bearer prefix)") +# ------------------------------------------------------------------------- +# Router Configuration +# ------------------------------------------------------------------------- -# Debugging helper function -# def debug(message: str): -# """Print debug messages with a consistent format""" -# print(f"[DEBUG] {message}") +router = APIRouter() +logger = get_logger(__name__, 'router') -async def get_current_user_info( - token: str = Depends(oauth2_scheme), - auth_service: AuthService = Depends(get_auth_service) -): - """ - Validates token and returns user information. - - Args: - token: JWT token from OAuth2 authentication - auth_service: Auth service instance - - Returns: - dict: User information and credentials - - Raises: - HTTPException: 401 error if token is invalid - """ - logger.debug(f"Validating token for user authentication...") - - try: - user_data = await auth_service.get_credentials_from_token(token) - logger.debug(f"User authenticated successfully: {user_data.get('user_info', {}).get('email', 'Unknown')}") - return user_data - except Exception as e: - logger.error(f"[ERROR] Authentication failed: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Invalid authentication: {str(e)}" - ) +# ------------------------------------------------------------------------- +# Endpoints +# ------------------------------------------------------------------------- @router.get( "/me", @@ -61,61 +33,20 @@ async def get_current_user_info( summary="Get current user profile", description="Retrieves the authenticated user's profile information or creates a new user record if one doesn't exist" ) -async def get_current_user( - user_data: dict = Depends(get_current_user_info), - user_service: UserService = Depends(get_user_service) +async def get_current_user_profile( + user: UserSchema = Depends(get_current_user) ): """ - Retrieve user details or create user if they don't exist. + Retrieve current user profile. Args: - user_data: User information and credentials from token validation - user_service: User service instance + user: Current authenticated user from dependency Returns: UserSchema: User profile information - - Raises: - HTTPException: If user retrieval fails """ - logger.debug("Retrieving current user...") - - try: - user_info = user_data['user_info'] - user_email = user_info.get('email') - google_id = user_info.get('google_id') - - logger.debug(f"Fetching user from database or creating new: {user_email}") - - # Try to get existing user - user = await user_service.get_user_by_email(user_email) - - # If user doesn't exist, create new user - if not user: - logger.debug(f"Creating new user: {user_email}") - user = await user_service.create_user({ - "email": user_email, - "name": user_info.get("name", ""), - "picture": user_info.get("picture", ""), - "google_id": google_id - }) - else: - logger.debug(f"Found existing user: {user_email}") - # Convert UserSchema to dict for checking google_id - user_dict = user.model_dump() - # Update google_id if it's missing - if not user_dict.get('google_id'): - logger.debug(f"Updating missing google_id for user: {user_email}") - await user_service.update_user(user_dict['_id'], {"google_id": google_id}) - - logger.debug(f"User retrieval successful: {user_email}") - return user - except Exception as e: - logger.error(f"[ERROR] Failed to retrieve user: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to retrieve user: {str(e)}" - ) + logger.debug(f"User profile retrieved: {user.email}") + return user @router.get( "/preferences", @@ -159,14 +90,8 @@ async def get_user_preferences( preferences = user.preferences.model_dump() logger.debug(f"Preferences retrieved successfully for user: {user_email}") return {"preferences": preferences} - except HTTPException: - raise except Exception as e: - logger.error(f"[ERROR] Failed to retrieve user preferences: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to retrieve user preferences: {str(e)}" - ) + raise standardize_error_response(e, "retrieve user preferences") @router.put( "/preferences", @@ -230,11 +155,7 @@ async def update_preferences( update_data ) except Exception as e: - logger.error(f"Error updating user: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update user: {str(e)}" - ) + raise standardize_error_response(e, "update preferences") if not updated_user: logger.debug("Update returned None") @@ -246,14 +167,8 @@ async def update_preferences( logger.debug(f"Updated user data: {updated_user.model_dump()}") logger.debug(f"Preferences updated successfully for user: {user_email}") return {"preferences": updated_user.preferences.model_dump()} - except HTTPException: - raise except Exception as e: - logger.error(f"[ERROR] Failed to update preferences: {str(e)}", exc_info=True) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update preferences: {str(e)}" - ) + raise standardize_error_response(e, "update preferences") @router.get("/{user_id}", response_model=UserSchema) async def get_user( @@ -277,9 +192,10 @@ async def get_user( """ user = await user_service.get_user(user_id) if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found" + raise standardize_error_response( + Exception("User not found"), + "get user", + user_id ) return user @@ -303,9 +219,10 @@ async def get_user_by_email( """ user = await user_service.get_user_by_email(email) if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found" + raise standardize_error_response( + Exception("User not found"), + "get user by email", + email ) return user @@ -350,9 +267,10 @@ async def update_user( """ user = await user_service.update_user(user_id, user_data) if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found" + raise standardize_error_response( + Exception("User not found"), + "update user", + user_id ) return user @@ -378,9 +296,10 @@ async def delete_user( """ success = await user_service.delete_user(user_id) if not success: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found" + raise standardize_error_response( + Exception("User not found"), + "delete user", + user_id ) return {"message": "User deleted successfully"} \ No newline at end of file diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 93a9bcf..b7fe871 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -5,33 +5,36 @@ and user authentication with Google. """ -import logging +# Standard library imports import os -from typing import Optional, Dict, Any from datetime import datetime, timedelta +from typing import Any, Dict, Optional + +# Third-party imports from fastapi import HTTPException, status +from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import Flow -from google.auth.transport.requests import Request from googleapiclient.discovery import build from starlette.concurrency import run_in_threadpool -# Import from app modules -from app.models import TokenData, AuthState -from app.services.database import TokenRepository, UserRepository, get_token_repository, get_user_repository +# Internal imports +from app.utils.helpers import get_logger, log_operation, standardize_error_response +from app.models import AuthState, TokenData, UserSchema +from app.services.database import ( + TokenRepository, + UserRepository, + get_token_repository, + get_user_repository, +) from app.services.user_service import UserService from app.utils.config import Settings, get_settings -from app.models import UserSchema -# Configure logging -# logging.basicConfig( -# level=logging.DEBUG, -# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# datefmt='%Y-%m-%d %H:%M:%S' -# ) - -logger = logging.getLogger(__name__) +# ------------------------------------------------------------------------- +# Configuration +# ------------------------------------------------------------------------- +logger = get_logger(__name__, 'service') settings = get_settings() SCOPES = [ @@ -87,56 +90,55 @@ async def verify_user_access( Raises: HTTPException: 403 if access is denied """ - logger.debug(f"Verifying user access for user ID: {user_id}") + log_operation(logger, 'debug', f"Verifying user access for user ID: {user_id}") try: # Get the current user's email from the token data token_record = await self.get_token_record(current_user_data['google_id']) if not token_record: - logger.warning(f"No token record found for user: {current_user_data['google_id']}") - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="No valid token record found" + log_operation(logger, 'warning', f"No token record found for user: {current_user_data['google_id']}") + raise standardize_error_response( + Exception("No valid token record found"), + "verify user access", + current_user_data['google_id'] ) current_user = await user_service.get_user(current_user_data['user_info']['id']) if not current_user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Current user not found" + raise standardize_error_response( + Exception("Current user not found"), + "verify user access", + current_user_data['google_id'] ) # Allow access if: # 1. User is accessing their own data # 2. User has admin privileges if current_user['google_id'] == user_id or current_user.get('is_admin', False): - logger.debug(f"Access granted for user ID: {user_id}") + log_operation(logger, 'debug', f"Access granted for user ID: {user_id}") return True - logger.debug(f"Access denied for user ID: {user_id}") - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="You do not have permission to access this resource" + log_operation(logger, 'debug', f"Access denied for user ID: {user_id}") + raise standardize_error_response( + Exception("You do not have permission to access this resource"), + "verify user access", + user_id ) - except HTTPException: - raise except Exception as e: - logger.error(f"Access verification failed: {str(e)}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to verify access: {str(e)}" - ) + raise standardize_error_response(e, "verify user access", user_id) def create_authorization_url(self, custom_state=None) -> Dict[str, str]: """Generates Google OAuth2 authorization URL.""" - logger.debug("Generating Google OAuth2 authorization URL...") + log_operation(logger, 'debug', "Generating Google OAuth2 authorization URL...") client_id = settings.google_client_id client_secret = settings.google_client_secret if not client_id or not client_secret: - logger.error("Google API credentials missing.") - raise HTTPException(status_code=500, detail="Google API credentials not found in settings.") + raise standardize_error_response( + Exception("Google API credentials missing"), + "create authorization URL" + ) client_config = { "web": { @@ -151,7 +153,7 @@ def create_authorization_url(self, custom_state=None) -> Dict[str, str]: flow = Flow.from_client_config(client_config, SCOPES) flow.redirect_uri = self.get_redirect_uri() - logger.debug(f"Using redirect URI: {flow.redirect_uri}") + log_operation(logger, 'debug', f"Using redirect URI: {flow.redirect_uri}") if custom_state: authorization_url, _ = flow.authorization_url( @@ -237,11 +239,7 @@ async def get_tokens_from_code(self, code: str, email: str) -> TokenData: return token except Exception as e: - logger.error(f"Failed to get tokens for user {email}: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get tokens" - ) + raise standardize_error_response(e, "get tokens from code", email) async def get_current_user(self, email: str) -> Optional[Dict[str, Any]]: """ @@ -265,11 +263,7 @@ async def get_current_user(self, email: str) -> Optional[Dict[str, Any]]: )) return user.model_dump() except Exception as e: - logger.error(f"Failed to get current user: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get current user" - ) + raise standardize_error_response(e, "get current user", email) async def get_token_data(self, google_id: str) -> Optional[TokenData]: """ @@ -284,18 +278,14 @@ async def get_token_data(self, google_id: str) -> Optional[TokenData]: try: return await self.token_repository.find_by_google_id(google_id) except Exception as e: - logger.error(f"Failed to get token record for google_id {google_id}: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get token record" - ) + raise standardize_error_response(e, "get token data", google_id) def get_redirect_uri(self): """Returns the OAuth redirect URI.""" - logger.debug("Retrieving redirect URI...") + log_operation(logger, 'debug', "Retrieving redirect URI...") if callback_url := settings.oauth_callback_url: - logger.debug(f"Using env-specified callback URL: {callback_url}") + log_operation(logger, 'debug', f"Using env-specified callback URL: {callback_url}") return callback_url environment = settings.environment @@ -312,7 +302,7 @@ async def get_credentials_from_token(self, token: str): Validates a token and returns user information from Google. Used for authenticating API requests. """ - logger.debug("Validating access token and retrieving user info...") + log_operation(logger, 'debug', "Validating access token and retrieving user info...") try: # First try to validate the token directly @@ -332,13 +322,13 @@ async def get_credentials_from_token(self, token: str): service.userinfo().get().execute() ) except Exception as e: - logger.debug(f"Initial token validation failed, attempting refresh: {e}") + log_operation(logger, 'debug', f"Initial token validation failed, attempting refresh: {e}") # If token validation fails, try to get a new token using refresh token token_record = await self.token_repository.find_by_token(token) if not token_record or not token_record.refresh_token: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid or expired token" + raise standardize_error_response( + Exception("Invalid or expired token"), + "get credentials from token" ) # Create credentials with refresh token @@ -370,13 +360,13 @@ async def get_credentials_from_token(self, token: str): ) if not user_info or not user_info.get('email'): - logger.error("Unable to retrieve user email from token.") + log_operation(logger, 'error', "Unable to retrieve user email from token.") raise ValueError("Unable to retrieve user email from token") # Add google_id to user_info user_info['google_id'] = user_info.get('id') - logger.info(f"User info retrieved for: {user_info.get('email')}") + log_operation(logger, 'info', f"User info retrieved for: {user_info.get('email')}") return { 'user_info': user_info, @@ -385,8 +375,7 @@ async def get_credentials_from_token(self, token: str): } except Exception as e: - logger.exception("Token validation failed.") - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid token: {str(e)}") + raise standardize_error_response(e, "get credentials from token") async def get_token_record(self, google_id: str) -> Optional[Dict[str, Any]]: """ @@ -399,16 +388,15 @@ async def get_token_record(self, google_id: str) -> Optional[Dict[str, Any]]: Optional[Dict[str, Any]]: Complete token record if found, None otherwise """ try: - logger.debug(f"Getting token record for google_id: {google_id}") + log_operation(logger, 'debug', f"Getting token record for google_id: {google_id}") token_data = await self.token_repository.find_by_google_id(google_id) if not token_data: - logger.warning(f"No token record found for google_id: {google_id}") + log_operation(logger, 'warning', f"No token record found for google_id: {google_id}") return None - logger.info(f"Found token record for google_id: {google_id}") + log_operation(logger, 'info', f"Found token record for google_id: {google_id}") # Convert TokenData to dict if it's a model instance if hasattr(token_data, 'model_dump'): return token_data.model_dump() return token_data except Exception as e: - logger.error(f"Failed to get token record for google_id {google_id}: {e}") - return None + raise standardize_error_response(e, "get token record", google_id) diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index b4a5689..1d5f7cb 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -2,34 +2,33 @@ Email service for handling email-related operations. """ -import logging -import os +# Standard library imports import email -from typing import List, Optional, Dict, Any, Tuple, Union +import os import re -from email.header import decode_header -from imapclient import IMAPClient from datetime import datetime -from google.auth.transport.requests import Request +from email.header import decode_header +from typing import Any, Dict, List, Optional, Tuple, Union + +# Third-party imports from fastapi import HTTPException, status +from google.auth.transport.requests import Request +from imapclient import IMAPClient from starlette.concurrency import run_in_threadpool -# Import from app modules +# Internal imports +from app.utils.helpers import get_logger, log_operation, standardize_error_response from app.models import EmailSchema, ReaderViewResponse -from app.services.database import EmailRepository, get_email_repository -from app.services.database.factories import get_user_service, get_auth_service from app.services import auth_service +from app.services.database import EmailRepository, get_email_repository +from app.services.database.factories import get_auth_service, get_user_service from app.utils.config import get_settings -# Configure logging : redundant with the logging in the main file -# logging.basicConfig( -# level=logging.INFO, -# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# datefmt='%Y-%m-%d %H:%M:%S' -# ) +# ------------------------------------------------------------------------- +# Configuration +# ------------------------------------------------------------------------- -# Create module-specific logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__, 'service') settings = get_settings() class EmailService: @@ -63,16 +62,7 @@ def _ensure_email_schema(self, email_data: Union[dict, EmailSchema]) -> EmailSch def _handle_email_error(self, error: Exception, operation: str, email_id: str = None, google_id: str = None) -> None: """Standardize error handling for email operations.""" - error_msg = f"Failed to {operation}" - if email_id: - error_msg += f" email {email_id}" - if google_id: - error_msg += f" for user {google_id}" - logger.exception(f"{error_msg}: {str(error)}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=error_msg - ) + raise standardize_error_response(error, operation, email_id, google_id) def _get_imap_connection(self, token: str, email_account: str) -> IMAPClient: """Create and authenticate IMAP connection.""" @@ -81,15 +71,7 @@ def _get_imap_connection(self, token: str, email_account: str) -> IMAPClient: server.oauth2_login(email_account, token) return server except Exception as e: - logger.error(f"IMAP Authentication Error: {e}") - if hasattr(e, 'args') and e.args: - logger.error(f"Additional error info: {e.args}") - raise - - def _log_operation(self, level: str, message: str, **kwargs) -> None: - """Standardize logging across the service.""" - log_method = getattr(logger, level.lower()) - log_method(message, **kwargs) + raise standardize_error_response(e, "get imap connection", email_account) def _build_search_query(self, search: str) -> Dict[str, Any]: """Build search query component.""" @@ -124,16 +106,13 @@ async def get_auth_token(self) -> str: if credentials.expired and credentials.refresh_token: await run_in_threadpool(lambda: credentials.refresh(Request())) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token expired and cannot be refreshed. User needs to re-authenticate." + raise standardize_error_response( + Exception("Token expired and cannot be refreshed. User needs to re-authenticate."), + "get auth token" ) return credentials.token except Exception as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Token retrieval failed: {str(e)}" - ) + raise standardize_error_response(e, "get auth token") # ------------------------------------------------------------------------- # Email Parsing Methods @@ -275,7 +254,7 @@ def _extract_email_body(self, email_message: email.message.Message) -> Tuple[str body = html_part.get_payload(decode=True).decode(errors="replace") is_html = True except Exception as e: - self._log_operation('error', f"Error decoding HTML part: {e}") + log_operation(logger, 'error', f"Error decoding HTML part: {e}") # Fall back to text part if available if text_part: body = text_part.get_payload(decode=True).decode(errors="replace") @@ -289,14 +268,14 @@ def _extract_email_body(self, email_message: email.message.Message) -> Tuple[str body = email_message.get_payload(decode=True).decode(errors="replace") is_html = content_type == "text/html" except Exception as e: - self._log_operation('error', f"Error decoding non-multipart message: {e}") + log_operation(logger, 'error', f"Error decoding non-multipart message: {e}") body = email_message.get_payload(decode=False) # Try to detect HTML if content-type wasn't reliable is_html = bool(re.search(r'<(?:html|body|div|p|h[1-6])[^>]*>', body, re.IGNORECASE)) # Validate HTML detection with regex if needed if is_html and not bool(re.search(r'<(?:html|body|div|p|h[1-6])[^>]*>', body, re.IGNORECASE)): - self._log_operation('warning', "Content marked as HTML but no HTML tags found, validating...") + log_operation(logger, 'warning', "Content marked as HTML but no HTML tags found, validating...") is_html = False # Reset if no HTML tags found # Apply minimal sanitization for HTML content @@ -403,7 +382,7 @@ def _fetch_from_imap_sync(self, token: str, email_account: str, emails.append(email_data) except Exception as e: - self._log_operation('error', f"Error processing email {uid}: {e}") + log_operation(logger, 'error', f"Error processing email {uid}: {e}") continue return emails @@ -420,7 +399,7 @@ async def save_email_to_db(self, email_data: dict) -> None: if not existing_email: email_schema = self._ensure_email_schema(email_data) await self.email_repository.insert_one(email_schema) - self._log_operation('info', f"Email {email_id} inserted successfully") + log_operation(logger, 'info', f"Email {email_id} inserted successfully") except Exception as e: self._handle_email_error(e, "save", email_data.get("email_id"), email_data.get("google_id")) @@ -469,7 +448,7 @@ async def mark_email_as_read(self, email_id: str, google_id: str) -> Optional[Em email_id = str(email_id) email_data = await self.email_repository.find_by_email_and_google_id(email_id, google_id) if not email_data or email_data["google_id"] != google_id: - self._log_operation('warning', f"Email {email_id} not found for user {google_id}") + log_operation(logger, 'warning', f"Email {email_id} not found for user {google_id}") return None email_data["is_read"] = True @@ -641,7 +620,7 @@ async def fetch_emails(self, google_id: str, skip: int = 0, limit: int = 20, ) debug_info["timing"]["main_query"] = (datetime.now() - start_time).total_seconds() - self._log_operation('info', f"Retrieved {len(emails)} emails out of {total} total for user {google_id}") + log_operation(logger, 'info', f"Retrieved {len(emails)} emails out of {total} total for user {google_id}") return emails, total, debug_info except Exception as e: @@ -661,26 +640,26 @@ async def _refresh_emails_from_imap(self, google_id: str, debug_info: Dict[str, # Get user by google_id user = await user_service.get_user(google_id) if not user: - self._log_operation('error', f"User {google_id} not found in database during IMAP refresh") + log_operation(logger, 'error', f"User {google_id} not found in database during IMAP refresh") debug_info["imap_error"] = f"User {google_id} not found" return user_email = user.email if not user_email: - self._log_operation('error', f"Email address not found for user {google_id}") + log_operation(logger, 'error', f"Email address not found for user {google_id}") debug_info["imap_error"] = "User email not found" return - self._log_operation('info', f"Fetching emails for {user_email}") + log_operation(logger, 'info', f"Fetching emails for {user_email}") # Get token using google_id token_data = await auth_service.get_token_data(google_id) if not token_data: - self._log_operation('error', f"No token found for user {google_id}") + log_operation(logger, 'error', f"No token found for user {google_id}") debug_info["imap_error"] = "No token found for user" return - self._log_operation('info', f"Fetching emails from IMAP for {user_email}") + log_operation(logger, 'info', f"Fetching emails from IMAP for {user_email}") imap_emails = await self.fetch_from_imap( token=token_data.token, email_account=user_email, @@ -688,18 +667,18 @@ async def _refresh_emails_from_imap(self, google_id: str, debug_info: Dict[str, limit=50 ) - self._log_operation('info', f"Retrieved {len(imap_emails)} emails from IMAP for {user_email}") + log_operation(logger, 'info', f"Retrieved {len(imap_emails)} emails from IMAP for {user_email}") for email_data in imap_emails: email_data["google_id"] = google_id await self.save_email_to_db(email_data) debug_info["imap_fetch_count"] = len(imap_emails) - self._log_operation('info', f"Saved {len(imap_emails)} emails to database for {user_email}") + log_operation(logger, 'info', f"Saved {len(imap_emails)} emails to database for {user_email}") except Exception as e: - self._log_operation('exception', f"IMAP fetch failed for user {google_id}: {str(e)}") debug_info["imap_error"] = str(e) + raise standardize_error_response(e, "refresh emails from imap", google_id) finally: debug_info["timing"]["imap_fetch_duration"] = (datetime.now() - start_time).total_seconds() diff --git a/backend/app/services/summarization/__init__.py b/backend/app/services/summarization/__init__.py index 3b67676..7bf3c1c 100644 --- a/backend/app/services/summarization/__init__.py +++ b/backend/app/services/summarization/__init__.py @@ -5,11 +5,15 @@ and strategies. """ +# Standard library imports from typing import TypeVar, Generic + +# Third-party imports from fastapi import Depends, HTTPException -from app.utils.config import Settings, get_settings, SummarizerProvider +# Internal imports from app.models import EmailSchema +from app.utils.config import Settings, get_settings, SummarizerProvider from .base import AdaptiveSummarizer from .providers.openai.openai import OpenAIEmailSummarizer from .providers.google.google import GeminiEmailSummarizer diff --git a/backend/app/services/summarization/base.py b/backend/app/services/summarization/base.py index cba3272..2cbd96a 100644 --- a/backend/app/services/summarization/base.py +++ b/backend/app/services/summarization/base.py @@ -1,9 +1,11 @@ +# Standard library imports from abc import ABC, abstractmethod from typing import Generic, List, Optional, TypeVar from datetime import datetime, timezone import asyncio -import logging +# Internal imports +from app.utils.helpers import get_logger from app.models import SummarySchema, EmailSchema from app.services.summarization.types import( ModelBackend, @@ -43,7 +45,7 @@ def __init__( self.timeout = timeout self.model_config = model_config or {} self._metrics: List[SummaryMetrics] = [] - self._logger = logging.getLogger(self.__class__.__name__) + self._logger = get_logger(self.__class__.__name__, 'service') @abstractmethod async def prepare_content(self, email: T) -> str: diff --git a/backend/app/services/summarization/prompts.py b/backend/app/services/summarization/prompts.py index dc6218a..c047347 100644 --- a/backend/app/services/summarization/prompts.py +++ b/backend/app/services/summarization/prompts.py @@ -1,9 +1,17 @@ -# summarization/providers/prompts.py +""" +Prompt management for email summarization. + +This module provides abstract base classes and concrete implementations for managing +prompts across different LLM providers. +""" + +# Standard library imports from abc import ABC, abstractmethod from typing import Optional, Protocol, Dict, Any, runtime_checkable from dataclasses import dataclass, field from enum import Enum +# Internal imports from app.utils.config import PromptVersion @dataclass diff --git a/backend/app/services/summarization/providers/google/google.py b/backend/app/services/summarization/providers/google/google.py index 39c8beb..9ec33c5 100644 --- a/backend/app/services/summarization/providers/google/google.py +++ b/backend/app/services/summarization/providers/google/google.py @@ -1,6 +1,9 @@ +# Standard library imports from typing import Dict, TypeVar, List, Optional from datetime import datetime, timezone import json + +# Third-party imports from google import genai from google.genai import types from tenacity import ( @@ -9,13 +12,13 @@ wait_exponential, retry_if_exception_type ) -# internal -from app.services.summarization.prompts import PromptManager -from app.utils.config import ProviderModel, SummarizerProvider -from app.services.summarization.providers.openai.openai import OpenAIBackend, OpenAIEmailSummarizer + +# Internal imports from app.models import SummarySchema +from app.services.summarization.prompts import PromptManager, PromptVersion +from app.services.summarization.providers.openai.openai import OpenAIBackend, OpenAIEmailSummarizer from app.services.summarization.types import ModelBackend, ModelConfig -from app.services.summarization.prompts import PromptVersion +from app.utils.config import ProviderModel, SummarizerProvider from .prompts import GeminiPromptManager diff --git a/backend/app/services/summarization/providers/google/prompts.py b/backend/app/services/summarization/providers/google/prompts.py index 7b7cf8f..adfa6af 100644 --- a/backend/app/services/summarization/providers/google/prompts.py +++ b/backend/app/services/summarization/providers/google/prompts.py @@ -1,12 +1,15 @@ -from dataclasses import dataclass, field +# Standard library imports +from dataclasses import dataclass from typing import Any, Dict, Optional + +# Internal imports from app.services.summarization.prompts import( PromptManager, + PromptTemplate, EMAIL_SUMMARY_SYSTEM_PROMPT, EMAIL_SUMMARY_USER_PROMPT ) from app.utils.config import PromptVersion -from app.services.summarization.prompts import PromptTemplate @dataclass class GeminiPromptManager(PromptManager): diff --git a/backend/app/services/summarization/providers/openai/openai.py b/backend/app/services/summarization/providers/openai/openai.py index 9d576d1..3ea8655 100644 --- a/backend/app/services/summarization/providers/openai/openai.py +++ b/backend/app/services/summarization/providers/openai/openai.py @@ -1,7 +1,10 @@ +# Standard library imports from typing import List, Optional, Dict, TypeVar from datetime import datetime, timezone import asyncio import json + +# Third-party imports from openai import ( RateLimitError, APITimeoutError, @@ -14,14 +17,14 @@ wait_exponential, retry_if_exception_type ) -# internal -from app.services.summarization.base import AdaptiveSummarizer -from app.services.summarization.types import ModelBackend, ModelConfig + +# Internal imports from app.models import EmailSchema, SummarySchema -from app.utils.config import ProviderModel, SummarizerProvider +from app.services.summarization.base import AdaptiveSummarizer from app.services.summarization.prompts import PromptManager +from app.services.summarization.types import ModelBackend, ModelConfig +from app.utils.config import ProviderModel, SummarizerProvider, PromptVersion from .prompts import OpenAIPromptManager -from app.utils.config import PromptVersion T = TypeVar('T') diff --git a/backend/app/services/summarization/providers/openai/prompts.py b/backend/app/services/summarization/providers/openai/prompts.py index 85ab042..85872ad 100644 --- a/backend/app/services/summarization/providers/openai/prompts.py +++ b/backend/app/services/summarization/providers/openai/prompts.py @@ -1,5 +1,8 @@ -from dataclasses import dataclass, field +# Standard library imports +from dataclasses import dataclass from typing import Any, Dict, Optional + +# Internal imports from app.services.summarization.prompts import( PromptManager, EMAIL_SUMMARY_SYSTEM_PROMPT, diff --git a/backend/app/services/summarization/summary_service.py b/backend/app/services/summarization/summary_service.py index f66b770..622eb92 100644 --- a/backend/app/services/summarization/summary_service.py +++ b/backend/app/services/summarization/summary_service.py @@ -2,13 +2,16 @@ Service for handling email summarization operations. """ -import logging +# Standard library imports from typing import List, Optional, Dict, Any from datetime import datetime, timezone, timedelta -from fastapi import HTTPException, status -from fastapi import Depends + +# Third-party imports +from fastapi import HTTPException, status, Depends from bson import ObjectId +# Internal imports +from app.utils.helpers import get_logger, log_operation, standardize_error_response from app.models import EmailSchema, SummarySchema from app.services.database.repositories.summary_repository import SummaryRepository from app.services.database.factories import get_summary_repository, get_email_service @@ -19,15 +22,11 @@ GeminiEmailSummarizer ) -# Configure logging : redundant with the logging in the main file -# logging.basicConfig( -# level=logging.DEBUG, -# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# datefmt='%Y-%m-%d %H:%M:%S' -# ) +# ------------------------------------------------------------------------- +# Configuration +# ------------------------------------------------------------------------- -# Create module-specific logger -logger = logging.getLogger(__name__) +logger = get_logger(__name__, 'service') class SummaryService: """ @@ -61,10 +60,9 @@ async def initialize(self): await self.summary_repository.create_index("generated_at") # For time-based queries await self.summary_repository.create_index("google_id") # For user-specific summaries - logger.info("Summary collection indexes initialized") + log_operation(logger, 'info', "Summary collection indexes initialized") except Exception as e: - logger.error(f"Failed to initialize summary indexes: {e}") - raise + raise standardize_error_response(e, "initialize summary indexes") async def save_summary(self, summary: SummarySchema, google_id: str) -> str: """ @@ -101,12 +99,11 @@ async def save_summary(self, summary: SummarySchema, google_id: str) -> str: if not result: raise Exception("Failed to save summary") - logger.debug(f"Summary saved for email {summary.email_id} for user {google_id}") + log_operation(logger, 'debug', f"Summary saved for email {summary.email_id} for user {google_id}") return summary.email_id except Exception as e: - logger.error(f"Failed to save summary: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "save summary", summary.email_id) async def get_summary(self, email_id: str, google_id: str) -> Optional[SummarySchema]: """ @@ -132,8 +129,7 @@ async def get_summary(self, email_id: str, google_id: str) -> Optional[SummarySc # Otherwise create a new SummarySchema instance return SummarySchema(**result) except Exception as e: - logger.error(f"Failed to retrieve summary for email {email_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "get summary", email_id) async def get_summaries( self, @@ -177,8 +173,7 @@ async def get_summaries( # Convert results to SummarySchema objects if needed return [result if isinstance(result, SummarySchema) else SummarySchema(**result) for result in results] except Exception as e: - logger.error(f"Failed to retrieve summaries: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "get summaries") async def search_by_keywords( self, @@ -208,8 +203,7 @@ async def search_by_keywords( results = await self.summary_repository.find_many(query, limit=limit) return [result if isinstance(result, SummarySchema) else SummarySchema(**result) for result in results] except Exception as e: - logger.error(f"Failed to search summaries by keywords: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "search summaries by keywords") async def get_recent_summaries( self, @@ -246,7 +240,7 @@ async def get_recent_summaries( if google_id: query["google_id"] = google_id - logger.debug(f"Querying summaries between {cutoff_date.isoformat()} and {now.isoformat()}") + log_operation(logger, 'debug', f"Querying summaries between {cutoff_date.isoformat()} and {now.isoformat()}") results = await self.summary_repository.find_many( query, @@ -254,11 +248,10 @@ async def get_recent_summaries( sort=[("generated_at", -1)] ) - logger.debug(f"Found {len(results)} summaries matching query") + log_operation(logger, 'debug', f"Found {len(results)} summaries matching query") return [result if isinstance(result, SummarySchema) else SummarySchema(**result) for result in results] except Exception as e: - logger.error(f"Failed to retrieve recent summaries: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "get recent summaries") async def delete_summary(self, email_id: str, google_id: str) -> bool: """ @@ -279,14 +272,13 @@ async def delete_summary(self, email_id: str, google_id: str) -> bool: deleted = result.deleted_count > 0 if deleted: - logger.info(f"Summary for email {email_id} deleted for user {google_id}") + log_operation(logger, 'info', f"Summary for email {email_id} deleted for user {google_id}") else: - logger.info(f"No summary found for email {email_id} for user {google_id} to delete") + log_operation(logger, 'info', f"No summary found for email {email_id} for user {google_id} to delete") return deleted except Exception as e: - logger.error(f"Failed to delete summary for email {email_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "delete summary", email_id) async def save_summaries_batch(self, summaries: List[SummarySchema], google_id: str) -> Dict[str, int]: """ @@ -326,14 +318,13 @@ async def save_summaries_batch(self, summaries: List[SummarySchema], google_id: "inserted": result.upserted_count, "modified": result.modified_count } - logger.info(f"Batch summary save: {result.upserted_count} inserted, " + log_operation(logger, 'info', f"Batch summary save: {result.upserted_count} inserted, " f"{result.modified_count} modified for user {google_id}") return stats return {"inserted": 0, "modified": 0} except Exception as e: - logger.error(f"Error in batch saving summaries: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "save summaries batch") async def count_summaries(self, query: Dict = None, google_id: str = None) -> int: """ @@ -355,8 +346,7 @@ async def count_summaries(self, query: Dict = None, google_id: str = None) -> in query["google_id"] = google_id return await self.summary_repository.count_documents(query) except Exception as e: - logger.error(f"Failed to count summaries: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "count summaries") async def get_summaries_by_ids(self, email_ids: List[str], google_id: str) -> List[SummarySchema]: """ @@ -389,12 +379,11 @@ async def get_summaries_by_ids(self, email_ids: List[str], google_id: str) -> Li summaries.append(SummarySchema(**doc)) # Log how many were found - logger.debug(f"Found {len(summaries)} summaries out of {len(email_ids)} requested for user {google_id}") + log_operation(logger, 'debug', f"Found {len(summaries)} summaries out of {len(email_ids)} requested for user {google_id}") return summaries except Exception as e: - logger.error(f"Failed to retrieve summaries by IDs: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "get summaries by ids") async def get_or_create_summary( self, @@ -422,7 +411,7 @@ async def get_or_create_summary( # Get email data email = await self.email_service.get_email(email_id, google_id) if not email: - logger.warning(f"Email {email_id} not found for user {google_id}") + log_operation(logger, 'warning', f"Email {email_id} not found for user {google_id}") return None # Generate summary using EmailSchema directly @@ -432,7 +421,7 @@ async def get_or_create_summary( ) if not summaries: - logger.warning(f"Failed to generate summary for email {email_id}") + log_operation(logger, 'warning', f"Failed to generate summary for email {email_id}") return None # Create a new SummarySchema with the google_id @@ -442,13 +431,12 @@ async def get_or_create_summary( # Store summary await self.save_summary(summary, google_id) - logger.info(f"Created new summary for email {email_id}") + log_operation(logger, 'info', f"Created new summary for email {email_id}") return summary.model_dump() except Exception as e: - logger.error(f"Failed to get or create summary for email {email_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise standardize_error_response(e, "get or create summary", email_id) async def get_or_create_summaries_batch( self, @@ -509,10 +497,10 @@ async def get_or_create_summaries_batch( missing_emails.append(email) else: failed_emails.append(email_id) - logger.warning(f"Email {email_id} not found for user {google_id}") + log_operation(logger, 'warning', f"Email {email_id} not found for user {google_id}") except Exception as e: failed_emails.append(email_id) - logger.warning(f"Error fetching email {email_id}: {e}") + log_operation(logger, 'warning', f"Error fetching email {email_id}: {e}") if missing_emails: # Generate summaries for missing emails @@ -527,7 +515,7 @@ async def get_or_create_summaries_batch( await self.save_summaries_batch(new_summaries, google_id) all_summaries.extend(new_summaries) except Exception as e: - logger.error(f"Failed to generate summaries for batch: {e}") + log_operation(logger, 'error', f"Failed to generate summaries for batch: {e}") all_failed_summaries.extend([email.email_id for email in missing_emails]) continue @@ -537,13 +525,13 @@ async def get_or_create_summaries_batch( all_summaries.extend(existing_summaries) except Exception as e: - logger.error(f"Error processing batch {i//batch_size + 1}: {e}") + log_operation(logger, 'error', f"Error processing batch {i//batch_size + 1}: {e}") # Mark all emails in this batch as failed all_failed_summaries.extend(batch_ids) continue # Log final results - logger.info( + log_operation(logger, 'info', f"Batch summary results for user {google_id}: " f"{len(all_summaries)} successful, {len(all_missing_emails)} missing emails, " f"{len(all_failed_summaries)} failed summaries" @@ -556,8 +544,4 @@ async def get_or_create_summaries_batch( } except Exception as e: - logger.error(f"Failed to process batch summaries: {e}") - raise HTTPException( - status_code=500, - detail=f"Failed to process summaries: {str(e)}" - ) \ No newline at end of file + raise standardize_error_response(e, "process batch summaries") \ No newline at end of file diff --git a/backend/app/services/summarization/types.py b/backend/app/services/summarization/types.py index 1360618..ddd2e0d 100644 --- a/backend/app/services/summarization/types.py +++ b/backend/app/services/summarization/types.py @@ -1,9 +1,12 @@ +# Standard library imports from typing import Protocol, List, Optional, Dict from enum import Enum, auto -from pydantic import Field from dataclasses import dataclass from datetime import datetime, timezone +# Third-party imports +from pydantic import Field + ModelConfig = Dict[str, any] class ProcessingStrategy(Enum): diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 1764b0d..eb34db6 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -2,22 +2,22 @@ User service for handling user-related operations. """ -import logging +# Standard library imports from typing import Optional, Dict, Any + +# Third-party imports from fastapi import HTTPException, status -# Import from app modules +# Internal imports +from app.utils.helpers import get_logger, log_operation, standardize_error_response from app.models import UserSchema, PreferencesSchema from app.services.database import get_user_repository, UserRepository -# Configure logging : redundant with the logging in the main file -# logging.basicConfig( -# level=logging.DEBUG, -# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# datefmt='%Y-%m-%d %H:%M:%S' -# ) +# ------------------------------------------------------------------------- +# Configuration +# ------------------------------------------------------------------------- -logger = logging.getLogger(__name__) +logger = get_logger(__name__, 'service') class UserService: """ @@ -36,10 +36,7 @@ def __init__(self, user_repository: UserRepository = None): Args: user_repository: User repository instance """ - # TODO Make sure this is a good way to do this.. self.user_repository = user_repository or get_user_repository() - # Note: We can't call ensure_indexes here because it's async - # The indexes will be created on first use async def get_user(self, user_id: str) -> Optional[UserSchema]: """ @@ -54,11 +51,7 @@ async def get_user(self, user_id: str) -> Optional[UserSchema]: try: return await self.user_repository.find_by_id(user_id) except Exception as e: - logger.error(f"Failed to get user: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get user" - ) + raise standardize_error_response(e, "get user", user_id) async def get_user_by_email(self, email: str) -> Optional[UserSchema]: """ @@ -73,11 +66,7 @@ async def get_user_by_email(self, email: str) -> Optional[UserSchema]: try: return await self.user_repository.find_by_email(email) except Exception as e: - logger.error(f"Failed to get user by email: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get user by email" - ) + raise standardize_error_response(e, "get user by email", email) async def create_user(self, user_data: Dict[str, Any]) -> UserSchema: """ @@ -89,7 +78,6 @@ async def create_user(self, user_data: Dict[str, Any]) -> UserSchema: Returns: Created user """ - try: # Create a complete user data dictionary with all required fields complete_user_data = { @@ -117,20 +105,17 @@ async def create_user(self, user_data: Dict[str, Any]) -> UserSchema: user_id = await self.user_repository.insert_one(user) user._id = user_id + log_operation(logger, 'info', f"Created user: {user.email}") return user except Exception as e: - logger.error(f"Failed to create user: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to create user" - ) + raise standardize_error_response(e, "create user", user_data.get("email")) async def update_user(self, google_id: str, user_data: Dict[str, Any]) -> Optional[UserSchema]: """ Update a user. Args: - user_id: User ID + google_id: User Google ID user_data: Updated user data Returns: @@ -140,47 +125,43 @@ async def update_user(self, google_id: str, user_data: Dict[str, Any]) -> Option # First get the current user to ensure it exists current_user = await self.user_repository.find_by_id(google_id) if not current_user: - logger.error(f"User not found: {google_id}") + log_operation(logger, 'warning', f"User not found: {google_id}") return None # Update the user success = await self.user_repository.update_one(google_id, user_data) if not success: - logger.error(f"Update failed for user: {google_id}") + log_operation(logger, 'warning', f"Update failed for user: {google_id}") return None # Get the updated user updated_user = await self.user_repository.find_by_id(google_id) if not updated_user: - logger.error(f"Failed to fetch updated user: {google_id}") + log_operation(logger, 'warning', f"Failed to fetch updated user: {google_id}") return None + log_operation(logger, 'info', f"Updated user: {google_id}") return UserSchema(**updated_user) except Exception as e: - logger.error(f"Failed to update user: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update user" - ) + raise standardize_error_response(e, "update user", google_id) async def delete_user(self, google_id: str) -> bool: """ Delete a user. Args: - google_id: User ID + google_id: User Google ID Returns: True if deleted, False otherwise """ try: - return await self.user_repository.delete_one(google_id) + result = await self.user_repository.delete_one(google_id) + if result: + log_operation(logger, 'info', f"Deleted user: {google_id}") + return result except Exception as e: - logger.error(f"Failed to delete user: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to delete user" - ) + raise standardize_error_response(e, "delete user", google_id) async def get_preferences(self, google_id: str) -> Dict[str, Any]: """ @@ -193,15 +174,11 @@ async def get_preferences(self, google_id: str) -> Dict[str, Any]: Dict[str, Any]: User preferences """ try: - logger.debug(f"Fetching preferences for Google ID: {google_id}") + log_operation(logger, 'debug', f"Fetching preferences for Google ID: {google_id}") user = await self.user_repository.find_one({"google_id": google_id}) return user.get("preferences", {}) if user else {} except Exception as e: - logger.error(f"Failed to get preferences: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get preferences" - ) + raise standardize_error_response(e, "get preferences", google_id) async def update_preferences(self, google_id: str, preferences: Dict[str, Any]) -> bool: """ @@ -215,15 +192,13 @@ async def update_preferences(self, google_id: str, preferences: Dict[str, Any]) bool: True if update successful """ try: - logger.debug(f"Updating preferences for Google ID: {google_id}") + log_operation(logger, 'debug', f"Updating preferences for Google ID: {google_id}") result = await self.user_repository.update_one( - {"google_id": google_id}, - {"$set": {"preferences": preferences}} - ) + {"google_id": google_id}, + {"$set": {"preferences": preferences}} + ) + if result: + log_operation(logger, 'info', f"Updated preferences for user: {google_id}") return result except Exception as e: - logger.error(f"Failed to update preferences: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update preferences" - ) + raise standardize_error_response(e, "update preferences", google_id) diff --git a/backend/app/utils/helpers.py b/backend/app/utils/helpers.py new file mode 100644 index 0000000..179e700 --- /dev/null +++ b/backend/app/utils/helpers.py @@ -0,0 +1,136 @@ +""" +Helper functions for Email Essence FastAPI application. + +This module contains shared utilities for logging and error handling that can be +used across the application without creating circular dependencies. +""" + +# Standard library imports +import logging +import os +from typing import Optional + +# Third-party imports +from fastapi import HTTPException, status + + +def _get_logging_level_config() -> dict: + """ + Get logging level configuration based on environment. + + Returns: + dict: Mapping of module types to logging levels + """ + environment = os.getenv('ENVIRONMENT', 'production').lower() + + # Logging level configuration by environment + config = { + 'development': { + 'router': logging.DEBUG, + 'service': logging.DEBUG, + 'default': logging.DEBUG + }, + 'production': { + 'router': logging.INFO, + 'service': logging.INFO, + 'default': logging.INFO + }, + 'testing': { + 'router': logging.WARNING, + 'service': logging.WARNING, + 'default': logging.WARNING + } + } + + return config.get(environment, config['production']) + + +def get_logger(name: str, module_type: str = 'default') -> logging.Logger: + """ + Get a configured logger instance with appropriate level based on environment. + + Args: + name: Logger name (typically __name__) + module_type: Type of module ('router', 'service', or 'default') + + Returns: + logging.Logger: Configured logger instance + """ + logger = logging.getLogger(name) + + # Get appropriate logging level based on environment and module type + level_config = _get_logging_level_config() + logging_level = level_config.get(module_type, level_config['default']) + + logger.setLevel(logging_level) + return logger + + +def configure_module_logging(module_name: str, module_type: str = 'default') -> logging.Logger: + """ + Configure logging for a specific module with environment-based levels. + + Args: + module_name: Name of the module + module_type: Type of module ('router', 'service', or 'default') + + Returns: + logging.Logger: Configured logger instance + """ + return get_logger(module_name, module_type) + + +def standardize_error_response( + error: Exception, + operation: str, + resource_id: str = None, + user_id: str = None +) -> HTTPException: + """ + Standardize error responses across the application. + + Args: + error: The original exception + operation: Description of the operation that failed + resource_id: Optional resource identifier + user_id: Optional user identifier + + Returns: + HTTPException: Standardized HTTP exception + """ + error_msg = f"Failed to {operation}" + if resource_id: + error_msg += f" resource {resource_id}" + if user_id: + error_msg += f" for user {user_id}" + + # Log the original error for debugging + logger = get_logger(__name__, 'default') + logger.exception(f"{error_msg}: {str(error)}") + + return HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=error_msg + ) + + +def log_operation( + logger: logging.Logger, + level: str, + message: str, + **kwargs +) -> None: + """ + Standardize logging across services. + + Args: + logger: Logger instance to use + level: Log level (debug, info, warning, error, exception) + message: Log message + **kwargs: Additional context for logging + """ + log_method = getattr(logger, level.lower()) + if kwargs: + log_method(message, extra=kwargs) + else: + log_method(message) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index acf1034..2c312b9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,10 +1,22 @@ # uvicorn main:app --reload + +# Standard library imports import os +from contextlib import asynccontextmanager +import logging + +# Third-party imports from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware -from contextlib import asynccontextmanager -import logging + +# Internal imports +from app.routers import emails_router, summaries_router, auth_router, user_router +from app.services.database.connection import DatabaseConnection + +# ------------------------------------------------------------------------- +# Logging Configuration +# ------------------------------------------------------------------------- # Configure logging logging.basicConfig(level=logging.INFO) @@ -13,11 +25,9 @@ logging.getLogger("httpcore").setLevel(logging.WARNING) logger = logging.getLogger(__name__) -from app.routers import emails_router, summaries_router, auth_router, user_router -from app.services.database.connection import DatabaseConnection - - -# from app.models.user_model import User +# ------------------------------------------------------------------------- +# Database Lifecycle Management +# ------------------------------------------------------------------------- async def startup_db_client(): """ @@ -53,6 +63,10 @@ async def lifespan(app: FastAPI): yield await shutdown_db_client() +# ------------------------------------------------------------------------- +# FastAPI Application Setup +# ------------------------------------------------------------------------- + app = FastAPI( title="Email Essence API", description="API for the Email Essence application", @@ -62,6 +76,9 @@ async def lifespan(app: FastAPI): lifespan=lifespan ) +# ------------------------------------------------------------------------- +# Middleware Configuration +# ------------------------------------------------------------------------- # Configure CORS app.add_middleware( @@ -83,7 +100,10 @@ async def lifespan(app: FastAPI): allow_headers=["*"], # Allows all headers ) -# API Route Handlers (definitions moved before router inclusions) +# ------------------------------------------------------------------------- +# API Route Handlers +# ------------------------------------------------------------------------- + @app.get("/", tags=["Root"]) async def root(): """ @@ -165,7 +185,10 @@ async def health_check(): return health_status -# Register Routers +# ------------------------------------------------------------------------- +# Router Registration +# ------------------------------------------------------------------------- + app.include_router(auth_router, prefix="/auth", tags=["Auth"]) app.include_router(user_router, prefix="/user", tags=["User"]) app.include_router(emails_router, prefix="/emails", tags=["Emails"]) From 515a62337cd4e94c571aaf4cb070e7e0f131d832 Mon Sep 17 00:00:00 2001 From: Joseph Madigan Date: Tue, 10 Jun 2025 23:21:09 -0700 Subject: [PATCH 04/33] delete by google id.. --- backend/app/services/user_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index eb34db6..64f4b1c 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -156,7 +156,7 @@ async def delete_user(self, google_id: str) -> bool: True if deleted, False otherwise """ try: - result = await self.user_repository.delete_one(google_id) + result = await self.user_repository.delete_by_google_id(google_id) if result: log_operation(logger, 'info', f"Deleted user: {google_id}") return result From 7baaffa017b6ab602978c833b8cad19ebf568e6e Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Thu, 29 May 2025 00:15:59 -0700 Subject: [PATCH 05/33] fixing issue with client import error --- frontend/src/components/client/client.jsx | 2 +- .../components/client/{ => dashboard}/client.css | 0 frontend/src/components/login/Home.css | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) rename frontend/src/components/client/{ => dashboard}/client.css (100%) diff --git a/frontend/src/components/client/client.jsx b/frontend/src/components/client/client.jsx index 1e6e2f3..2003788 100644 --- a/frontend/src/components/client/client.jsx +++ b/frontend/src/components/client/client.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useEffect, useReducer } from "react"; import { Outlet, Route, Routes, useNavigate } from "react-router"; import { fetchNewEmails } from "../../emails/emailHandler"; -import "./client.css"; +import "../client/dashboard/client.css"; import Dashboard from "./dashboard/dashboard"; import Inbox from "./inbox/inbox"; import { clientReducer, userPreferencesReducer } from "./reducers"; diff --git a/frontend/src/components/client/client.css b/frontend/src/components/client/dashboard/client.css similarity index 100% rename from frontend/src/components/client/client.css rename to frontend/src/components/client/dashboard/client.css diff --git a/frontend/src/components/login/Home.css b/frontend/src/components/login/Home.css index ff508fb..731bdf7 100644 --- a/frontend/src/components/login/Home.css +++ b/frontend/src/components/login/Home.css @@ -1,5 +1,20 @@ @import url("../../main.css"); +/* html, +body { + margin: 0; + padding: 0; + width: 250vw; + overflow-x: hidden; +} */ + +.home { + height: 100vh; + background-color: var(--grey-split); + padding: calc(2rem + 1vh) calc(5rem + 4vw); +} + + /* html, body { margin: 0; From 2d9bd73a6f542eed2f5540936ba0433c17c369dd Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Fri, 30 May 2025 16:50:11 -0700 Subject: [PATCH 06/33] created a logout function --- .../components/client/settings/settings.jsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index 1314136..92ba996 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { baseUrl } from "../../../emails/emailHandler"; import "./settings.css"; @@ -11,6 +11,11 @@ export function Settings({ handleSetTheme, }) { const isDarkTheme = useSystemTheme(); + const handleLogout = () => { + localStorage.removeItem("auth_token"); + window.location.href = "/login"; + }; + // useEffect that sets the dark mode class when the theme is set to system useEffect(() => { if (theme === "system") { @@ -34,6 +39,7 @@ export function Settings({ onSetEmailFetchInterval={handleSetEmailFetchInterval} /> +
); } @@ -121,6 +127,18 @@ export function Theme({ theme, onSetTheme }) { ); } +export function Logout({onLogout}) { + return ( +
{ if (e.key === "Enter" || e.key === " ") onLogout(); }} + > + Log Out +
+ ); +} + const useSystemTheme = () => { const getCurrentTheme = () => window.matchMedia("(prefers-color-scheme: dark)").matches; From b569dbcfd08c9185978eeeae7d7a96a6a15578a3 Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Fri, 30 May 2025 17:06:50 -0700 Subject: [PATCH 07/33] Css for logout --- .../components/client/settings/settings.css | 36 +++++++++++++++++-- .../components/client/settings/settings.jsx | 4 +-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/client/settings/settings.css b/frontend/src/components/client/settings/settings.css index 6a8430f..98b17eb 100644 --- a/frontend/src/components/client/settings/settings.css +++ b/frontend/src/components/client/settings/settings.css @@ -83,12 +83,12 @@ h2 { } /* When the checkbox is checked, add a background color */ -.switch input:checked + .toggle { +.switch input:checked+.toggle { background-color: var(--color-toggle-border); } /* Move the slider when the checkbox is checked */ -.switch input:checked + .toggle:before { +.switch input:checked+.toggle:before { transform: translateX(calc(1rem + 0.5vw)); left: 75%; transform: translate(-50%, -50%); @@ -221,3 +221,35 @@ h2 { outline-offset: 2px; } +.logout { + position: absolute; + right: 2rem; + bottom: 2rem; + margin: 0; + color: var(--color-button-text); + cursor: pointer; + justify-content: center; + transition: background 0.2s; + height: auto; + flex-shrink: 0; + padding: calc(0.4rem + 0.55vw) 20px; + border: 1px solid var(--color-border); + border-radius: 4px; + background: var(--color-background); + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + display: flex; + align-items: center; +} + +.logout-block:hover, +.logout-block:focus { + background: var(--color-hover-background); +} + +.logout-text { + color: var(--color-text); + font-family: var(--font-family); + font-size: var(--font-size); + font-weight: var(--font-weight-bold); + line-height: normal; +} \ No newline at end of file diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index 92ba996..e78477f 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -129,12 +129,12 @@ export function Theme({ theme, onSetTheme }) { export function Logout({onLogout}) { return ( -
{ if (e.key === "Enter" || e.key === " ") onLogout(); }} > - Log Out + Logout
); } From 7a4c73b68ec7a60a12446eea90fa84a98149d5ff Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Sat, 31 May 2025 21:45:32 -0700 Subject: [PATCH 08/33] changed logout button to a button div --- frontend/src/components/client/settings/settings.jsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index e78477f..8e812c3 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -129,16 +129,14 @@ export function Theme({ theme, onSetTheme }) { export function Logout({onLogout}) { return ( -
{ if (e.key === "Enter" || e.key === " ") onLogout(); }} - > +
+ ); } + + const useSystemTheme = () => { const getCurrentTheme = () => window.matchMedia("(prefers-color-scheme: dark)").matches; From 7b12a6ef6877d2a81a7d4404dfd0ff97f2e9b906 Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Sat, 31 May 2025 23:50:43 -0700 Subject: [PATCH 09/33] shifted things, created an empty delete account button --- .../components/client/settings/settings.jsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index 8e812c3..0fde5b5 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -10,12 +10,21 @@ export function Settings({ theme, handleSetTheme, }) { - const isDarkTheme = useSystemTheme(); const handleLogout = () => { localStorage.removeItem("auth_token"); window.location.href = "/login"; }; + const handleDeleteAccount = () => { + //confirm if the user wants to delete their account + if (window.confirm("Are you sure you want to delete your account? This action is permanent and cannot be undone.")) return; + try { + + + } catch (error) { }; + } + + const isDarkTheme = useSystemTheme(); // useEffect that sets the dark mode class when the theme is set to system useEffect(() => { if (theme === "system") { @@ -39,7 +48,10 @@ export function Settings({ onSetEmailFetchInterval={handleSetEmailFetchInterval} /> - +
+ + +
); } @@ -127,7 +139,7 @@ export function Theme({ theme, onSetTheme }) { ); } -export function Logout({onLogout}) { +export function Logout({ onLogout }) { return ( + ); +} const useSystemTheme = () => { const getCurrentTheme = () => From 6121c299790045b7e0b4bc807d609b1a5ddbf0c5 Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Sat, 31 May 2025 23:51:01 -0700 Subject: [PATCH 10/33] the css for the button --- .../components/client/settings/settings.css | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/client/settings/settings.css b/frontend/src/components/client/settings/settings.css index 98b17eb..912c386 100644 --- a/frontend/src/components/client/settings/settings.css +++ b/frontend/src/components/client/settings/settings.css @@ -221,10 +221,17 @@ h2 { outline-offset: 2px; } -.logout { +.settings-account-actions { position: absolute; right: 2rem; bottom: 2rem; + display: flex; + gap: 1rem; +} + +.logout, +.delete-account { + position: static; margin: 0; color: var(--color-button-text); cursor: pointer; @@ -241,12 +248,15 @@ h2 { align-items: center; } -.logout-block:hover, -.logout-block:focus { +.logout:hover, +.logout:focus, +.delete-account:hover, +.delete-account:focus { background: var(--color-hover-background); } -.logout-text { +.logout-text, +.delete-account-text { color: var(--color-text); font-family: var(--font-family); font-size: var(--font-size); From 049d27a595af21a50f3f4bbbd7380fe8ade39b47 Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Sun, 8 Jun 2025 22:57:34 -0700 Subject: [PATCH 11/33] still needs testing with a temp account and verifying --- .../components/client/settings/settings.jsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index 0fde5b5..f821fac 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -15,13 +15,20 @@ export function Settings({ window.location.href = "/login"; }; - const handleDeleteAccount = () => { + const handleDeleteAccount = async() => { //confirm if the user wants to delete their account if (window.confirm("Are you sure you want to delete your account? This action is permanent and cannot be undone.")) return; try { + const profile = await fetchUserProfile(); + const userId = profile.id || profile.user_id; + await deleteUserById(userId); - - } catch (error) { }; + localStorage.removeItem("auth_token"); + window.location.href = "/login"; + } catch (error) { + console.error("Failed to delete account:", error); + alert("Failed to delete account. Please try again later."); + }; } const isDarkTheme = useSystemTheme(); @@ -242,14 +249,15 @@ export const updateUserById = async (user_id) => { // @router.delete( "/user_id"), deletes user, deletes user account by user ID export const deleteUserById = async (user_id) => { - const response = await fetch("${baseUrl}/user/${user_id}", { + const response = await fetch(`${baseUrl}/user/${user_id}`, { + method: "DELETE", headers: { - Authorization: "Bearer ${token}", + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!response.ok) { - throw new Error("Failed to delete user by ID ${response.status}"); + throw new Error(`Failed to delete user by ID ${response.status}`); } return response.json(); }; From 60087e01ec6a37b94703149535358b94a0a1b9ec Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:23:09 -0700 Subject: [PATCH 12/33] fixs to the API functions; replaced " with ` and added const token --- .../components/client/settings/settings.jsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index f821fac..070e2b6 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -17,7 +17,7 @@ export function Settings({ const handleDeleteAccount = async() => { //confirm if the user wants to delete their account - if (window.confirm("Are you sure you want to delete your account? This action is permanent and cannot be undone.")) return; + if (!window.confirm("Are you sure you want to delete your account? This action is permanent and cannot be undone.")) return; try { const profile = await fetchUserProfile(); const userId = profile.id || profile.user_id; @@ -182,8 +182,12 @@ const useSystemTheme = () => { // @router.get("/me"), gets current users profile, Retrieves the authenticated user's profile export const fetchUserProfile = async () => { + const token = localStorage.getItem("auth_token"); const response = await fetch(`${baseUrl}/user/me`, { - headers: {}, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, }); if (!response.ok) { throw new Error(`Failed to fetch users profile ${response.status}`); @@ -193,9 +197,10 @@ export const fetchUserProfile = async () => { // @router.get("/preferences"), gets the user preferences, Retrieves the authenticated user's preferences settings export const fetchUserPreferences = async () => { + const token = localStorage.getItem("auth_token"); const response = await fetch(`${baseUrl}/user/preferences`, { headers: { - Authorization: "Bearer ${token}", + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); @@ -207,48 +212,52 @@ export const fetchUserPreferences = async () => { // @router.put("/preferences"), updates the user preferences, Updates the authenticated user's preferences settings export const updateUserPreferences = async () => { - const response = await fetch("${baseUrl}/user/preferences", { + const token = localStorage.getItem("auth_token"); + const response = await fetch(`${baseUrl}/user/preferences`, { headers: { - Authorization: "Bearer ${token}", + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!response.ok) { - throw new Error("Failed to update user preferences ${response.status}"); + throw new Error(`Failed to update user preferences ${response.status}`); } return response.json(); }; // @router.get ("/user_id"). gets user by ID, retrieves user information by user ID -export const fetchUserById = async (user_id) => { - const response = await fetch("${baseUrl}/user/${user_id}", { +export const fetchUserById = async () => { + const token = localStorage.getItem("auth_token"); + const response = await fetch(`${baseUrl}/user/${user_id}`, { headers: { - Authorization: "Bearer ${token}", + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!response.ok) { - throw new Error("Failed to fetch user by ID ${response.status}"); + throw new Error(`Failed to fetch user by ID ${response.status}`); } return response.json(); }; // @router.put("/user_id"), updates user, updates user information by user ID export const updateUserById = async (user_id) => { - const response = await fetch("${baseUrl}/user/${user_id}", { + const token = localStorage.getItem("auth_token"); + const response = await fetch(`${baseUrl}/user/${user_id}`, { headers: { - Authorization: "Bearer ${token}", + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!response.ok) { - throw new Error("Failed to update user by ID ${response.status}"); + throw new Error(`Failed to update user by ID ${response.status}`); } return response.json(); }; // @router.delete( "/user_id"), deletes user, deletes user account by user ID export const deleteUserById = async (user_id) => { + const token = localStorage.getItem("auth_token"); const response = await fetch(`${baseUrl}/user/${user_id}`, { method: "DELETE", headers: { @@ -262,22 +271,17 @@ export const deleteUserById = async (user_id) => { return response.json(); }; -// function that gets the user preferences from the backend -// export const fetchUserPreferences = async (user_id) => { -// const response = await fetch(`http://localhost:8000/preferences`); -// if (!response.ok) { -// throw new Error(`Failed to fetch ${response.status}`); -// } -// return response.json(); -// } - // function saves the user preferences to the backend -export const saveUserPreferences = async (userPreferences) => { +export const saveUserPreferences = async (user_id, userPreferences) => { + const token = localStorage.getItem("auth_token"); const response = await fetch( - `http://localhost:8000/user/${user_id}/preferences`, + `${baseUrl}/user/${user_id}/preferences`, { method: "PUT", - headers: { "Content-Type": "application/json" }, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json" + }, body: JSON.stringify(userPreferences), } ); From 17928ebe2bb87dc4c8f6eaff66b3da56499332c5 Mon Sep 17 00:00:00 2001 From: riteshsamal-ship <82118620+riteshsamal-ship@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:47:44 -0700 Subject: [PATCH 13/33] Update HandleDeleteAccount Change userId to hold google ID --- frontend/src/components/client/settings/settings.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index 070e2b6..f99812e 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -18,9 +18,9 @@ export function Settings({ const handleDeleteAccount = async() => { //confirm if the user wants to delete their account if (!window.confirm("Are you sure you want to delete your account? This action is permanent and cannot be undone.")) return; - try { + try{ const profile = await fetchUserProfile(); - const userId = profile.id || profile.user_id; + const userId = profile.google_id await deleteUserById(userId); localStorage.removeItem("auth_token"); @@ -258,6 +258,7 @@ export const updateUserById = async (user_id) => { // @router.delete( "/user_id"), deletes user, deletes user account by user ID export const deleteUserById = async (user_id) => { const token = localStorage.getItem("auth_token"); + const response = await fetch(`${baseUrl}/user/${user_id}`, { method: "DELETE", headers: { From 0991563afc9c206f002ebc682cd9673c48e11b52 Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:24:09 -0700 Subject: [PATCH 14/33] edit wording --- frontend/src/components/client/settings/settings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/client/settings/settings.jsx b/frontend/src/components/client/settings/settings.jsx index f99812e..fd09261 100644 --- a/frontend/src/components/client/settings/settings.jsx +++ b/frontend/src/components/client/settings/settings.jsx @@ -17,7 +17,7 @@ export function Settings({ const handleDeleteAccount = async() => { //confirm if the user wants to delete their account - if (!window.confirm("Are you sure you want to delete your account? This action is permanent and cannot be undone.")) return; + if (!window.confirm("Are you sure you want to delete your EmailEssence Account? \nThis will remove all information associated with your gmail account from our server and lead to longer loading times when you log back in next time.")) return; try{ const profile = await fetchUserProfile(); const userId = profile.google_id From a31a4ef509d792b7785264e5fa6c943f3b32136b Mon Sep 17 00:00:00 2001 From: riteshsamal-ship <82118620+riteshsamal-ship@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:24:20 -0700 Subject: [PATCH 15/33] Deletion of Emails Added deletion of emails from database when deleting account --- backend/app/routers/user_router.py | 19 +++++++++++++++---- .../database/repositories/base_repository.py | 16 ++++++++++++++++ .../database/repositories/email_repository.py | 12 ++++++++++++ backend/app/services/email_service.py | 17 +++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/backend/app/routers/user_router.py b/backend/app/routers/user_router.py index 1e1866a..7dd7983 100644 --- a/backend/app/routers/user_router.py +++ b/backend/app/routers/user_router.py @@ -13,8 +13,9 @@ from app.utils.helpers import get_logger, log_operation, standardize_error_response from app.models import PreferencesSchema, UserSchema from app.services.auth_service import AuthService -from app.services.database.factories import get_auth_service, get_user_service from app.services.user_service import UserService +from app.services.email_service import EmailService +from app.services.database.factories import get_auth_service, get_user_service, get_email_service # ------------------------------------------------------------------------- # Router Configuration @@ -278,7 +279,8 @@ async def update_user( async def delete_user( user_id: str, user_service: UserService = Depends(get_user_service), - auth_service: AuthService = Depends(get_auth_service) + auth_service: AuthService = Depends(get_auth_service), + email_service: EmailService = Depends(get_email_service) ) -> dict: """ Delete a user. @@ -294,12 +296,21 @@ async def delete_user( Raises: HTTPException: 404 if user not found """ - success = await user_service.delete_user(user_id) - if not success: + successDeleteUser = await user_service.delete_user(user_id) + if not successDeleteUser: raise standardize_error_response( Exception("User not found"), "delete user", user_id ) + + successDeleteEmails = await email_service.delete_emails(user_id) + if not successDeleteEmails: + raise standardize_error_response( + Exception("User not found"), + "delete Emails by user ID", + user_id + ) + return {"message": "User deleted successfully"} \ No newline at end of file diff --git a/backend/app/services/database/repositories/base_repository.py b/backend/app/services/database/repositories/base_repository.py index 83f7280..bb1fd2e 100644 --- a/backend/app/services/database/repositories/base_repository.py +++ b/backend/app/services/database/repositories/base_repository.py @@ -270,6 +270,22 @@ async def delete_one(self, query: Dict[str, Any]) -> bool: return result.deleted_count > 0 except Exception as e: raise + + async def delete_many(self, query: Dict[str, Any]) -> bool: + """ + Delete multiple documents matching the query. + + Args: + query: MongoDB query filter + + Returns: + bool: True if deletion successful + """ + try: + result = await self._get_collection().delete_many(query) + return result.deleted_count > 0 + except Exception as e: + raise async def count_documents(self, query: Dict[str, Any]) -> int: """ diff --git a/backend/app/services/database/repositories/email_repository.py b/backend/app/services/database/repositories/email_repository.py index 0bec7af..1275dee 100644 --- a/backend/app/services/database/repositories/email_repository.py +++ b/backend/app/services/database/repositories/email_repository.py @@ -82,6 +82,18 @@ async def update_by_email_and_google_id( update_data ) + async def delete_by_google_id(self, google_id: str) -> bool: + """ + Delete all emails by Google user ID. + + Args: + google_id: Google ID of the user + + Returns: + bool: True if deletion successful + """ + return await self.delete_many({"google_id": google_id}) + async def delete_by_email_and_google_id(self, email_id: str, google_id: str) -> bool: """ Delete an email by IMAP UID and Google user ID. diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index 1d5f7cb..8d14d62 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -473,6 +473,23 @@ async def delete_email(self, email_id: str, google_id: str) -> bool: return await self.email_repository.delete_by_id(str(email_id), google_id) except Exception as e: self._handle_email_error(e, "delete", email_id, google_id) + + async def delete_emails(self, google_id: str) -> bool: + """ + Deletes all emails attached to given Google ID. + + Args: + google_id: Google ID of the user + + Returns: + bool: True if deletion successful + """ + try: + return await self.email_repository.delete_by_google_id(google_id) + except Exception as e: + self._handle_email_error(e, "delete", google_id) + + # ------------------------------------------------------------------------- # Content Processing Methods From 85b338915cb3cc923b16364612d181ee7662888e Mon Sep 17 00:00:00 2001 From: riteshsamal-ship <82118620+riteshsamal-ship@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:39:09 -0700 Subject: [PATCH 16/33] Deletion of Summaries Adding deletion of summaries when deleting users. Currently commented out to avoid extra cost of re-summarizing. --- backend/app/routers/user_router.py | 18 +++++++++++-- .../repositories/summary_repository.py | 13 ++++++++++ .../services/summarization/summary_service.py | 26 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/user_router.py b/backend/app/routers/user_router.py index 7dd7983..9344167 100644 --- a/backend/app/routers/user_router.py +++ b/backend/app/routers/user_router.py @@ -15,7 +15,8 @@ from app.services.auth_service import AuthService from app.services.user_service import UserService from app.services.email_service import EmailService -from app.services.database.factories import get_auth_service, get_user_service, get_email_service +from app.services.summarization.summary_service import SummaryService +from app.services.database.factories import get_auth_service, get_user_service, get_email_service, get_summary_service # ------------------------------------------------------------------------- # Router Configuration @@ -280,7 +281,8 @@ async def delete_user( user_id: str, user_service: UserService = Depends(get_user_service), auth_service: AuthService = Depends(get_auth_service), - email_service: EmailService = Depends(get_email_service) + email_service: EmailService = Depends(get_email_service), + summary_service: SummaryService = Depends(get_summary_service) ) -> dict: """ Delete a user. @@ -289,6 +291,8 @@ async def delete_user( user_id: The ID of the user to delete user_service: Injected UserService instance auth_service: Injected AuthService instance + email_service: Injected EmailService instance + summary_service: Injected SummaryService instance Returns: dict: Success message @@ -312,5 +316,15 @@ async def delete_user( user_id ) + # Note: This is commented out to avoid having to inccur the cost of deleting and resummarizing + + #successDeleteSummaries = await summary_service.delete_summaries_by_google_id(user_id) + #if not successDeleteSummaries: + # raise standardize_error_response( + # Exception("User not found"), + # "delete Summaries by user ID", + # user_id + # ) + return {"message": "User deleted successfully"} \ No newline at end of file diff --git a/backend/app/services/database/repositories/summary_repository.py b/backend/app/services/database/repositories/summary_repository.py index 9b778f5..cfe5d05 100644 --- a/backend/app/services/database/repositories/summary_repository.py +++ b/backend/app/services/database/repositories/summary_repository.py @@ -94,6 +94,19 @@ async def delete_by_email_and_google_id(self, email_id: str, google_id: str) -> bool: True if deletion successful """ return await self.delete_one({"email_id": email_id, "google_id": google_id}) + + async def delete_by_google_id(self, google_id): + """ + Delete all summaries attached to given Google user ID. + + Args: + google_id: Google ID of the user + + Returns: + bool: True if deletion successful + """ + return await self.delete_many({"google_id": google_id}) + async def find_many( self, diff --git a/backend/app/services/summarization/summary_service.py b/backend/app/services/summarization/summary_service.py index 622eb92..f618f06 100644 --- a/backend/app/services/summarization/summary_service.py +++ b/backend/app/services/summarization/summary_service.py @@ -280,6 +280,32 @@ async def delete_summary(self, email_id: str, google_id: str) -> bool: except Exception as e: raise standardize_error_response(e, "delete summary", email_id) + async def delete_summaries_by_google_id(self, google_id: str) -> bool: + """ + Delete all summaries for a specific Google user ID. + + Args: + google_id: Google ID of the user whose summaries to delete + + Returns: + bool: True if any summaries were deleted, False otherwise + + Raises: + Exception: If database operation fails + """ + try: + result = await self.summary_repository.delete_by_google_id({"google_id": google_id}) + deleted = result.deleted_count > 0 + + if deleted: + log_operation(logger, 'info', f"All summaries deleted for user {google_id}") + else: + log_operation(logger, 'info', f"No summaries found for user {google_id} to delete") + + return deleted + except Exception as e: + raise standardize_error_response(e, "delete summaries by google id", google_id) + async def save_summaries_batch(self, summaries: List[SummarySchema], google_id: str) -> Dict[str, int]: """ Store multiple summaries in a single operation. From 1dd44fed8aaffbbe192aef9fe0c0c88a421ab053 Mon Sep 17 00:00:00 2001 From: riteshsamal-ship <82118620+riteshsamal-ship@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:42:56 -0700 Subject: [PATCH 17/33] error handling for deletion better error handling for deletion --- backend/app/routers/user_router.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/app/routers/user_router.py b/backend/app/routers/user_router.py index 9344167..2c9133e 100644 --- a/backend/app/routers/user_router.py +++ b/backend/app/routers/user_router.py @@ -303,28 +303,28 @@ async def delete_user( successDeleteUser = await user_service.delete_user(user_id) if not successDeleteUser: raise standardize_error_response( - Exception("User not found"), - "delete user", - user_id + HTTPException(status_code=404, detail="User not found"), + action="delete user", + context=user_id ) successDeleteEmails = await email_service.delete_emails(user_id) if not successDeleteEmails: raise standardize_error_response( - Exception("User not found"), - "delete Emails by user ID", - user_id - ) + HTTPException(status_code=500, detail="Failed to delete user emails"), + action="delete emails by user ID", + context=user_id + ) # Note: This is commented out to avoid having to inccur the cost of deleting and resummarizing #successDeleteSummaries = await summary_service.delete_summaries_by_google_id(user_id) #if not successDeleteSummaries: # raise standardize_error_response( - # Exception("User not found"), - # "delete Summaries by user ID", - # user_id - # ) + # HTTPException(status_code=404, detail="Summaries not found"), + # action="delete summaries by user ID", + # context=user_id + #) return {"message": "User deleted successfully"} \ No newline at end of file From 5a96cac849f6f68ccf3d183881f6212288c9a95c Mon Sep 17 00:00:00 2001 From: emmamelkumian <147114444+emmamelkumian@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:55:34 -0700 Subject: [PATCH 18/33] updated homepage --- frontend/src/components/login/Home.css | 158 ++++++++++++++++++---- frontend/src/components/login/contact.jsx | 21 ++- frontend/src/components/login/privacy.jsx | 76 ++++++++++- frontend/src/components/login/terms.jsx | 66 +-------- 4 files changed, 230 insertions(+), 91 deletions(-) diff --git a/frontend/src/components/login/Home.css b/frontend/src/components/login/Home.css index 731bdf7..5c20516 100644 --- a/frontend/src/components/login/Home.css +++ b/frontend/src/components/login/Home.css @@ -1,28 +1,10 @@ @import url("../../main.css"); -/* html, -body { - margin: 0; - padding: 0; - width: 250vw; - overflow-x: hidden; -} */ - .home { height: 100vh; background-color: var(--grey-split); - padding: calc(2rem + 1vh) calc(5rem + 4vw); } - -/* html, -body { - margin: 0; - padding: 0; - width: 250vw; - overflow-x: hidden; -} */ - .home { width: 100vw; height: 100vh; @@ -40,9 +22,9 @@ body { align-items: center; padding: 0 calc(2rem + 1vw); background-color: var(--white); - /* box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.2); */ z-index: 1000; font-size: var(--font-size); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.10); } .home .logo-container { @@ -102,16 +84,17 @@ body { .snap-of-ui { background-image: url("../../assets/Minimal_Browser_Dashboard.png"); + margin-left: 80rem; width: 35vw; height: 20vw; background-size: cover; background-position: center; position: absolute; top: 65%; - right: 30vw; + right: 20vw; transform: translateY(-50%); z-index: 2; - box-shadow: 0 0.2rem 0.7rem rgba(0, 0, 0, 0.2); + box-shadow: 0 0.2rem 0.7rem rgba(0, 0, 0, 0.7); } .home .title .subtitle { @@ -156,21 +139,21 @@ body { .home .overview h1 { display: flex; flex-direction: column; - align-items: flex-start; + align-items: center; justify-content: flex-start; text-align: left; - font-size: calc(0.5rem + 1vh); + } .home .overview-items { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); flex-direction: column; align-items: center; justify-content: center; text-align: center; margin-top: calc(2rem + 2vh); - padding: 2 calc(1rem + 1vw); + padding: 3 calc(3rem + 3vw); gap: calc(1rem + 1vh); } @@ -185,7 +168,7 @@ body { border-radius: calc(0.3rem + 0.3vh); padding: calc(0.7rem + 0.5vh) calc(2rem + 1vw); background-color: var(--grey-4); - box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.2); + box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.5); } .item1 svg, @@ -208,11 +191,12 @@ body { align-items: center; justify-content: center; text-align: center; - margin: calc(2rem + 2vh); + margin: calc(2.5rem + 3vh); } .about-us { border-top: 3px solid var(--grey-2); + padding-top: calc(2rem + 3vh); } .privacy-policy, @@ -256,3 +240,123 @@ body { font-size: calc(0.2rem + 1vh); color: var(--grey-light); } + +/*-----------------------------------Privacy Policy--------------------------------*/ +.privacy-policy { + padding: 2.5rem 2rem; + font-family: var(--font-family); +} + +.privacy-policy h1 { + text-align: center; + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 2rem; + color: var(--black); + letter-spacing: 1px; +} + +.privacy-policy h2 { + text-align: center; + font-size: 1.3rem; + font-weight: 600; + margin-top: 2rem; + margin-bottom: 1rem; + color: var(--black); +} + +.privacy-policy h3 { + text-align: left; + font-size: 1.1rem; + font-weight: 600; + margin-top: 1.5rem; + margin-bottom: 0.5rem; + color: var(--black); +} + +.privacy-policy p { + text-align: left; + font-size: 1rem; + margin-bottom: 1rem; + line-height: 1.7; +} + +.privacy-policy ul { + margin-left: 1.5rem; + margin-bottom: 1rem; + padding-left: 1.2rem; +} + +.privacy-policy li { + text-align: left; + font-size: 1rem; + margin-bottom: 0.5rem; + line-height: 1.5; + list-style-type: disc; +} + +.privacy-policy a, .terms-of-service a, .contact-us a { + color: #2a55a3; + text-decoration: underline; +} + +/*-----------------------------------Terms of Service--------------------------------*/ +.terms-of-service { + padding: 2.5rem 2rem; + font-family: var(--font-family); + color: var(--black); +} + +.terms-of-service h1 { + text-align: center; + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 2rem; + color: var(--black); + letter-spacing: 1px; +} + +.terms-of-service h2 { + text-align: center; + font-size: 1.3rem; + font-weight: 600; + margin-top: 2rem; + margin-bottom: 1rem; + color: var(--black); +} + +.terms-of-service h3 { + text-align: left; + font-size: 1.1rem; + font-weight: 600; + margin-top: 1.5rem; + margin-bottom: 0.5rem; + color: var(--black); +} + +.terms-of-service p { + text-align: left; + font-size: 1rem; + margin-bottom: 1rem; + line-height: 1.7; +} + +.terms-of-service ul { + margin-left: 1.5rem; + margin-bottom: 1rem; + padding-left: 1.2rem; +} + +.terms-of-service li { + text-align: left; + font-size: 1rem; + margin-bottom: 0.5rem; + line-height: 1.5; + list-style-type: disc; +} + +/*-----------------------------------Contact Us--------------------------------*/ +.contact-us { + padding: 2.5rem 2rem; + font-family: var(--font-family); +} \ No newline at end of file diff --git a/frontend/src/components/login/contact.jsx b/frontend/src/components/login/contact.jsx index 1357bd0..a268731 100644 --- a/frontend/src/components/login/contact.jsx +++ b/frontend/src/components/login/contact.jsx @@ -33,7 +33,26 @@ export default function Contact() {

Contact Us

-

Feel free to reach out to us for any inquiries.

+

+ Thank you for using EmailEssence! +

+

+ We hope you enjoy our service and find it helpful in managing your + emails. +

+

+ If you have any feedback or suggestions for improvement, please + feel free to reach out to us. We value your input and are always + looking for ways to enhance our service. +

+

+ We look forward to serving you and helping you make the most of + your email experience. +

+

+ If you have any questions or concerns, + please contact us at emailessencellc@gmail.com +