jebin2 commited on
Commit
e39877e
Β·
1 Parent(s): 3229cec
app.py CHANGED
@@ -11,9 +11,9 @@ from fastapi import FastAPI, Request
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import JSONResponse
13
 
14
- from database import init_db
15
  from routers import auth, blink, general
16
- from drive_service import DriveService
17
 
18
  # Configure logging
19
  logging.basicConfig(
 
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import JSONResponse
13
 
14
+ from core.database import init_db
15
  from routers import auth, blink, general
16
+ from services.drive_service import DriveService
17
 
18
  # Configure logging
19
  logging.basicConfig(
auth_utils.py DELETED
@@ -1,89 +0,0 @@
1
- import secrets
2
- import smtplib
3
- import ssl
4
- from email.mime.text import MIMEText
5
- from email.mime.multipart import MIMEMultipart
6
- import bcrypt
7
- import logging
8
- import os
9
-
10
- # Configure logging
11
- logger = logging.getLogger(__name__)
12
-
13
- # Email configuration
14
- # Email configuration
15
- SMTP_SERVER = os.getenv("SMTP_SERVER", "127.0.0.1")
16
- SMTP_PORT = int(os.getenv("SMTP_PORT", "1025"))
17
- # Prioritize EMAIL_ID and EMAIL_PASSWORD if available
18
- SMTP_USERNAME = os.getenv("EMAIL_ID") or os.getenv("SMTP_USERNAME", "sender@domain.com")
19
- SMTP_PASSWORD = os.getenv("EMAIL_PASSWORD") or os.getenv("SMTP_PASSWORD", "yourpassword")
20
-
21
- # Auto-configure for Gmail if using defaults and gmail address
22
- if SMTP_SERVER == "127.0.0.1" and "gmail.com" in SMTP_USERNAME:
23
- SMTP_SERVER = "smtp.gmail.com"
24
- SMTP_PORT = 465
25
-
26
- SMTP_SENDER = os.getenv("SMTP_SENDER", SMTP_USERNAME)
27
-
28
- from gmail_service import GmailService
29
-
30
- # Initialize Gmail Service
31
- gmail_service = GmailService()
32
-
33
- def verify_password(plain_password: str, hashed_password: str) -> bool:
34
- """
35
- Verify a password against a hash.
36
- """
37
- if isinstance(hashed_password, str):
38
- hashed_password = hashed_password.encode('utf-8')
39
- return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password)
40
-
41
- def get_password_hash(password: str) -> str:
42
- """
43
- Hash a password using bcrypt.
44
- """
45
- # rounds=12 as per spec
46
- salt = bcrypt.gensalt(rounds=12)
47
- hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
48
- return hashed.decode('utf-8')
49
-
50
- def generate_secret_key() -> str:
51
- """
52
- Generate a secure secret key starting with 'sk_'.
53
- """
54
- return "sk_" + secrets.token_urlsafe(32)
55
-
56
- def send_email(to_email: str, subject: str, body: str):
57
- """
58
- Send an email using Gmail API.
59
- This function is blocking and should be run in a background task.
60
- """
61
- # Try Gmail API first
62
- if gmail_service.authenticate():
63
- return gmail_service.send_email(to_email, subject, body)
64
-
65
- logger.warning("Gmail API credentials not found or invalid. Falling back to SMTP (may fail on some platforms).")
66
-
67
- # Fallback to SMTP (Original Implementation)
68
- try:
69
- message = MIMEMultipart()
70
- message["From"] = SMTP_SENDER
71
- message["To"] = to_email
72
- message["Subject"] = subject
73
-
74
- message.attach(MIMEText(body, "plain"))
75
-
76
- # Create a secure SSL context
77
- context = ssl.create_default_context()
78
- context.check_hostname = False
79
- context.verify_mode = ssl.CERT_NONE
80
-
81
- with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context) as server:
82
- server.login(SMTP_USERNAME, SMTP_PASSWORD)
83
- server.sendmail(SMTP_SENDER, to_email, message.as_string())
84
-
85
- logger.info(f"Email sent successfully to {to_email} via SMTP")
86
- return True
87
- except Exception as e:
88
- logger.error(f"Failed to send email to {to_email}: {e}")
89
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database.py β†’ core/database.py RENAMED
File without changes
models.py β†’ core/models.py RENAMED
@@ -1,9 +1,9 @@
1
  """
2
- SQLAlchemy models for the URL Blink application.
3
  """
