api: drop bearer-token gate from /api/* CRUD + simulate
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

The SPA can't carry a Bearer header — there's no client-side mechanism
to obtain the RECOMPUTE_BEARER_TOKEN, and the value can't safely be
embedded in the JS bundle. Result: every POST/PATCH/DELETE on
scenarios/life-events/goals + every /simulate + /compare returned 401
in prod, breaking the SPA end-to-end.

Strip require_bearer from the routers. Authentik forward-auth on the
SPA path (/) is now the security boundary; /api/* is open at both
ingress + app level. Single-tenant personal tool — the data is
the user's own anonymous numeric projections.

Kept on /recompute (heavy admin batch in app.py) since that's an
operator action, not a user one.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-09 23:56:37 +00:00
parent 2efd1edad0
commit f781afe3fa
4 changed files with 2 additions and 14 deletions

View file

@ -5,7 +5,6 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import delete, select from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from fire_planner.api.auth import require_bearer
from fire_planner.api.dependencies import get_session from fire_planner.api.dependencies import get_session
from fire_planner.api.schemas import GoalCreate, GoalOut from fire_planner.api.schemas import GoalCreate, GoalOut
from fire_planner.db import RetirementGoal, Scenario from fire_planner.db import RetirementGoal, Scenario
@ -34,7 +33,6 @@ async def list_goals(
"/scenarios/{scenario_id}/goals", "/scenarios/{scenario_id}/goals",
response_model=GoalOut, response_model=GoalOut,
status_code=201, status_code=201,
dependencies=[Depends(require_bearer)],
) )
async def create_goal( async def create_goal(
scenario_id: int, scenario_id: int,
@ -55,7 +53,6 @@ async def create_goal(
"/goals/{goal_id}", "/goals/{goal_id}",
status_code=204, status_code=204,
response_model=None, response_model=None,
dependencies=[Depends(require_bearer)],
) )
async def delete_goal( async def delete_goal(
goal_id: int, goal_id: int,

View file

@ -5,7 +5,6 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import delete, select from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from fire_planner.api.auth import require_bearer
from fire_planner.api.dependencies import get_session from fire_planner.api.dependencies import get_session
from fire_planner.api.schemas import LifeEventCreate, LifeEventOut, LifeEventPatch from fire_planner.api.schemas import LifeEventCreate, LifeEventOut, LifeEventPatch
from fire_planner.db import LifeEvent, Scenario from fire_planner.db import LifeEvent, Scenario
@ -34,7 +33,6 @@ async def list_events(
"/scenarios/{scenario_id}/life-events", "/scenarios/{scenario_id}/life-events",
response_model=LifeEventOut, response_model=LifeEventOut,
status_code=201, status_code=201,
dependencies=[Depends(require_bearer)],
) )
async def create_event( async def create_event(
scenario_id: int, scenario_id: int,
@ -56,7 +54,6 @@ async def create_event(
@router.patch( @router.patch(
"/life-events/{event_id}", "/life-events/{event_id}",
response_model=LifeEventOut, response_model=LifeEventOut,
dependencies=[Depends(require_bearer)],
) )
async def patch_event( async def patch_event(
event_id: int, event_id: int,
@ -80,7 +77,6 @@ async def patch_event(
"/life-events/{event_id}", "/life-events/{event_id}",
status_code=204, status_code=204,
response_model=None, response_model=None,
dependencies=[Depends(require_bearer)],
) )
async def delete_event( async def delete_event(
event_id: int, event_id: int,

View file

@ -16,7 +16,6 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import delete, select from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from fire_planner.api.auth import require_bearer
from fire_planner.api.dependencies import get_session from fire_planner.api.dependencies import get_session
from fire_planner.api.schemas import ( from fire_planner.api.schemas import (
ProjectionPoint, ProjectionPoint,
@ -58,7 +57,6 @@ async def get_scenario(
"", "",
response_model=ScenarioOut, response_model=ScenarioOut,
status_code=201, status_code=201,
dependencies=[Depends(require_bearer)],
) )
async def create_scenario( async def create_scenario(
payload: ScenarioCreate, payload: ScenarioCreate,
@ -96,7 +94,6 @@ async def create_scenario(
@router.patch( @router.patch(
"/{scenario_id}", "/{scenario_id}",
response_model=ScenarioOut, response_model=ScenarioOut,
dependencies=[Depends(require_bearer)],
) )
async def patch_scenario( async def patch_scenario(
scenario_id: int, scenario_id: int,
@ -121,7 +118,6 @@ async def patch_scenario(
"/{scenario_id}", "/{scenario_id}",
status_code=204, status_code=204,
response_model=None, response_model=None,
dependencies=[Depends(require_bearer)],
) )
async def delete_scenario( async def delete_scenario(
scenario_id: int, scenario_id: int,

View file

@ -15,9 +15,8 @@ from decimal import Decimal
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, HTTPException
from fire_planner.api.auth import require_bearer
from fire_planner.api.schemas import ( from fire_planner.api.schemas import (
CompareRequest, CompareRequest,
CompareResult, CompareResult,
@ -32,7 +31,7 @@ from fire_planner.returns.shiller import load_from_csv, synthetic_returns
from fire_planner.scenarios import build_regime_schedule, build_strategy from fire_planner.scenarios import build_regime_schedule, build_strategy
from fire_planner.simulator import SimulationResult, simulate from fire_planner.simulator import SimulationResult, simulate
router = APIRouter(tags=["simulate"], dependencies=[Depends(require_bearer)]) router = APIRouter(tags=["simulate"])
_RETURNS_CSV = Path("/data/shiller_returns.csv") _RETURNS_CSV = Path("/data/shiller_returns.csv")