n8n-fixes
#2
by
Yeetek
- opened
- app.py +1 -58
- application/dto/extraction_response.py +3 -3
- application/use_cases/process_job.py +33 -43
- domain/services/notification_service.py +1 -2
- infrastructure/clients/n8n/models.py +2 -2
- infrastructure/clients/n8n/n8n_client.py +9 -15
- infrastructure/clients/n8n/settings.py +1 -0
- infrastructure/config/settings.py +1 -1
- infrastructure/services/container.py +6 -12
- infrastructure/services/n8n_notification_service.py +8 -6
- interfaces/api/dependencies.py +17 -21
- interfaces/api/routes/extraction_routes.py +0 -3
- interfaces/api/routes/job_routes.py +1 -3
app.py
CHANGED
@@ -76,71 +76,14 @@ app = FastAPI(
|
|
76 |
3. **Check status**: Monitor progress with `/jobs/{job_id}`
|
77 |
4. **Download result**: Get extracted audio with `/jobs/{job_id}/download`
|
78 |
5. **Optional trimming**: Use `start` and `end` parameters to download specific segments
|
79 |
-
|
80 |
-
### Authentication
|
81 |
-
Most endpoints require JWT Bearer token authentication. Include your token in the Authorization header:
|
82 |
-
```
|
83 |
-
Authorization: Bearer <your-jwt-token>
|
84 |
-
```
|
85 |
-
|
86 |
-
Public endpoints (no authentication required):
|
87 |
-
- `GET /api/v1/info` - API information
|
88 |
-
- `GET /api/v1/health` - Health check
|
89 |
""",
|
90 |
version=settings.app_version,
|
91 |
docs_url="/docs",
|
92 |
redoc_url="/redoc",
|
93 |
openapi_url="/openapi.json",
|
94 |
-
lifespan=lifespan
|
95 |
-
# Add OpenAPI security scheme for Bearer token authentication
|
96 |
-
openapi_tags=[
|
97 |
-
{
|
98 |
-
"name": "extraction",
|
99 |
-
"description": "Audio extraction operations (requires authentication)"
|
100 |
-
},
|
101 |
-
{
|
102 |
-
"name": "jobs",
|
103 |
-
"description": "Job management operations (requires authentication)"
|
104 |
-
},
|
105 |
-
{
|
106 |
-
"name": "info",
|
107 |
-
"description": "Public API information (no authentication required)"
|
108 |
-
}
|
109 |
-
]
|
110 |
)
|
111 |
|
112 |
-
# Configure OpenAPI security scheme
|
113 |
-
def custom_openapi():
|
114 |
-
if app.openapi_schema:
|
115 |
-
return app.openapi_schema
|
116 |
-
|
117 |
-
from fastapi.openapi.utils import get_openapi
|
118 |
-
|
119 |
-
openapi_schema = get_openapi(
|
120 |
-
title=app.title,
|
121 |
-
version=app.version,
|
122 |
-
description=app.description,
|
123 |
-
routes=app.routes,
|
124 |
-
)
|
125 |
-
|
126 |
-
# Add Bearer token security scheme
|
127 |
-
openapi_schema["components"]["securitySchemes"] = {
|
128 |
-
"BearerAuth": {
|
129 |
-
"type": "http",
|
130 |
-
"scheme": "bearer",
|
131 |
-
"bearerFormat": "JWT",
|
132 |
-
"description": "JWT Bearer token authentication"
|
133 |
-
}
|
134 |
-
}
|
135 |
-
|
136 |
-
app.openapi_schema = openapi_schema
|
137 |
-
return app.openapi_schema
|
138 |
-
|
139 |
-
app.openapi = custom_openapi
|
140 |
-
|
141 |
-
# Let FastAPI handle OpenAPI generation automatically
|
142 |
-
# The HTTPBearer scheme in dependencies.py will be automatically detected
|
143 |
-
|
144 |
# Configure middleware
|
145 |
configure_cors(app)
|
146 |
register_exception_handlers(app)
|
|
|
76 |
3. **Check status**: Monitor progress with `/jobs/{job_id}`
|
77 |
4. **Download result**: Get extracted audio with `/jobs/{job_id}/download`
|
78 |
5. **Optional trimming**: Use `start` and `end` parameters to download specific segments
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
""",
|
80 |
version=settings.app_version,
|
81 |
docs_url="/docs",
|
82 |
redoc_url="/redoc",
|
83 |
openapi_url="/openapi.json",
|
84 |
+
lifespan=lifespan
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
)
|
86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
# Configure middleware
|
88 |
configure_cors(app)
|
89 |
register_exception_handlers(app)
|
application/dto/extraction_response.py
CHANGED
@@ -16,10 +16,10 @@ class DirectExtractionResultDTO:
|
|
16 |
class JobStatusDTO:
|
17 |
"""DTO for job status."""
|
18 |
job_id: str
|
|
|
19 |
status: str
|
20 |
created_at: datetime
|
21 |
updated_at: datetime
|
22 |
-
external_job_id: Optional[str] = None
|
23 |
filename: Optional[str] = None
|
24 |
file_size_mb: Optional[float] = None
|
25 |
output_format: Optional[str] = None
|
@@ -35,14 +35,14 @@ class DownloadResultDTO:
|
|
35 |
media_type: str
|
36 |
filename: str
|
37 |
processing_time: float
|
38 |
-
storage_key:
|
39 |
|
40 |
@dataclass
|
41 |
class JobCreationDTO:
|
42 |
"""DTO for job creation."""
|
43 |
job_id: str
|
|
|
44 |
status: str
|
45 |
message: str
|
46 |
check_url: str
|
47 |
-
external_job_id: Optional[str] = None
|
48 |
file_size_mb: Optional[float] = None
|
|
|
16 |
class JobStatusDTO:
|
17 |
"""DTO for job status."""
|
18 |
job_id: str
|
19 |
+
external_job_id: Optional[str] = None
|
20 |
status: str
|
21 |
created_at: datetime
|
22 |
updated_at: datetime
|
|
|
23 |
filename: Optional[str] = None
|
24 |
file_size_mb: Optional[float] = None
|
25 |
output_format: Optional[str] = None
|
|
|
35 |
media_type: str
|
36 |
filename: str
|
37 |
processing_time: float
|
38 |
+
storage_key: str = None
|
39 |
|
40 |
@dataclass
|
41 |
class JobCreationDTO:
|
42 |
"""DTO for job creation."""
|
43 |
job_id: str
|
44 |
+
external_job_id: Optional[str] = None
|
45 |
status: str
|
46 |
message: str
|
47 |
check_url: str
|
|
|
48 |
file_size_mb: Optional[float] = None
|
application/use_cases/process_job.py
CHANGED
@@ -88,25 +88,18 @@ class ProcessJobUseCase:
|
|
88 |
processing_time=processing_time
|
89 |
)
|
90 |
|
91 |
-
#
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
status="completed",
|
102 |
-
processing_time=processing_time,
|
103 |
-
bearer_token=bearer_token,
|
104 |
-
external_job_id=external_job_id
|
105 |
-
)
|
106 |
-
else:
|
107 |
-
logger.debug(f"Skipping N8N notification for job {job_id} - no bearer token available")
|
108 |
|
109 |
-
# Clear bearer token for security
|
110 |
await self.job_repository.clear_bearer_token(job_id)
|
111 |
|
112 |
logger.info(f"Job {job_id} completed in {processing_time:.2f} seconds")
|
@@ -122,32 +115,29 @@ class ProcessJobUseCase:
|
|
122 |
processing_time=processing_time
|
123 |
)
|
124 |
|
125 |
-
#
|
126 |
-
if self.notification_service:
|
127 |
-
try:
|
128 |
-
job_record = await self.job_repository.get(job_id)
|
129 |
-
bearer_token = job_record.bearer_token if job_record else None
|
130 |
-
external_job_id = job_record.external_job_id if job_record else None
|
131 |
-
|
132 |
-
if bearer_token:
|
133 |
-
await self.notification_service.send_job_completion_notification(
|
134 |
-
job_id=job_id,
|
135 |
-
status="failed",
|
136 |
-
processing_time=processing_time,
|
137 |
-
bearer_token=bearer_token,
|
138 |
-
external_job_id=external_job_id
|
139 |
-
)
|
140 |
-
else:
|
141 |
-
logger.debug(f"Skipping N8N failure notification for job {job_id} - no bearer token available")
|
142 |
-
|
143 |
-
except Exception as notify_error:
|
144 |
-
# Don't let notification failures mask the original job failure
|
145 |
-
logger.warning(f"Failed to send failure notification for job {job_id}: {notify_error}")
|
146 |
-
|
147 |
-
# Clear bearer token for security (regardless of notification status)
|
148 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
await self.job_repository.clear_bearer_token(job_id)
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
|
153 |
raise
|
|
|
88 |
processing_time=processing_time
|
89 |
)
|
90 |
|
91 |
+
# Get job record to retrieve bearer token
|
92 |
+
job_record = await self.job_repository.get(job_id)
|
93 |
+
bearer_token = job_record.bearer_token if job_record else None
|
94 |
+
|
95 |
+
await self.notification_service.send_job_completion_notification(
|
96 |
+
job_id=job_id,
|
97 |
+
status="completed",
|
98 |
+
processing_time=processing_time,
|
99 |
+
bearer_token=bearer_token
|
100 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
|
102 |
+
# Clear bearer token for security after notification is sent
|
103 |
await self.job_repository.clear_bearer_token(job_id)
|
104 |
|
105 |
logger.info(f"Job {job_id} completed in {processing_time:.2f} seconds")
|
|
|
115 |
processing_time=processing_time
|
116 |
)
|
117 |
|
118 |
+
# Get job record to retrieve bearer token for failure notification
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
try:
|
120 |
+
job_record = await self.job_repository.get(job_id)
|
121 |
+
bearer_token = job_record.bearer_token if job_record else None
|
122 |
+
|
123 |
+
await self.notification_service.send_job_completion_notification(
|
124 |
+
job_id=job_id,
|
125 |
+
status="failed",
|
126 |
+
processing_time=processing_time,
|
127 |
+
bearer_token=bearer_token
|
128 |
+
)
|
129 |
+
|
130 |
+
# Clear bearer token for security after notification is sent
|
131 |
await self.job_repository.clear_bearer_token(job_id)
|
132 |
+
|
133 |
+
except Exception as notify_error:
|
134 |
+
# Don't let notification failures mask the original job failure
|
135 |
+
logger.warning(f"Failed to send failure notification for job {job_id}: {notify_error}")
|
136 |
+
|
137 |
+
# Still clear the token even if notification failed
|
138 |
+
try:
|
139 |
+
await self.job_repository.clear_bearer_token(job_id)
|
140 |
+
except Exception:
|
141 |
+
logger.warning(f"Failed to clear bearer token for failed job {job_id}")
|
142 |
|
143 |
raise
|
domain/services/notification_service.py
CHANGED
@@ -17,6 +17,5 @@ class NotificationService(Protocol):
|
|
17 |
job_id: str,
|
18 |
status: str,
|
19 |
processing_time: float,
|
20 |
-
bearer_token: str
|
21 |
-
external_job_id: Optional[str] = None) -> NotificationResponse:
|
22 |
...
|
|
|
17 |
job_id: str,
|
18 |
status: str,
|
19 |
processing_time: float,
|
20 |
+
bearer_token: Optional[str] = None) -> NotificationResponse:
|
|
|
21 |
...
|
infrastructure/clients/n8n/models.py
CHANGED
@@ -18,6 +18,6 @@ class WebhooksResponse:
|
|
18 |
class N8NClientProtocol(Protocol):
|
19 |
"""Protocol defining the API client interface."""
|
20 |
|
21 |
-
async def post_completion_event(self, data: WebhooksRequest, bearer_token:
|
22 |
-
"""Post to webhooks endpoint with client bearer token
|
23 |
...
|
|
|
18 |
class N8NClientProtocol(Protocol):
|
19 |
"""Protocol defining the API client interface."""
|
20 |
|
21 |
+
async def post_completion_event(self, data: WebhooksRequest, bearer_token: Optional[str] = None) -> WebhooksResponse:
|
22 |
+
"""Post to webhooks endpoint with optional client bearer token."""
|
23 |
...
|
infrastructure/clients/n8n/n8n_client.py
CHANGED
@@ -21,8 +21,9 @@ class N8NClient(N8NClientProtocol):
|
|
21 |
)
|
22 |
|
23 |
def _get_default_headers(self) -> Dict[str, str]:
|
24 |
-
"""Get default headers
|
25 |
return {
|
|
|
26 |
"Content-Type": "application/json",
|
27 |
"Accept": "application/json"
|
28 |
}
|
@@ -87,26 +88,19 @@ class N8NClient(N8NClientProtocol):
|
|
87 |
self.logger.error(f"Failed to parse JSON response from {url}: {str(e)}")
|
88 |
raise APIResponseError(f"Invalid JSON response: {str(e)}", response.status_code)
|
89 |
|
90 |
-
async def post_completion_event(self, data: WebhooksRequest, bearer_token:
|
91 |
-
"""Post to webhooks endpoint with client bearer token
|
92 |
from dataclasses import asdict
|
93 |
payload = asdict(data)
|
94 |
|
95 |
# Extract job_id for header and remove from payload
|
96 |
job_id = payload.pop("job_id")
|
|
|
97 |
|
98 |
-
#
|
99 |
-
|
100 |
-
"
|
101 |
-
|
102 |
-
}
|
103 |
-
|
104 |
-
# Add external job ID as custom header if provided
|
105 |
-
if external_job_id:
|
106 |
-
custom_headers["externalJobID"] = external_job_id
|
107 |
-
self.logger.debug(f"Sending N8N notification for job {job_id} with external job ID {external_job_id}")
|
108 |
-
else:
|
109 |
-
self.logger.debug(f"Sending N8N notification for job {job_id} using client bearer token")
|
110 |
|
111 |
response_data = await self._make_request("POST", "/lovable-analysis", payload, custom_headers)
|
112 |
|
|
|
21 |
)
|
22 |
|
23 |
def _get_default_headers(self) -> Dict[str, str]:
|
24 |
+
"""Get default headers including authentication."""
|
25 |
return {
|
26 |
+
"Authorization": f"Bearer {self.settings.token}",
|
27 |
"Content-Type": "application/json",
|
28 |
"Accept": "application/json"
|
29 |
}
|
|
|
88 |
self.logger.error(f"Failed to parse JSON response from {url}: {str(e)}")
|
89 |
raise APIResponseError(f"Invalid JSON response: {str(e)}", response.status_code)
|
90 |
|
91 |
+
async def post_completion_event(self, data: WebhooksRequest, bearer_token: Optional[str] = None) -> WebhooksResponse:
|
92 |
+
"""Post to webhooks endpoint with optional client bearer token."""
|
93 |
from dataclasses import asdict
|
94 |
payload = asdict(data)
|
95 |
|
96 |
# Extract job_id for header and remove from payload
|
97 |
job_id = payload.pop("job_id")
|
98 |
+
custom_headers = {"rowID": job_id}
|
99 |
|
100 |
+
# Add client bearer token if provided
|
101 |
+
if bearer_token:
|
102 |
+
custom_headers["Authorization"] = f"Bearer {bearer_token}"
|
103 |
+
self.logger.debug(f"Adding client bearer token to N8N request for job {job_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
|
105 |
response_data = await self._make_request("POST", "/lovable-analysis", payload, custom_headers)
|
106 |
|
infrastructure/clients/n8n/settings.py
CHANGED
@@ -6,4 +6,5 @@ from typing import Optional
|
|
6 |
class ClientSettings:
|
7 |
"""Settings for the HTTP client."""
|
8 |
base_url: str
|
|
|
9 |
timeout: Optional[int] = 30
|
|
|
6 |
class ClientSettings:
|
7 |
"""Settings for the HTTP client."""
|
8 |
base_url: str
|
9 |
+
token: str
|
10 |
timeout: Optional[int] = 30
|
infrastructure/config/settings.py
CHANGED
@@ -88,8 +88,8 @@ class Settings(BaseSettings):
|
|
88 |
|
89 |
# N8N Configuration
|
90 |
n8n_base_url: str = Field(default="http://localhost:5678", env="N8N_BASE_URL")
|
|
|
91 |
n8n_timeout: int = Field(default=30, env="N8N_TIMEOUT")
|
92 |
-
n8n_enabled: bool = Field(default=True, env="N8N_ENABLED")
|
93 |
|
94 |
# Authentication Configuration
|
95 |
enforce_authentication: bool = Field(default=True, env="ENFORCE_AUTHENTICATION")
|
|
|
88 |
|
89 |
# N8N Configuration
|
90 |
n8n_base_url: str = Field(default="http://localhost:5678", env="N8N_BASE_URL")
|
91 |
+
n8n_token: str = Field(env="N8N_TOKEN")
|
92 |
n8n_timeout: int = Field(default=30, env="N8N_TIMEOUT")
|
|
|
93 |
|
94 |
# Authentication Configuration
|
95 |
enforce_authentication: bool = Field(default=True, env="ENFORCE_AUTHENTICATION")
|
infrastructure/services/container.py
CHANGED
@@ -61,16 +61,12 @@ class ServiceContainer:
|
|
61 |
else:
|
62 |
raise ValueError(f"Unsupported storage type: {settings.storage_type}")
|
63 |
|
64 |
-
def _create_notification_service(self) ->
|
65 |
-
"""Create N8N notification service
|
66 |
-
#
|
67 |
-
if not settings.n8n_enabled:
|
68 |
-
logging.getLogger(__name__).info("N8N notifications disabled via configuration")
|
69 |
-
return None
|
70 |
-
|
71 |
-
# Create N8N client settings (no token needed - client tokens are passed per request)
|
72 |
n8n_settings = ClientSettings(
|
73 |
base_url=settings.n8n_base_url,
|
|
|
74 |
timeout=getattr(settings, 'n8n_timeout', 30)
|
75 |
)
|
76 |
|
@@ -97,10 +93,8 @@ class ServiceContainer:
|
|
97 |
async def shutdown(self):
|
98 |
"""Cleanup services on shutdown."""
|
99 |
await self.cleanup_service.stop()
|
100 |
-
# Close N8N client if it
|
101 |
-
if (self.notification_service
|
102 |
-
hasattr(self.notification_service, 'n8n_client') and
|
103 |
-
hasattr(self.notification_service.n8n_client, 'close')):
|
104 |
await self.notification_service.n8n_client.close()
|
105 |
|
106 |
# Convenience function for getting services
|
|
|
61 |
else:
|
62 |
raise ValueError(f"Unsupported storage type: {settings.storage_type}")
|
63 |
|
64 |
+
def _create_notification_service(self) -> N8NNotificationService:
|
65 |
+
"""Create N8N notification service."""
|
66 |
+
# Create N8N client settings
|
|
|
|
|
|
|
|
|
|
|
67 |
n8n_settings = ClientSettings(
|
68 |
base_url=settings.n8n_base_url,
|
69 |
+
token=settings.n8n_token,
|
70 |
timeout=getattr(settings, 'n8n_timeout', 30)
|
71 |
)
|
72 |
|
|
|
93 |
async def shutdown(self):
|
94 |
"""Cleanup services on shutdown."""
|
95 |
await self.cleanup_service.stop()
|
96 |
+
# Close N8N client if it has async cleanup
|
97 |
+
if hasattr(self.notification_service.n8n_client, 'close'):
|
|
|
|
|
98 |
await self.notification_service.n8n_client.close()
|
99 |
|
100 |
# Convenience function for getting services
|
infrastructure/services/n8n_notification_service.py
CHANGED
@@ -25,17 +25,19 @@ class N8NNotificationService(NotificationService):
|
|
25 |
job_id: str,
|
26 |
status: str,
|
27 |
processing_time: float,
|
28 |
-
bearer_token: str
|
29 |
-
|
30 |
-
"""Send job completion notification via N8N with client bearer token for authentication."""
|
31 |
try:
|
32 |
message = f"Job {job_id} {status} in {processing_time:.2f}s"
|
33 |
request = NotificationRequest(message=message, job_id=job_id)
|
34 |
|
35 |
-
# Pass
|
36 |
-
response = await self.n8n_client.post_completion_event(request, bearer_token
|
37 |
|
38 |
-
|
|
|
|
|
|
|
39 |
|
40 |
return NotificationResponse(acknowledged=response.acknowledged)
|
41 |
|
|
|
25 |
job_id: str,
|
26 |
status: str,
|
27 |
processing_time: float,
|
28 |
+
bearer_token: Optional[str] = None) -> NotificationResponse:
|
29 |
+
"""Send job completion notification via N8N with optional client bearer token."""
|
|
|
30 |
try:
|
31 |
message = f"Job {job_id} {status} in {processing_time:.2f}s"
|
32 |
request = NotificationRequest(message=message, job_id=job_id)
|
33 |
|
34 |
+
# Pass bearer token to N8N client if provided
|
35 |
+
response = await self.n8n_client.post_completion_event(request, bearer_token)
|
36 |
|
37 |
+
if bearer_token:
|
38 |
+
logger.debug(f"Sent N8N notification for job {job_id} with client bearer token")
|
39 |
+
else:
|
40 |
+
logger.debug(f"Sent N8N notification for job {job_id} without client bearer token")
|
41 |
|
42 |
return NotificationResponse(acknowledged=response.acknowledged)
|
43 |
|
interfaces/api/dependencies.py
CHANGED
@@ -1,7 +1,7 @@
|
|
|
|
1 |
"""FastAPI dependency injection configuration."""
|
2 |
from typing import Annotated, Optional
|
3 |
from fastapi import Depends, UploadFile, Form, HTTPException, Request, Header
|
4 |
-
from fastapi.security import HTTPBearer
|
5 |
from pydantic import BaseModel, Field, validator
|
6 |
import re
|
7 |
|
@@ -9,13 +9,6 @@ from application.use_cases.container import UseCaseContainer
|
|
9 |
from infrastructure.services.container import ServiceContainer
|
10 |
from infrastructure.services.jwt_validation_service import JWTValidationService
|
11 |
|
12 |
-
# Create HTTPBearer scheme that will be recognized by OpenAPI
|
13 |
-
bearer_scheme = HTTPBearer(
|
14 |
-
scheme_name="BearerAuth",
|
15 |
-
bearerFormat="JWT",
|
16 |
-
description="JWT Bearer token authentication"
|
17 |
-
)
|
18 |
-
|
19 |
class ExtractionRequest(BaseModel):
|
20 |
"""Request model for audio extraction."""
|
21 |
output_format: str = Field(default="mp3", description="Output audio format")
|
@@ -63,14 +56,14 @@ async def validate_video_file(video: UploadFile) -> UploadFile:
|
|
63 |
|
64 |
return video
|
65 |
|
66 |
-
async def
|
67 |
-
|
68 |
) -> str:
|
69 |
"""
|
70 |
-
Extract and validate bearer token from
|
71 |
|
72 |
Args:
|
73 |
-
|
74 |
|
75 |
Returns:
|
76 |
str: The validated bearer token
|
@@ -78,20 +71,23 @@ async def get_validated_token(
|
|
78 |
Raises:
|
79 |
HTTPException: 401 if token is missing or invalid
|
80 |
"""
|
81 |
-
|
82 |
-
|
83 |
-
if not settings.enforce_authentication:
|
84 |
-
return "authentication-disabled"
|
85 |
-
|
86 |
-
if not credentials:
|
87 |
raise HTTPException(
|
88 |
status_code=401,
|
89 |
detail="Missing Authorization header",
|
90 |
headers={"WWW-Authenticate": "Bearer"}
|
91 |
)
|
92 |
|
93 |
-
#
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
if not token:
|
96 |
raise HTTPException(
|
97 |
status_code=401,
|
@@ -123,4 +119,4 @@ ValidatedVideo = Annotated[UploadFile, Depends(validate_video_file)]
|
|
123 |
ExtractionParams = Annotated[ExtractionRequest, Depends(extraction_params)]
|
124 |
Services = Annotated[ServiceContainer, Depends(get_services)]
|
125 |
UseCases = Annotated[UseCaseContainer, Depends(get_use_cases)]
|
126 |
-
BearerToken = Annotated[str, Depends(
|
|
|
1 |
+
# interfaces/api/dependencies.py
|
2 |
"""FastAPI dependency injection configuration."""
|
3 |
from typing import Annotated, Optional
|
4 |
from fastapi import Depends, UploadFile, Form, HTTPException, Request, Header
|
|
|
5 |
from pydantic import BaseModel, Field, validator
|
6 |
import re
|
7 |
|
|
|
9 |
from infrastructure.services.container import ServiceContainer
|
10 |
from infrastructure.services.jwt_validation_service import JWTValidationService
|
11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
class ExtractionRequest(BaseModel):
|
13 |
"""Request model for audio extraction."""
|
14 |
output_format: str = Field(default="mp3", description="Output audio format")
|
|
|
56 |
|
57 |
return video
|
58 |
|
59 |
+
async def validate_bearer_token(
|
60 |
+
authorization: Optional[str] = Header(None, description="Bearer token for authentication")
|
61 |
) -> str:
|
62 |
"""
|
63 |
+
Extract and validate bearer token from Authorization header.
|
64 |
|
65 |
Args:
|
66 |
+
authorization: Authorization header value
|
67 |
|
68 |
Returns:
|
69 |
str: The validated bearer token
|
|
|
71 |
Raises:
|
72 |
HTTPException: 401 if token is missing or invalid
|
73 |
"""
|
74 |
+
if not authorization:
|
|
|
|
|
|
|
|
|
|
|
75 |
raise HTTPException(
|
76 |
status_code=401,
|
77 |
detail="Missing Authorization header",
|
78 |
headers={"WWW-Authenticate": "Bearer"}
|
79 |
)
|
80 |
|
81 |
+
# Check if it starts with "Bearer "
|
82 |
+
if not authorization.startswith("Bearer "):
|
83 |
+
raise HTTPException(
|
84 |
+
status_code=401,
|
85 |
+
detail="Invalid Authorization header format. Expected: Bearer <token>",
|
86 |
+
headers={"WWW-Authenticate": "Bearer"}
|
87 |
+
)
|
88 |
+
|
89 |
+
# Extract token
|
90 |
+
token = authorization[7:] # Remove "Bearer " prefix
|
91 |
if not token:
|
92 |
raise HTTPException(
|
93 |
status_code=401,
|
|
|
119 |
ExtractionParams = Annotated[ExtractionRequest, Depends(extraction_params)]
|
120 |
Services = Annotated[ServiceContainer, Depends(get_services)]
|
121 |
UseCases = Annotated[UseCaseContainer, Depends(get_use_cases)]
|
122 |
+
BearerToken = Annotated[str, Depends(validate_bearer_token)]
|
interfaces/api/routes/extraction_routes.py
CHANGED
@@ -22,12 +22,9 @@ router = APIRouter()
|
|
22 |
description="""
|
23 |
Extract audio from uploaded video file.
|
24 |
|
25 |
-
**Authentication Required**: This endpoint requires a valid JWT Bearer token.
|
26 |
-
|
27 |
All files are processed asynchronously and return a job ID for tracking progress.
|
28 |
Use the job status endpoint to check processing status and download the result when complete.
|
29 |
""",
|
30 |
-
|
31 |
responses={
|
32 |
202: {
|
33 |
"description": "Job created for async processing",
|
|
|
22 |
description="""
|
23 |
Extract audio from uploaded video file.
|
24 |
|
|
|
|
|
25 |
All files are processed asynchronously and return a job ID for tracking progress.
|
26 |
Use the job status endpoint to check processing status and download the result when complete.
|
27 |
""",
|
|
|
28 |
responses={
|
29 |
202: {
|
30 |
"description": "Job created for async processing",
|
interfaces/api/routes/job_routes.py
CHANGED
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|
19 |
@router.get("/jobs/{job_id}",
|
20 |
response_model=JobStatusResponse,
|
21 |
summary="Get Job Status",
|
22 |
-
description="Check the status of an audio extraction job
|
23 |
responses={
|
24 |
200: {"description": "Job status retrieved successfully"},
|
25 |
401: {"description": "Authentication required", "content": {"application/json": {"example": {"error": "Authentication required", "code": "AUTHENTICATION_ERROR"}}}},
|
@@ -78,8 +78,6 @@ async def get_job_status(
|
|
78 |
description="""
|
79 |
Download the audio file from a completed extraction job.
|
80 |
|
81 |
-
**Authentication Required**: This endpoint requires a valid JWT Bearer token.
|
82 |
-
|
83 |
Optionally specify start and end times to download only a portion of the audio:
|
84 |
- start: Start time in HH:MM:SS format (e.g., 00:01:30 for 1 minute 30 seconds)
|
85 |
- end: End time in HH:MM:SS format (e.g., 00:03:45 for 3 minutes 45 seconds)
|
|
|
19 |
@router.get("/jobs/{job_id}",
|
20 |
response_model=JobStatusResponse,
|
21 |
summary="Get Job Status",
|
22 |
+
description="Check the status of an audio extraction job",
|
23 |
responses={
|
24 |
200: {"description": "Job status retrieved successfully"},
|
25 |
401: {"description": "Authentication required", "content": {"application/json": {"example": {"error": "Authentication required", "code": "AUTHENTICATION_ERROR"}}}},
|
|
|
78 |
description="""
|
79 |
Download the audio file from a completed extraction job.
|
80 |
|
|
|
|
|
81 |
Optionally specify start and end times to download only a portion of the audio:
|
82 |
- start: Start time in HH:MM:SS format (e.g., 00:01:30 for 1 minute 30 seconds)
|
83 |
- end: End time in HH:MM:SS format (e.g., 00:03:45 for 3 minutes 45 seconds)
|