4
  from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, Boolean
5
  from sqlalchemy.sql import func
6
- from database import Base
7
 
8
 
9
  class BlinkData(Base):
 
1
  """
2
+ SQLAlchemy models for the APIGateway application.
3
  """
4
  from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, Boolean
5
  from sqlalchemy.sql import func
6
+ from core.database import Base
7
 
8
 
9
  class BlinkData(Base):
schemas.py β†’ core/schemas.py RENAMED
File without changes
core/security.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import secrets
2
+ import bcrypt
3
+
4
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
5
+ """
6
+ Verify a password against a hash.
7
+ """
8
+ if isinstance(hashed_password, str):
9
+ hashed_password = hashed_password.encode('utf-8')
10
+ return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password)
11
+
12
+ def get_password_hash(password: str) -> str:
13
+ """
14
+ Hash a password using bcrypt.
15
+ """
16
+ # rounds=12 as per spec
17
+ salt = bcrypt.gensalt(rounds=12)
18
+ hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
19
+ return hashed.decode('utf-8')
20
+
21
+ def generate_secret_key() -> str:
22
+ """
23
+ Generate a secure secret key starting with 'sk_'.
24
+ """
25
+ return "sk_" + secrets.token_urlsafe(32)
dependencies.py CHANGED
@@ -7,9 +7,9 @@ from fastapi import Request, Depends, HTTPException, status
7
  from sqlalchemy import select, and_
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
 
10
- from database import get_db
11
- from models import User, RateLimit
12
- from auth_utils import verify_password
13
 
14
  logger = logging.getLogger(__name__)
15
 
 
7
  from sqlalchemy import select, and_
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
 
10
+ from core.database import get_db
11
+ from core.models import User, RateLimit
12
+ from core.security import verify_password
13
 
14
  logger = logging.getLogger(__name__)
15
 
routers/auth.py CHANGED
@@ -5,12 +5,13 @@ from sqlalchemy import select
5
  from datetime import datetime
6
  import uuid
7
 
8
- from database import get_db
9
- from models import User, AuditLog
10
- from schemas import CheckRegistrationRequest, RegisterRequest, ValidateRequest, ResetRequest
11
- from auth_utils import get_password_hash, verify_password, generate_secret_key, send_email
 
12
  from dependencies import check_rate_limit
13
- from drive_service import DriveService
14
 
15
  router = APIRouter(prefix="/auth", tags=["auth"])
16
  drive_service = DriveService()
 
5
  from datetime import datetime
6
  import uuid
7
 
8
+ from core.database import get_db
9
+ from core.models import User, AuditLog
10
+ from core.schemas import CheckRegistrationRequest, RegisterRequest, ValidateRequest, ResetRequest
11
+ from core.security import get_password_hash, verify_password, generate_secret_key
12
+ from services.email_service import send_email
13
  from dependencies import check_rate_limit
14
+ from services.drive_service import DriveService
15
 
16
  router = APIRouter(prefix="/auth", tags=["auth"])
17
  drive_service = DriveService()
routers/blink.py CHANGED
@@ -5,9 +5,9 @@ from sqlalchemy import select, func
5
  import ipaddress
6
  import logging
7
 
8
- from database import get_db
9
- from models import BlinkData
10
- from encryption import decrypt_multiple_blocks
11
  from dependencies import get_geolocation
12
 
13
  logger = logging.getLogger(__name__)
 
5
  import ipaddress
6
  import logging
7
 
8
+ from core.database import get_db
9
+ from core.models import BlinkData
10
+ from services.encryption_service import decrypt_multiple_blocks
11
  from dependencies import get_geolocation
12
 
13
  logger = logging.getLogger(__name__)
drive_service.py β†’ services/drive_service.py RENAMED
File without changes
gmail_service.py β†’ services/email_service.py RENAMED
@@ -1,16 +1,31 @@
1
  import os
2
  import base64
3
  import logging
 
 
4
  from email.mime.text import MIMEText
5
  from email.mime.multipart import MIMEMultipart
6
  from google.auth.transport.requests import Request
7
  from google.oauth2.credentials import Credentials
8
- from google_auth_oauthlib.flow import InstalledAppFlow
9
  from googleapiclient.discovery import build
10
  from googleapiclient.errors import HttpError
11
 
12
  logger = logging.getLogger(__name__)
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class GmailService:
15
  SCOPES = ['https://www.googleapis.com/auth/gmail.send']
