File size: 10,575 Bytes
c05ab2d
8e4a45e
c05ab2d
3229cec
c05ab2d
 
887aa67
c05ab2d
050d8f8
887aa67
c05ab2d
50c20bf
887aa67
d72816f
e39877e
50c20bf
 
c05ab2d
 
 
 
 
 
 
 
c4fd9c8
 
c05ab2d
 
 
8e4a45e
7715603
 
 
 
 
 
 
 
c4fd9c8
50c20bf
 
 
 
be85b16
 
 
 
 
 
 
 
 
7715603
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a650e63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e4a45e
a650e63
 
 
 
bcc8074
 
 
 
1c302c7
 
 
 
 
 
 
 
 
 
 
 
 
 
43df312
 
 
 
 
 
 
 
 
 
7715603
 
 
1bd7131
 
bc8ed4e
1bd7131
7715603
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1bd7131
c05ab2d
c4fd9c8
1bd7131
 
 
 
c4fd9c8
 
be85b16
 
 
c05ab2d
 
 
 
 
a95115e
c05ab2d
 
 
 
 
8e4a45e
7715603
 
 
178694a
7715603
 
c05ab2d
8e4a45e
1c302c7
 
 
8e4a45e
43df312
 
 
8e4a45e
bcc8074
 
 
8e4a45e
7715603
 
 
 
 
 
 
 
 
 
 
 
050d8f8
 
 
1bd7131
661c02e
de3cb16
9869f13
d72816f
c05ab2d
 
887aa67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c05ab2d
 
887aa67
 
c05ab2d
050d8f8
887aa67
 
 
 
c05ab2d
 
 
 
 
 
 
 
 
 
 
 
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""
FastAPI Application - API Gateway with credit management and AI services.
"""
import os
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

from core.database import engine, DB_FILENAME
from core.api_response import APIError, error_response, status_to_error_code, ErrorCode
from routers import auth, blink, contact, credits, general, gemini, payments, schema
from services.drive_service import DriveService
from services.db_service import init_database, reset_database
from services.db_service.register_config import register_db_service_config

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# Initialize Drive Service
drive_service = DriveService()

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan manager."""
    # Startup Banner
    logger.info("═" * 60)
    logger.info("    API Gateway v1.0.0 - Starting Up")
    logger.info("═" * 60)
    
    # Database Initialization Section
    logger.info("")
    logger.info("πŸ“¦ [DATABASE INITIALIZATION]")
    
    # Register DB Service configuration
    register_db_service_config()
    logger.info("βœ… DB Service configured")
    
    # Initialize Backup Service
    from services.backup_service import initialize_backup_service
    local_drive_service = DriveService() 
    backup_service = initialize_backup_service(
        local_drive_service,
        min_interval_seconds=30  # Minimum 30s between backups
    )
    logger.info("βœ… Backup Service initialized")
    
    # Check for RESET_DB environment variable
    if os.getenv("RESET_DB", "").lower() == "true":
        logger.warning(f"⚠️  RESET_DB enabled - Clearing local database ({DB_FILENAME})")
        if os.path.exists(DB_FILENAME):
            os.remove(DB_FILENAME)
            logger.info("   Local database deleted")
        
        # Reset database (drop + create all tables)
        await reset_database(engine)
        logger.info("βœ… Database reset complete")
    else:
        # Startup: Download DB from Drive ONLY if local file doesn't exist
        if not os.path.exists(DB_FILENAME):
            logger.info("⬇️  Downloading database from Google Drive...")
            drive_service.download_db()
        else:
            logger.info("βœ“  Local database found")
        
        # Initialize database (create tables if not exist)
        await init_database(engine)
        logger.info("βœ… Database initialized")
    
    # Service Registration Section
    logger.info("")
    logger.info("βš™οΈ  [SERVICE REGISTRATION]")
    
    # Register Auth Service configuration
    from services.auth_service import register_auth_service
    register_auth_service(
        required_urls=[
            "/blink",
            "/api/*",  # All admin blink API endpoints
            "/contact",
            "/gemini/*",
            "/credits/balance",
            "/credits/history",
            "/payments/create-order",
            "/payments/verify/*",
        ],
        optional_urls=[
            "/",  # Home page works with or without auth
        ],
        public_urls=[
            "/health",
            "/auth/*",
            "/payments/packages",  # Public pricing info
            "/payments/webhook/*",  # Webhooks from payment gateway
            "/docs",
            "/openapi.json",
            "/redoc",
        ],
        jwt_secret=os.getenv("JWT_SECRET"),
        jwt_algorithm="HS256",
        jwt_expiry_hours=24,
        google_client_id=os.getenv("AUTH_SIGN_IN_GOOGLE_CLIENT_ID"),
        admin_emails=os.getenv("ADMIN_EMAILS", "").split(",") if os.getenv("ADMIN_EMAILS") else [],
    )
    logger.info("βœ… Auth Service configured")
    
    # Register Credit Service configuration
    from services.credit_service import CreditServiceConfig
    CreditServiceConfig.register(
        route_configs={
            # Synchronous operations - credits confirmed/refunded immediately
            "/gemini/generate-animation-prompt": {
                "cost": 1,
                "type": "sync"
            },
            "/gemini/edit-image": {
                "cost": 1,
                "type": "sync"
            },
            "/gemini/generate-text": {
                "cost": 1,
                "type": "sync"
            },
            "/gemini/analyze-image": {
                "cost": 1,
                "type": "sync"
            },
            # Asynchronous operations - credits reserved until job completes
            "/gemini/generate-video": {
                "cost": 10,
                "type": "async"
            },

            "/gemini/job/{job_id}": {
                "cost": 0,  # No additional cost for status checks
                "type": "async"
            }
        }
    )
    logger.info("βœ… Credit Service configured")
    
    # Register Audit Service configuration
    from services.audit_service import AuditServiceConfig
    AuditServiceConfig.register(
        excluded_paths=[
            "/health",
            "/docs",
            "/openapi.json",
            "/redoc"
        ],
        log_all_requests=True,
        log_response_bodies=False  # Privacy: don't log response bodies
    )
    logger.info("βœ… Audit Service configured")
    
    # Register API Key Service configuration
    from services.gemini_service import APIKeyServiceConfig
    APIKeyServiceConfig.register(
        rotation_strategy="least_used",  # or "round_robin"
        cooldown_seconds=60,  # Wait 1 min after quota error
        max_requests_per_minute=60,
        retry_on_quota_error=True  # Auto-retry with different key
    )
    logger.info("βœ… API Key Service configured")
    
    # Worker Pool Section
    logger.info("")
    logger.info("πŸ‘· [WORKER POOL]")
    
    # Start background job worker
    from services.gemini_service import start_worker, stop_worker
    await start_worker()
    logger.info("βœ… Worker pool started")
    
    # Log CORS configuration
    allowed_origins = os.getenv("CORS_ORIGINS").split(",")
    logger.info("")
    logger.info("🌐 [NETWORK CONFIGURATION]")
    logger.info(f"βœ… CORS origins: {', '.join(allowed_origins)}")
    
    # Startup Complete Summary
    logger.info("")
    logger.info("═" * 60)
    logger.info("    πŸš€ API Gateway Ready")
    logger.info("    β€’ Database: βœ… Ready")
    logger.info("    β€’ Services: 5 initialized (DB, Auth, Credit, Audit, API Key)")
    logger.info("    β€’ Workers: 15 active")
    logger.info("    β€’ Endpoint: http://0.0.0.0:8000")
    logger.info("═" * 60)
    logger.info("")
    
    yield
    
    # Stop background job worker
    await stop_worker()
    logger.info("Background job worker stopped")
    
    # Shutdown: Upload DB to Drive
    logger.info("Shutdown: Uploading database to Google Drive...")
    from services.backup_service import get_backup_service
    backup_service = get_backup_service()
    await backup_service.backup_async(force=True)  # Force backup on shutdown
    logger.info("Shutting down...")


