fix(merge): resolve tests/test_auth.py conflict keeping all tests from both branches
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+13
@@ -3,9 +3,11 @@ import inspect
|
||||
import logging
|
||||
import pathlib
|
||||
from functools import wraps
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.responses import RedirectResponse
|
||||
@@ -81,6 +83,17 @@ def require_login(func):
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
if not request.session.get("user"):
|
||||
# For API endpoints return 401 instead of storing the URL in the session
|
||||
# and redirecting to /login. Without this guard, the /api/auth/whoami
|
||||
# probe issued by common.js on every page load would overwrite
|
||||
# redirect_after_login with the API URL, causing the post-login redirect
|
||||
# to land on a JSON endpoint rather than the original page.
|
||||
url_path = urlparse(str(request.url)).path
|
||||
if url_path.startswith("/api/"):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={"error": "Not authenticated"},
|
||||
)
|
||||
request.session["redirect_after_login"] = str(request.url)
|
||||
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
|
||||
# Pass request as a keyword argument so that endpoints whose first
|
||||
|
||||
+10
-1
@@ -432,7 +432,7 @@ class Settings(BaseSettings):
|
||||
# In development/testing, set to True to disable verification (not recommended)
|
||||
sftp_disable_host_key_verification: bool = False # Default enforces host key verification
|
||||
|
||||
# Email settings
|
||||
# Email settings (shared SMTP – used for password reset, verification emails, etc.)
|
||||
email_host: Optional[str] = None
|
||||
email_port: Optional[int] = 587
|
||||
email_username: Optional[str] = None
|
||||
@@ -441,6 +441,15 @@ class Settings(BaseSettings):
|
||||
email_sender: Optional[str] = None # From address, defaults to email_username if not set
|
||||
email_default_recipient: Optional[str] = None
|
||||
|
||||
# Email destination settings (dedicated SMTP for document delivery – decoupled from shared email above)
|
||||
dest_email_host: Optional[str] = None
|
||||
dest_email_port: Optional[int] = 587
|
||||
dest_email_username: Optional[str] = None
|
||||
dest_email_password: Optional[str] = None
|
||||
dest_email_use_tls: bool = True
|
||||
dest_email_sender: Optional[str] = None # From address for delivered documents
|
||||
dest_email_default_recipient: Optional[str] = None # Fallback recipient for document delivery
|
||||
|
||||
# OneDrive settings
|
||||
onedrive_client_id: Optional[str] = None
|
||||
onedrive_client_secret: Optional[str] = None
|
||||
|
||||
@@ -80,13 +80,6 @@ def finalize_document_storage(self, original_file: str, processed_file: str, met
|
||||
from app.tasks.convert_to_pdfa import convert_to_pdfa
|
||||
|
||||
logger.info(f"[{task_id}] PDF/A conversion enabled, queueing archival conversion")
|
||||
log_task_progress(
|
||||
task_id,
|
||||
"finalize_document_storage",
|
||||
"in_progress",
|
||||
"Queueing PDF/A archival conversion",
|
||||
file_id=file_id,
|
||||
)
|
||||
convert_to_pdfa.delay(file_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{task_id}] Could not queue PDF/A conversion: {e}")
|
||||
|
||||
@@ -64,7 +64,10 @@ def _should_upload_to_sftp():
|
||||
|
||||
def _should_upload_to_email():
|
||||
return bool(
|
||||
settings.email_host and settings.email_username and settings.email_password and settings.email_default_recipient
|
||||
settings.dest_email_host
|
||||
and settings.dest_email_username
|
||||
and settings.dest_email_password
|
||||
and settings.dest_email_default_recipient
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -125,11 +125,11 @@ def attach_logo(msg):
|
||||
def _prepare_recipients(recipients):
|
||||
"""Helper function to prepare email recipients list."""
|
||||
if not recipients:
|
||||
if not settings.email_default_recipient:
|
||||
if not settings.dest_email_default_recipient:
|
||||
error_msg = "No recipients specified and no default recipient configured"
|
||||
logger.error(error_msg)
|
||||
return None, error_msg
|
||||
return [settings.email_default_recipient], None
|
||||
return [settings.dest_email_default_recipient], None
|
||||
elif isinstance(recipients, str):
|
||||
return [recipients], None # Convert single email to list
|
||||
return recipients, None
|
||||
@@ -139,17 +139,17 @@ def _send_email_with_smtp(msg, filename, recipients):
|
||||
"""Helper function to handle SMTP connection and sending."""
|
||||
try:
|
||||
# First try to resolve the hostname
|
||||
socket.gethostbyname(settings.email_host)
|
||||
socket.gethostbyname(settings.dest_email_host)
|
||||
|
||||
# Connect to the SMTP server
|
||||
with smtplib.SMTP(settings.email_host, settings.email_port, timeout=30) as server:
|
||||
with smtplib.SMTP(settings.dest_email_host, settings.dest_email_port, timeout=30) as server:
|
||||
# Use TLS if specified
|
||||
if settings.email_use_tls:
|
||||
if settings.dest_email_use_tls:
|
||||
server.starttls()
|
||||
|
||||
# Login if credentials are provided
|
||||
if settings.email_username and settings.email_password:
|
||||
server.login(settings.email_username, settings.email_password)
|
||||
if settings.dest_email_username and settings.dest_email_password:
|
||||
server.login(settings.dest_email_username, settings.dest_email_password)
|
||||
|
||||
# Send the email
|
||||
server.send_message(msg)
|
||||
@@ -157,11 +157,11 @@ def _send_email_with_smtp(msg, filename, recipients):
|
||||
logger.info(f"Successfully sent {filename} via email to {', '.join(recipients)}")
|
||||
return None
|
||||
except socket.gaierror as e:
|
||||
error_msg = f"Failed to resolve email host: {settings.email_host} - {str(e)}"
|
||||
error_msg = f"Failed to resolve email host: {settings.dest_email_host} - {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return {"status": "Failed", "reason": error_msg, "error": str(e)}
|
||||
except (ConnectionRefusedError, TimeoutError) as e:
|
||||
error_msg = f"Connection error to SMTP server {settings.email_host}:{settings.email_port} - {str(e)}"
|
||||
error_msg = f"Connection error to SMTP server {settings.dest_email_host}:{settings.dest_email_port} - {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return {"status": "Failed", "reason": error_msg, "error": str(e)}
|
||||
|
||||
@@ -205,17 +205,17 @@ def upload_to_email(
|
||||
# Extract filename
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
# Check if email settings are configured
|
||||
if not settings.email_host:
|
||||
error_msg = "Email host is not configured"
|
||||
# Check if email destination settings are configured
|
||||
if not settings.dest_email_host:
|
||||
error_msg = "Email destination host is not configured (DEST_EMAIL_HOST)"
|
||||
logger.error(f"[{task_id}] {error_msg}")
|
||||
log_task_progress(task_id, "upload_to_email", "skipped", error_msg, file_id=file_id)
|
||||
return {"status": "Skipped", "reason": error_msg}
|
||||
|
||||
# Log email configuration for debugging
|
||||
logger.debug(
|
||||
f"[{task_id}] Email config - Host: {settings.email_host}, Port: {settings.email_port}, "
|
||||
f"Username: {settings.email_username}, TLS: {settings.email_use_tls}"
|
||||
f"[{task_id}] Email destination config - Host: {settings.dest_email_host}, Port: {settings.dest_email_port}, "
|
||||
f"Username: {settings.dest_email_username}, TLS: {settings.dest_email_use_tls}"
|
||||
)
|
||||
|
||||
# Process recipients
|
||||
@@ -236,7 +236,7 @@ def upload_to_email(
|
||||
try:
|
||||
# Create the email
|
||||
msg = MIMEMultipart("related")
|
||||
msg["From"] = settings.email_sender or settings.email_username
|
||||
msg["From"] = settings.dest_email_sender or settings.dest_email_username
|
||||
msg["To"] = ", ".join(recipients)
|
||||
msg["Subject"] = subject
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ filename }} – DocuElevate</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; background-color: #f4f4f5; font-family: Arial, Helvetica, sans-serif; }
|
||||
.wrapper { max-width: 600px; margin: 32px auto; background: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.08); overflow: hidden; }
|
||||
.header { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); padding: 32px 40px; text-align: center; }
|
||||
.header img { max-height: 48px; }
|
||||
.header h1 { color: #ffffff; font-size: 22px; margin: 16px 0 0; }
|
||||
.body { padding: 32px 40px; }
|
||||
.body p { color: #374151; font-size: 15px; line-height: 1.6; margin: 0 0 16px; }
|
||||
.attachment-box { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin: 24px 0; }
|
||||
.attachment-box .label { font-size: 11px; font-weight: bold; color: #6b7280; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
|
||||
.attachment-box .filename { color: #111827; font-size: 15px; font-weight: bold; word-break: break-all; }
|
||||
.metadata-table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 13px; }
|
||||
.metadata-table td { padding: 6px 0; color: #374151; vertical-align: top; }
|
||||
.metadata-table td:first-child { font-weight: bold; color: #6b7280; width: 40%; padding-right: 12px; }
|
||||
.footer { background: #f9fafb; border-top: 1px solid #e5e7eb; padding: 20px 40px; text-align: center; color: #9ca3af; font-size: 12px; }
|
||||
.footer a { color: #4f46e5; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
{% if has_logo %}
|
||||
<img src="cid:logo" alt="{{ app_name }} logo">
|
||||
{% endif %}
|
||||
<h1>Document Delivery</h1>
|
||||
</div>
|
||||
<div class="body">
|
||||
<p>{{ message }}</p>
|
||||
|
||||
<div class="attachment-box">
|
||||
<div class="label">Attached file</div>
|
||||
<div class="filename">📎 {{ filename }}</div>
|
||||
</div>
|
||||
|
||||
{% if has_metadata and metadata %}
|
||||
<p style="font-weight:bold; color:#374151; margin-bottom:8px;">Document metadata</p>
|
||||
<table class="metadata-table">
|
||||
{% for key, value in metadata.items() %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin-top:24px; color:#6b7280; font-size:13px;">
|
||||
This document was sent automatically by {{ app_name }}.{% if app_url %} Visit <a href="{{ app_url }}" style="color:#4f46e5;">{{ app_url }}</a> to manage your documents.{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© {{ current_year }} {{ app_name }} · Intelligent Document Processing
|
||||
{% if app_url %}· <a href="{{ app_url }}">{{ app_url }}</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -154,23 +154,23 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
},
|
||||
}
|
||||
|
||||
# Add Email configuration
|
||||
# Add Email destination configuration (dedicated settings for document delivery)
|
||||
providers["Email"] = {
|
||||
"name": "Email",
|
||||
"icon": "fa-solid fa-envelope",
|
||||
"configured": bool(
|
||||
getattr(settings, "email_host", None) and getattr(settings, "email_default_recipient", None)
|
||||
getattr(settings, "dest_email_host", None) and getattr(settings, "dest_email_default_recipient", None)
|
||||
),
|
||||
"enabled": True,
|
||||
"description": "Send documents via email",
|
||||
"details": {
|
||||
"host": getattr(settings, "email_host", "Not set"),
|
||||
"port": getattr(settings, "email_port", "Not set"),
|
||||
"username": getattr(settings, "email_username", "Not set"),
|
||||
"password": mask_sensitive_value(getattr(settings, "email_password", None)),
|
||||
"use_tls": getattr(settings, "email_use_tls", "Not set"),
|
||||
"sender": getattr(settings, "email_sender", "Not set"),
|
||||
"default_recipient": getattr(settings, "email_default_recipient", "Not set"),
|
||||
"host": getattr(settings, "dest_email_host", "Not set"),
|
||||
"port": getattr(settings, "dest_email_port", "Not set"),
|
||||
"username": getattr(settings, "dest_email_username", "Not set"),
|
||||
"password": mask_sensitive_value(getattr(settings, "dest_email_password", None)),
|
||||
"use_tls": getattr(settings, "dest_email_use_tls", "Not set"),
|
||||
"sender": getattr(settings, "dest_email_sender", "Not set"),
|
||||
"default_recipient": getattr(settings, "dest_email_default_recipient", "Not set"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -107,12 +107,12 @@ def validate_storage_configs() -> dict[str, list[str]]:
|
||||
|
||||
issues["sftp"] = sftp_issues
|
||||
|
||||
# Validate Email sending
|
||||
# Validate Email sending (destination-specific settings)
|
||||
email_issues = []
|
||||
if not getattr(settings, "email_host", None):
|
||||
email_issues.append("EMAIL_HOST is not configured")
|
||||
if not getattr(settings, "email_default_recipient", None):
|
||||
email_issues.append("EMAIL_DEFAULT_RECIPIENT is not configured")
|
||||
if not getattr(settings, "dest_email_host", None):
|
||||
email_issues.append("DEST_EMAIL_HOST is not configured")
|
||||
if not getattr(settings, "dest_email_default_recipient", None):
|
||||
email_issues.append("DEST_EMAIL_DEFAULT_RECIPIENT is not configured")
|
||||
issues["email"] = email_issues
|
||||
|
||||
# Validate S3
|
||||
|
||||
@@ -917,7 +917,7 @@ SETTING_METADATA = {
|
||||
# Email Settings
|
||||
"email_host": {
|
||||
"category": "Email",
|
||||
"description": "SMTP server hostname",
|
||||
"description": "SMTP server hostname (shared – used for password reset and verification emails)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
@@ -965,7 +965,64 @@ SETTING_METADATA = {
|
||||
},
|
||||
"email_default_recipient": {
|
||||
"category": "Email",
|
||||
"description": "Default recipient email address",
|
||||
"description": "Default recipient email address (shared – used for system notifications)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Email Destination Settings (dedicated SMTP for document delivery)
|
||||
"dest_email_host": {
|
||||
"category": "Email Destination",
|
||||
"description": "SMTP server hostname for document delivery (separate from shared email settings)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_port": {
|
||||
"category": "Email Destination",
|
||||
"description": "SMTP port for document delivery (default: 587)",
|
||||
"type": "integer",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_username": {
|
||||
"category": "Email Destination",
|
||||
"description": "SMTP username for document delivery",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_password": {
|
||||
"category": "Email Destination",
|
||||
"description": "SMTP password for document delivery",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_use_tls": {
|
||||
"category": "Email Destination",
|
||||
"description": "Use TLS encryption for document delivery SMTP",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_sender": {
|
||||
"category": "Email Destination",
|
||||
"description": "From address for document delivery emails (defaults to dest_email_username)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"dest_email_default_recipient": {
|
||||
"category": "Email Destination",
|
||||
"description": "Default recipient email for document delivery when none is specified",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
|
||||
Reference in New Issue
Block a user