16
 
@@ -25,7 +40,7 @@ class GmailService:
25
  def authenticate(self):
26
  """Authenticate using the refresh token."""
27
  if not all([self.client_id, self.client_secret, self.refresh_token]):
28
- logger.error("Missing Google API credentials (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)")
29
  return False
30
 
31
  try:
@@ -60,10 +75,10 @@ class GmailService:
60
  message['to'] = to_email
61
  # Format sender with display name
62
  sender_name = "AnimateImage"
63
- if "<" not in self.sender_email:
64
  message['from'] = f"{sender_name} <{self.sender_email}>"
65
  else:
66
- message['from'] = self.sender_email
67
  message['subject'] = subject
68
  message.attach(MIMEText(body, 'plain'))
69
 
@@ -80,3 +95,42 @@ class GmailService:
80
  except Exception as e:
81
  logger.error(f"Unexpected error sending email: {e}")
82
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import base64
3
  import logging
4
+ import smtplib
5
+ import ssl
6
  from email.mime.text import MIMEText
7
  from email.mime.multipart import MIMEMultipart
8
  from google.auth.transport.requests import Request
9
  from google.oauth2.credentials import Credentials
 
10
  from googleapiclient.discovery import build
11
  from googleapiclient.errors import HttpError
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ # Email configuration
16
+ SMTP_SERVER = os.getenv("SMTP_SERVER", "127.0.0.1")
17
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "1025"))
18
+ # Prioritize EMAIL_ID and EMAIL_PASSWORD if available
19
+ SMTP_USERNAME = os.getenv("EMAIL_ID") or os.getenv("SMTP_USERNAME", "sender@domain.com")
20
+ SMTP_PASSWORD = os.getenv("EMAIL_PASSWORD") or os.getenv("SMTP_PASSWORD", "yourpassword")
21
+
22
+ # Auto-configure for Gmail if using defaults and gmail address
23
+ if SMTP_SERVER == "127.0.0.1" and "gmail.com" in SMTP_USERNAME:
24
+ SMTP_SERVER = "smtp.gmail.com"
25
+ SMTP_PORT = 465
26
+
27
+ SMTP_SENDER = os.getenv("SMTP_SENDER", SMTP_USERNAME)
28
+
29
  class GmailService:
30
  SCOPES = ['https://www.googleapis.com/auth/gmail.send']
31
 
 
40
  def authenticate(self):
41
  """Authenticate using the refresh token."""
42
  if not all([self.client_id, self.client_secret, self.refresh_token]):
43
+ # logger.error("Missing Google API credentials (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)")
44
  return False
45
 
46
  try:
 
75
  message['to'] = to_email
76
  # Format sender with display name
77
  sender_name = "AnimateImage"
78
+ if self.sender_email and "<" not in self.sender_email:
79
  message['from'] = f"{sender_name} <{self.sender_email}>"
80
  else:
81
+ message['from'] = self.sender_email or "unknown@example.com"
82
  message['subject'] = subject
83
  message.attach(MIMEText(body, 'plain'))
84
 
 
95
  except Exception as e:
96
  logger.error(f"Unexpected error sending email: {e}")
97
  return False
98
+
99
+ # Initialize global instance
100
+ gmail_service = GmailService()
101
+
102
+ def send_email(to_email: str, subject: str, body: str):
103
+ """
104
+ Send an email using Gmail API with SMTP fallback.
105
+ This function is blocking and should be run in a background task.
106
+ """
107
+ # Try Gmail API first
108
+ if gmail_service.authenticate():
109
+ if gmail_service.send_email(to_email, subject, body):
110
+ return True
111
+
112
+ logger.warning("Gmail API credentials not found or invalid. Falling back to SMTP.")
113
+
114
+ # Fallback to SMTP (Original Implementation)
115
+ try:
116
+ message = MIMEMultipart()
117
+ message["From"] = SMTP_SENDER
118
+ message["To"] = to_email
119
+ message["Subject"] = subject
120
+
121
+ message.attach(MIMEText(body, "plain"))
122
+
123
+ # Create a secure SSL context
124
+ context = ssl.create_default_context()
125
+ context.check_hostname = False
126
+ context.verify_mode = ssl.CERT_NONE
127
+
128
+ with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context) as server:
129
+ server.login(SMTP_USERNAME, SMTP_PASSWORD)
130
+ server.sendmail(SMTP_SENDER, to_email, message.as_string())
131
+
132
+ logger.info(f"Email sent successfully to {to_email} via SMTP")
133
+ return True
134
+ except Exception as e:
135
+ logger.error(f"Failed to send email to {to_email}: {e}")
136
+ return False
encryption.py β†’ services/encryption_service.py RENAMED
File without changes
tests/conftest.py CHANGED
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, Asyn
8
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
9
 
