-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
198 lines (175 loc) · 7.46 KB
/
main.py
File metadata and controls
198 lines (175 loc) · 7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
"""
This application serves as an API endpoint for the Signals and Trends project that connects
the frontend platform with the backend database.
"""
import os
import logging
import datetime
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from src import routers
from src.authentication import authenticate_user
from src.config.logging_config import setup_logging
from src.bugsnag_config import configure_bugsnag, setup_bugsnag_logging, get_bugsnag_middleware, BUGSNAG_ENABLED
# Load environment variables and set up logging
load_dotenv()
setup_logging()
# Get application version
app_version = os.environ.get("RELEASE_VERSION", "dev-fixed")
app_env = os.environ.get("ENVIRONMENT", "development")
# Override environment setting if in local mode
if os.environ.get("ENV_MODE") == "local":
app_env = "local"
logging.info(f"Starting application - version: {app_version}, environment: {app_env}")
# Configure Bugsnag for error tracking
configure_bugsnag()
setup_bugsnag_logging()
app = FastAPI(
debug=False,
title="Future Trends and Signals API",
version="3.0.0-beta",
summary="""The Future Trends and Signals (FTSS) API powers user experiences on UNDP Future
Trends and Signals System by providing functionality to to manage signals, trends and users.""",
description="""The FTSS API serves as a interface for the
[UNDP Future Trends and Signals System](https://signals.data.undp.org),
facilitating interaction between the front-end application and the underlying relational database.
This API enables users to submit, retrieve, and update data related to signals, trends, and user
profiles within the platform.
As a private API, it mandates authentication for all endpoints to ensure secure access.
Authentication is achieved by including the `access_token` in the request header, utilising JWT tokens
issued by [Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens).
This mechanism not only secures the API but also allows for the automatic recording of user information
derived from the API token. Approved signals and trends can be accesses using a predefined API key for
integration with other applications.
""".strip().replace(
" ", " "
),
contact={
"name": "UNDP Data Futures Platform",
"url": "https://data.undp.org",
"email": "data@undp.org",
},
openapi_tags=[
{"name": "signals", "description": "CRUD operations on signals."},
{"name": "trends", "description": "CRUD operations on trends."},
{"name": "users", "description": "CRUD operations on users."},
{"name": "choices", "description": "List valid options for forms fields."},
{"name": "favourites", "description": "Manage user's favorite signals."},
],
docs_url="/",
redoc_url=None,
)
# Add global exception handler to report errors to Bugsnag
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logging.error(f"Unhandled exception: {str(exc)}", exc_info=True)
if BUGSNAG_ENABLED:
import bugsnag
bugsnag.notify(
exc,
metadata={
"request": {
"url": str(request.url),
"method": request.method,
"headers": dict(request.headers),
"client": request.client.host if request.client else None,
}
}
)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)
# Configure CORS - simplified for local development
local_origins = [
"http://localhost:5175",
"http://127.0.0.1:5175",
"http://localhost:3000",
"http://127.0.0.1:3000"
]
# Create a custom middleware class for handling CORS
class CORSHandlerMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Handle OPTIONS preflight requests
if request.method == "OPTIONS":
headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
"Access-Control-Allow-Headers": "access_token, Authorization, Content-Type, Accept, X-API-Key",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "600", # Cache preflight for 10 minutes
}
# Set specific origin if in local mode
origin = request.headers.get("origin")
if os.environ.get("ENV_MODE") == "local" and origin:
headers["Access-Control-Allow-Origin"] = origin
return JSONResponse(content={}, status_code=200, headers=headers)
# Process all other requests normally
response = await call_next(request)
return response
# Apply custom CORS middleware BEFORE the standard CORS middleware
app.add_middleware(CORSHandlerMiddleware)
# Standard CORS middleware (as a backup)
if os.environ.get("ENV_MODE") == "local":
logging.info(f"Local mode: using specific CORS origins: {local_origins}")
app.add_middleware(
CORSMiddleware,
allow_origins=local_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*", "access_token", "Authorization", "Content-Type"],
expose_headers=["*"],
)
else:
# Production mode - use more restrictive CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*", "access_token", "Authorization", "Content-Type"],
)
# Add Bugsnag exception handling middleware
# Important: Add middleware AFTER registering exception handlers
bugsnag_app = get_bugsnag_middleware(app)
for router in routers.ALL:
app.include_router(router=router, dependencies=[Depends(authenticate_user)])
# Add diagnostic endpoint for health checks and Bugsnag verification
@app.get("/_health", include_in_schema=False)
async def health_check():
"""Health check endpoint that also shows the current environment and version."""
return {
"status": "ok",
"environment": app_env,
"version": app_version,
"bugsnag_enabled": BUGSNAG_ENABLED
}
# Test endpoint to trigger a test error report to Bugsnag if enabled
@app.get("/_test-error", include_in_schema=False)
async def test_error():
"""Trigger a test error to verify Bugsnag is working."""
if BUGSNAG_ENABLED:
import bugsnag
bugsnag.notify(
Exception("Test error triggered via /_test-error endpoint"),
metadata={
"test_info": {
"environment": app_env,
"version": app_version,
"timestamp": str(datetime.datetime.now())
}
}
)
return {"status": "error_reported", "message": "Test error sent to Bugsnag"}
else:
return {"status": "disabled", "message": "Bugsnag is not enabled"}
# Add special route for handling OPTIONS requests to /users/me
@app.options("/users/me", include_in_schema=False)
async def options_users_me():
"""Handle OPTIONS requests to /users/me specifically."""
return {}
# Use the Bugsnag middleware wrapped app for ASGI
app = bugsnag_app