# Create FastAPI application
app = FastAPI(
    title="APIGateway",
    description="API for receiving and processing encrypted user data",
    version="1.0.0",
    lifespan=lifespan
)


# Middleware order matters! They execute in reverse order (bottom to top)
# Request flow: CORS β†’ Auth β†’ APIKey β†’ Audit β†’ Credit β†’ Router
# So we add them in REVERSE order (last added = first to run on REQUEST)

from services.credit_service import CreditMiddleware
app.add_middleware(CreditMiddleware)


from services.audit_service import AuditMiddleware
app.add_middleware(AuditMiddleware)


from services.gemini_service import APIKeyMiddleware
app.add_middleware(APIKeyMiddleware)


from services.auth_service import AuthMiddleware
app.add_middleware(AuthMiddleware)


# CORS middleware MUST be added last to ensure error responses also have CORS headers
allowed_origins = os.getenv("CORS_ORIGINS").split(",")

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


app.include_router(general.router)
app.include_router(auth.router)
app.include_router(blink.router)
app.include_router(gemini.router)
app.include_router(credits.router)
app.include_router(payments.router)
app.include_router(contact.router)
app.include_router(schema.router)


@app.exception_handler(APIError)
async def api_error_handler(request: Request, exc: APIError):
    """Handle custom APIError exceptions with standardized format."""
    logger.warning(f"API Error: {exc.code} - {exc.message}")
    return JSONResponse(
        status_code=exc.status_code,
        content=error_response(exc.code, exc.message, exc.details)
    )


@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    """Convert HTTPException to standardized error format."""
    code = status_to_error_code(exc.status_code)
    return JSONResponse(
        status_code=exc.status_code,
        content=error_response(code, str(exc.detail))
    )


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    """Handle Pydantic validation errors with detailed field info."""
    errors = []
    for error in exc.errors():
        errors.append({
            "field": ".".join(str(loc) for loc in error["loc"]),
            "message": error["msg"],
            "type": error["type"]
        })
    
    return JSONResponse(
        status_code=422,
        content=error_response(
            ErrorCode.VALIDATION_ERROR,
            "Request validation failed",
            {"errors": errors}
        )
    )


@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """Global exception handler for unexpected errors."""
    logger.error(f"Unhandled exception: {exc}", exc_info=True)
    return JSONResponse(
        status_code=500,
        content=error_response(
            ErrorCode.SERVER_ERROR,
            "An unexpected error occurred. Please try again later."
        )
    )


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app:app",
        host="0.0.0.0",
        port=8000,
        reload=True,
        log_level="info"
    )