10
  from app import app
11
- from database import get_db, Base
12
 
13
  # Use a file-based SQLite database for testing to ensure persistence
14
  TEST_DATABASE_URL = "sqlite+aiosqlite:///./test_blink_data.db"
 
8
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
9
 
10
  from app import app
11
+ from core.database import get_db, Base
12
 
13
  # Use a file-based SQLite database for testing to ensure persistence
14
  TEST_DATABASE_URL = "sqlite+aiosqlite:///./test_blink_data.db"
test_email_env.py β†’ tests/debug_email_env.py RENAMED
@@ -5,7 +5,7 @@ from dotenv import load_dotenv
5
  # Load environment variables from .env file immediately
6
  load_dotenv()
7
 
8
- from auth_utils import send_email
9
 
10
  # Configure logging
11
  logging.basicConfig(level=logging.INFO)
@@ -20,7 +20,7 @@ def test_email_sending():
20
  logger.info(f"Testing email sending to {email_id}...")
21
 
22
  # Debug config
23
- from auth_utils import SMTP_SERVER, SMTP_PORT
24
  logger.info(f"Using SMTP Server: {SMTP_SERVER}:{SMTP_PORT}")
25
 
26
  subject = "Test Email from API Gateway"
 
5
  # Load environment variables from .env file immediately
6
  load_dotenv()
7
 
8
+ from services.email_service import send_email
9
 
10
  # Configure logging
11
  logging.basicConfig(level=logging.INFO)
 
20
  logger.info(f"Testing email sending to {email_id}...")
21
 
22
  # Debug config
23
+ from services.email_service import SMTP_SERVER, SMTP_PORT
24
  logger.info(f"Using SMTP Server: {SMTP_SERVER}:{SMTP_PORT}")
25
 
26
  subject = "Test Email from API Gateway"
test_gmail_service.py β†’ tests/test_gmail_service.py RENAMED
@@ -1,11 +1,11 @@
1
  import unittest
2
  from unittest.mock import MagicMock, patch
3
- from gmail_service import GmailService
4
 
5
  class TestGmailService(unittest.TestCase):
6
 
7
- @patch('gmail_service.build')
8
- @patch('gmail_service.Credentials')
9
  def test_send_email_success(self, mock_credentials, mock_build):
10
  # Mock the service and its methods
11
  mock_service = MagicMock()
 
1
  import unittest
2
  from unittest.mock import MagicMock, patch
3
+ from services.email_service import GmailService
4
 
5
  class TestGmailService(unittest.TestCase):
6
 
7
+ @patch('services.email_service.build')
8
+ @patch('services.email_service.Credentials')
9
  def test_send_email_success(self, mock_credentials, mock_build):
10
  # Mock the service and its methods
11
  mock_service = MagicMock()
tests/test_integration.py CHANGED
@@ -27,7 +27,7 @@ async def clear_tables(db_session):
27
  await db_session.execute(text("DELETE FROM blink_data"))
28
  await db_session.commit()
29
 
30
- @patch("routers.auth.send_email")
31
  def test_credit_system_flow(mock_send_email, client):
32
  mock_send_email.return_value = True
33
 
@@ -79,7 +79,7 @@ def test_blink_flow(client):
79
  assert len(items) > 0
80
  assert items[0]["user_id"] == user_id
81
 
82
- @patch("routers.auth.send_email")
83
  def test_rate_limiting(mock_send_email, client):
84
  # 10 requests should succeed
85
  for _ in range(10):
 
27
  await db_session.execute(text("DELETE FROM blink_data"))
28
  await db_session.commit()
29
 
30
+ @patch("services.email_service.send_email")
31
  def test_credit_system_flow(mock_send_email, client):
32
  mock_send_email.return_value = True
33
 
 
79
  assert len(items) > 0
80
  assert items[0]["user_id"] == user_id
81
 
82
+ @patch("services.email_service.send_email")
83
  def test_rate_limiting(mock_send_email, client):
84
  # 10 requests should succeed
85
  for _ in range(10):