267 lines
11 KiB
Python
267 lines
11 KiB
Python
import datetime, json, os, pytz, random, secrets, contextlib # , uvicorn
|
|
|
|
from enum import Enum
|
|
#from datetime import datetime, time, timedelta
|
|
from fastapi import Body, Cookie, Depends, FastAPI, File, Form, Header, HTTPException, Path, Query, Request, Response, status, UploadFile
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from functools import lru_cache
|
|
from pydantic import BaseModel, EmailStr, Field
|
|
from typing import Dict, List, Optional, Set, Union
|
|
|
|
from . import config
|
|
# from app.lib_general import common_route_params, Common_Route_Params
|
|
import logging
|
|
import app.log
|
|
from app.log import setup_logging
|
|
|
|
# Import middleware with alias to avoid shadowing 'app' FastAPI instance
|
|
from app.middleware import add_process_time_header as process_time_middleware
|
|
|
|
# Centralized router registry
|
|
from app.routers.registry import setup_routers
|
|
|
|
from app.db_sql import sql_select, reset_redis, reconnect_db
|
|
from app.lib_config_v3 import bootstrap_db_config, validate_critical_config
|
|
|
|
|
|
print('### **** *** ** * The Aether API v3.0 using FastAPI is loading... * ** *** **** ###')
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
# log.setLevel(logging.DEBUG) # DEBUG > INFO > WARNING > ERROR > CRITICAL
|
|
#logging.basicConfig(
|
|
#format='[%(asctime)s] %(levelname)s @ %(module)s.%(funcName)s()#%(lineno)d: %(message)s'
|
|
#)
|
|
|
|
@contextlib.asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""
|
|
Handles application startup and shutdown lifecycle.
|
|
"""
|
|
# 1. Initialize Logging early but safely
|
|
setup_logging(config.settings)
|
|
log.info('### **** *** ** * Aether API v3.0 using FastAPI - Startup Lifespan Initiated * ** *** **** ###')
|
|
|
|
# 2. Bootstrapping Configuration from DB with robust error handling
|
|
log.info("Bootstrapping Configuration...")
|
|
|
|
# Save original settings for fallback
|
|
orig_db_server = config.settings.DB_SERVER
|
|
orig_db_user = config.settings.DB_USER
|
|
orig_db_pass = config.settings.DB_PASS
|
|
orig_db_name = config.settings.DB_NAME
|
|
orig_db_port = config.settings.DB_PORT
|
|
|
|
try:
|
|
if bootstrap_db_config(config.settings):
|
|
log.info("Successfully bootstrapped configuration from database.")
|
|
# Re-initialize the database engine with new credentials/URI
|
|
if reconnect_db():
|
|
log.info("Database connection re-established with production configuration.")
|
|
else:
|
|
log.warning("FAILED to re-establish database connection after bootstrap. Reverting to .env settings.")
|
|
config.settings.DB_SERVER = orig_db_server
|
|
config.settings.DB_USER = orig_db_user
|
|
config.settings.DB_PASS = orig_db_pass
|
|
config.settings.DB_NAME = orig_db_name
|
|
config.settings.DB_PORT = orig_db_port
|
|
reconnect_db()
|
|
else:
|
|
log.warning("System bootstrap from DB returned no results. Using environment defaults.")
|
|
except Exception as e:
|
|
log.error(f"Unexpected error during configuration bootstrap: {e}. Falling back to .env settings.")
|
|
config.settings.DB_SERVER = orig_db_server
|
|
config.settings.DB_USER = orig_db_user
|
|
config.settings.DB_PASS = orig_db_pass
|
|
config.settings.DB_NAME = orig_db_name
|
|
config.settings.DB_PORT = orig_db_port
|
|
reconnect_db()
|
|
|
|
# 3. Final validation of critical infrastructure
|
|
validate_critical_config(config.settings)
|
|
|
|
log.info('### **** *** ** * Aether API v3.0 using FastAPI - Startup Sequence Complete * ** *** **** ###')
|
|
|
|
yield
|
|
|
|
# Shutdown logic
|
|
log.info('### **** *** ** * Aether API v3.0 using FastAPI - Shutdown Lifespan Initiated * ** *** **** ###')
|
|
log.info('The Aether FastAPI API is shutting down...')
|
|
|
|
|
|
print('### **** *** ** * Aether API v3.0 using FastAPI - About to try FastAPI() while loading... * ** *** **** ###')
|
|
app = FastAPI(
|
|
# debug = True,
|
|
title = 'Aether API',
|
|
description = 'One Sky IT\'s Aether API v3.0 using FastAPI.',
|
|
version = '3.00.03',
|
|
operationsSorter = 'method',
|
|
lifespan = lifespan,
|
|
)
|
|
|
|
|
|
# @lru_cache()
|
|
# def get_settings():
|
|
# return config.Settings()
|
|
|
|
|
|
app.mount('/static', StaticFiles(directory='static'), name='static')
|
|
|
|
|
|
# Register all application routes
|
|
setup_routers(app)
|
|
|
|
|
|
# Updated 2026-02-23
|
|
# BEGIN: CORS
|
|
# NOTE: Eventually this should query the DB for the specific list based on the cfg table and or site_domain table. That way it is dynamic and only allowing those defined in the DB. No wildcards or regex.
|
|
# NOTE: Need to include .localhost for less browser restrictions! Mainly for audio and video.
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
# allow_origins = origins,
|
|
allow_origins = config.settings.ORIGINS,
|
|
allow_origin_regex = config.settings.ORIGINS_REGEX,
|
|
# allow_origin_regex = 'https://.*\.oneskyit\.com',
|
|
allow_credentials = True,
|
|
allow_methods = ['*'],
|
|
allow_headers = ['*'],
|
|
#expose_headers = [],
|
|
#max_age = 600,
|
|
)
|
|
# END: CORS
|
|
|
|
|
|
# Updated 2026-02-23
|
|
# Add middleware to ensure Access-Control-Allow-Private-Network is present
|
|
# when the response already includes CORS allow-origin (i.e. origin was allowed).
|
|
@app.middleware("http")
|
|
async def cors_pna_middleware(request: Request, call_next):
|
|
"""Add `Access-Control-Allow-Private-Network: true` to responses
|
|
only when CORS has already allowed the request's origin. This avoids
|
|
echoing PNA for disallowed origins and leverages the existing
|
|
CORSMiddleware origin validation.
|
|
"""
|
|
response = await call_next(request)
|
|
# Rely on existing CORS logic (CORSMiddleware) to validate origin.
|
|
# Only add the PNA header if an Allow-Origin header is present.
|
|
if response.headers.get('access-control-allow-origin') or response.headers.get('Access-Control-Allow-Origin'):
|
|
response.headers['Access-Control-Allow-Private-Network'] = 'true'
|
|
return response
|
|
|
|
|
|
# Updated 2026-02-23
|
|
# Temporary debug middleware: logs Origin and PNA-related request/response headers
|
|
# Activate only when an Origin header is present to limit log noise.
|
|
# @app.middleware("http")
|
|
# async def debug_pna_logging_middleware(request: Request, call_next):
|
|
# origin = request.headers.get('origin')
|
|
# if origin:
|
|
# acrpn = request.headers.get('access-control-request-private-network')
|
|
# acrm = request.headers.get('access-control-request-method')
|
|
# log.debug(f"PNA_DEBUG REQ: method={request.method} path={request.url.path} remote={getattr(request.client, 'host', None)} origin={origin} acr_method={acrm} acr_private_network={acrpn}")
|
|
|
|
# response = await call_next(request)
|
|
|
|
# if origin:
|
|
# # collect CORS/PNA-related response headers for visibility
|
|
# interesting = {k: v for k, v in response.headers.items() if k.lower().startswith('access-control-') or k.lower() == 'vary'}
|
|
# log.debug(f"PNA_DEBUG RESP: status={response.status_code} headers={interesting}")
|
|
|
|
# return response
|
|
|
|
# Register utility middleware from external module
|
|
app.middleware('http')(process_time_middleware)
|
|
|
|
|
|
# ### BEGIN ### API Main ### fastapi_root() ###
|
|
@app.get('/', tags=['Root'], response_class=PlainTextResponse)
|
|
async def fastapi_root(response: Response = Response):
|
|
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARN, WARNING, ERROR, EXCEPTION, CRITICAL
|
|
log.debug(locals())
|
|
|
|
# log.info(config.settings.APP_NAME)
|
|
log.info('One Sky IT\'s Aether API root (FastAPI)')
|
|
|
|
log.info('***')
|
|
log.debug('This is debug') # 10 DEBUG
|
|
log.info('This is info') # 20 INFO
|
|
log.warning('This is a warning') # 30 WARNING (and WARN)
|
|
log.error('This is an error') # 40 ERROR
|
|
log.exception('This is an exception') # 40 ERROR
|
|
log.critical('This is critical') # 50 CRITICAL
|
|
log.info('^^^')
|
|
|
|
log.warning('Resetting Redis...')
|
|
reset_redis()
|
|
log.info('Reset Redis')
|
|
|
|
response_data = {}
|
|
response_data['message'] = 'This is One Sky IT\'s Aether API root (FastAPI).'
|
|
|
|
|
|
current_datetime = datetime.datetime.now()
|
|
current_datetime_string = current_datetime.isoformat()
|
|
|
|
timezone = pytz.timezone("America/New_York")
|
|
current_datetime_tz = timezone.localize(current_datetime)
|
|
current_datetime_tz_string = current_datetime_tz.isoformat()
|
|
|
|
current_datetime_utc = datetime.datetime.utcnow()
|
|
current_datetime_utc_string = current_datetime_utc.isoformat()
|
|
|
|
current_datetime_utc_localize = pytz.utc.localize(current_datetime_utc)
|
|
current_datetime_utc_localize_string = current_datetime_utc_localize.isoformat()
|
|
|
|
current_datetime_utc_localize_pst = current_datetime_utc_localize.astimezone(pytz.timezone("America/Los_Angeles"))
|
|
current_datetime_utc_localize_pst_string = current_datetime_utc_localize_pst.isoformat()
|
|
|
|
response_data['datetime'] = current_datetime_string
|
|
response_data['datetime_tz'] = current_datetime_tz_string
|
|
response_data['datetime_utc'] = current_datetime_utc_string
|
|
response_data['datetime_utc_localize'] = current_datetime_utc_localize_string
|
|
response_data['datetime_utc_localize_pst'] = current_datetime_utc_localize_pst_string
|
|
|
|
response_data['url_safe_string_4_bytes_1'] = secrets.token_urlsafe(4)
|
|
|
|
response_data['url_safe_string_8_bytes_1'] = secrets.token_urlsafe(8)
|
|
response_data['url_safe_string_8_bytes_2'] = secrets.token_urlsafe(8)
|
|
response_data['url_safe_string_8_bytes_3'] = secrets.token_urlsafe(8)
|
|
response_data['url_safe_string_8_bytes_4'] = secrets.token_urlsafe(8)
|
|
response_data['url_safe_string_8_bytes_5'] = secrets.token_urlsafe(8)
|
|
|
|
response_data['url_safe_string_16_bytes_1'] = secrets.token_urlsafe(16)
|
|
response_data['url_safe_string_16_bytes_2'] = secrets.token_urlsafe(16)
|
|
response_data['url_safe_string_16_bytes_3'] = secrets.token_urlsafe(16)
|
|
response_data['url_safe_string_16_bytes_4'] = secrets.token_urlsafe(16)
|
|
response_data['url_safe_string_16_bytes_5'] = secrets.token_urlsafe(16)
|
|
|
|
response_data['hex_string_4_bytes_1'] = secrets.token_hex(4)
|
|
response_data['hex_string_8_bytes_1'] = secrets.token_hex(8)
|
|
response_data['hex_string_16_bytes_1'] = secrets.token_hex(16)
|
|
response_data['hex_string_32_bytes_1'] = secrets.token_hex(32)
|
|
|
|
log.debug(json.dumps(response_data, indent=4))
|
|
return json.dumps(response_data, indent=4) # , sort_keys=True
|
|
# ### END ### API Main ### fastapi_root() ###
|
|
|
|
|
|
# ### BEGIN ### API Main ### generate_id_random() ###
|
|
# NOTE: This is just a quick utility function to generate a bunch of random IDs.
|
|
# Updated 2022-03-30
|
|
@app.get('/generate_id_random', tags=['Root'], response_class=PlainTextResponse)
|
|
async def generate_id_random(response: Response = Response):
|
|
log.setLevel(logging.INFO) # DEBUG, INFO, WARN, WARNING, ERROR, EXCEPTION, CRITICAL
|
|
log.debug(locals())
|
|
|
|
response_data = {}
|
|
|
|
html_list = '<ul>'
|
|
for x in range(50):
|
|
html_list += f'<li>{secrets.token_urlsafe(8)}</li>'
|
|
html_list += '</ul>'
|
|
|
|
return HTMLResponse(content=html_list, status_code=200)
|
|
# ### END ### API Main ### generate_id_random() ###
|