from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, RedirectResponse, Response from pydantic import ValidationError from fastapi.middleware.cors import CORSMiddleware from starlette.datastructures import MutableHeaders from starlette.types import ASGIApp, Receive, Scope, Send # FastAPI アプリケーション作成 app = FastAPI( title="日本小規模企業向け会計システム" ) class SecurityHeadersMiddleware: """キャッシュ禁止・セキュリティヘッダーを全レスポンスに付与(ASGIネイティブ実装)”""" def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": await self.app(scope, receive, send) return async def send_with_headers(message): if message["type"] == "http.response.start": headers = MutableHeaders(scope=message) headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private" headers["Pragma"] = "no-cache" headers["Expires"] = "0" headers["X-Frame-Options"] = "DENY" headers["X-Content-Type-Options"] = "nosniff" headers["X-XSS-Protection"] = "1; mode=block" await send(message) await self.app(scope, receive, send_with_headers) app.add_middleware(SecurityHeadersMiddleware) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization"], ) # ───────────────────────── # 自動データベース遷移 # ───────────────────────── @app.on_event("startup") async def run_startup_migration(): """アプリケーション起動時にデータベス遷移を実行""" try: from app.db_auto_migration import run_auto_migration run_auto_migration() except Exception as e: print(f"⚠️ 遷移エラー(無視): {e}") @app.exception_handler(ValidationError) async def validation_exception_handler(request: Request, exc: ValidationError): # ここでは最初のエラーだけ簡潔に返す(必要に応じて整形) first = exc.errors()[0] msg = first.get("msg", "入力値が不正です。") return JSONResponse(status_code=400, content={"detail": msg}) # ───────────────────────── # ルート(稼働確認) # ───────────────────────── @app.get("/", include_in_schema=False) def root(): return RedirectResponse("/login.html") # ───────────────────────── # ルーター登録(※ 必ず app 定義の後) # ───────────────────────── from app.modules.users.router import router as users_router app.include_router(users_router) from app.modules.accounts.router import router as accounts_router app.include_router(accounts_router) from app.modules.journals.router import router as journals_router app.include_router(journals_router) from app.modules.ledger.router import router as ledger_router app.include_router(ledger_router) from app.modules.trial_balance.router import router as trial_balance_router app.include_router(trial_balance_router) from app.modules.opening_balances.router import router as opening_router app.include_router(opening_router) from app.modules.general_ledger.router import router as general_ledger_router app.include_router(general_ledger_router) from app.modules.opening_balances.lock_router import router as opening_balance_lock_router app.include_router(opening_balance_lock_router) from app.routers import journal_entries from app.routers import cash from app.routers import accounts app.include_router(journal_entries.router) app.include_router(cash.router) app.include_router(accounts.router) from app.routers import month_locks app.include_router(month_locks.router) from app.routers import year_locks app.include_router(year_locks.router) from app.routers import cash app.include_router(cash.router) from app.routers import file_upload app.include_router(file_upload.router) from app.modules.egov.router import router as egov_router app.include_router(egov_router) from app.modules.bank_statements.router import router as bank_statements_router app.include_router(bank_statements_router) # ───────────────────────── # 給与モジュール (Payroll Module) # ───────────────────────── from app.payroll.employees.router import router as payroll_employees_router app.include_router(payroll_employees_router) from app.payroll.settings.router import router as payroll_settings_router app.include_router(payroll_settings_router) from app.payroll.settings.standard_remuneration_router import router as standard_remuneration_router app.include_router(standard_remuneration_router) from app.payroll.calculation.router import router as payroll_calculation_router app.include_router(payroll_calculation_router) from app.payroll.bonus.router import router as payroll_bonus_router app.include_router(payroll_bonus_router) from app.payroll.vouchers.router import router as payroll_vouchers_router app.include_router(payroll_vouchers_router) from app.payroll.withholding_slip.router import router as withholding_slip_router app.include_router(withholding_slip_router) from app.modules.nenkin_portal.router import router as nenkin_portal_router app.include_router(nenkin_portal_router) from fastapi.staticfiles import StaticFiles import os from pathlib import Path # アップロードファイル用の静的ファイル配信 uploads_dir = Path("uploads") uploads_dir.mkdir(exist_ok=True) app.mount( "/uploads", StaticFiles(directory="uploads"), name="uploads", ) # 静的ファイル配信(コメントアウト - ディレクトリが存在しない) # app.mount( # "/static", # StaticFiles(directory="app/static", html=True), # name="static", # ) # フロントエンドファイル配信 from fastapi.staticfiles import StaticFiles from pathlib import Path # フロントエンド配信(/app/frontend) BASE_DIR = Path(__file__).resolve().parent.parent # /app frontend_path = BASE_DIR / "frontend" print("Frontend path =", frontend_path) if frontend_path.exists(): app.mount( "/", StaticFiles(directory=frontend_path), name="frontend", ) else: print(f"Frontend path not found: {frontend_path}")