Files
rengine/api/app/api/v1/onboarding.py
T
Yogesh OjhaandClaude Fable 5.1 695c4803bb refactor(engine): one intensity table, tiered fields, built-in Default engine
Every stage config field is a decision; rate, concurrency, timeout and
retries come from a per-tool intensity profile the stage reads as its
transport. 173 fields become 83: 41 transport values, 40 constants and
the dead ones leave the models, and the builder groups by capability
with support stages as checkboxes and advanced fields behind one
disclosure. Aggressive now changes what a tool is handed.

Every project carries a built-in Default engine, seeded on project
creation and on first list, undeletable: stage defaults, subfinder as
the only passive source, vulnerability scan on with nuclei.

From the tracing review: tool args can no longer restate a flag the
stage set, the session check obeys the probe scheme, excluded scope
reaches every stage that reads addresses, hosts or paths, the sitemap
and source-map providers throttle at the transport rate, the blind tier
obeys its switch, the fuzzing lanes split the budget, and the removed
private-address and evidence switches are back as advanced fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq4xYJbgW5euUa9sCWBKFw
2026-09-14 12:26:27 +05:30

136 lines
4.4 KiB
Python

from typing import Annotated
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import CurrentSuperuser, CurrentUser
from app.core.database import get_session
from app.services.instance_settings import InstanceSettingsService
from app.services.scan_engine import ScanEngineService
from shared.models.api_key import APIKey
from shared.models.notification_channel import NotificationChannel
from shared.models.project import Project, ProjectCreate, ProjectRead
from shared.models.proxy import Proxy
from shared.utils.datetime import utc_now
from shared.utils.slug import add_with_unique_slug
router = APIRouter(prefix="/onboarding", tags=["onboarding"])
def get_service(
session: Annotated[AsyncSession, Depends(get_session)],
) -> InstanceSettingsService:
return InstanceSettingsService(session)
class OnboardingProgress(BaseModel):
current_step: int
state: dict
class OnboardingSummary(BaseModel):
proxies: int
channels: int
integrations: int
ai_enabled: bool
class OnboardingStatus(BaseModel):
completed: bool
can_setup: bool
current_step: int
instance_name: str
mode: str
summary: OnboardingSummary
async def _summary(session: AsyncSession, ai_enabled: bool) -> OnboardingSummary:
proxies = await session.execute(select(func.count(Proxy.id)).where(Proxy.is_active))
channels = await session.execute(
select(func.count(NotificationChannel.id)).where(NotificationChannel.is_active)
)
integrations = await session.execute(
select(func.count(APIKey.id)).where(APIKey.is_enabled)
)
return OnboardingSummary(
proxies=proxies.scalar_one(),
channels=channels.scalar_one(),
integrations=integrations.scalar_one(),
ai_enabled=ai_enabled,
)
async def _status(
session: AsyncSession, service: InstanceSettingsService, can_setup: bool
) -> OnboardingStatus:
settings = await service.get_or_create()
return OnboardingStatus(
completed=settings.onboarding_completed,
can_setup=can_setup,
current_step=settings.onboarding_step,
instance_name=settings.instance_name,
mode=settings.mode,
summary=await _summary(session, settings.ai_enabled),
)
@router.get("/status", response_model=OnboardingStatus)
async def get_status(
current_user: CurrentUser,
session: Annotated[AsyncSession, Depends(get_session)],
service: Annotated[InstanceSettingsService, Depends(get_service)],
):
return await _status(session, service, can_setup=current_user.is_superuser)
@router.patch("/progress", response_model=OnboardingStatus)
async def update_progress(
data: OnboardingProgress,
_current_user: CurrentSuperuser,
session: Annotated[AsyncSession, Depends(get_session)],
service: Annotated[InstanceSettingsService, Depends(get_service)],
):
settings = await service.get_or_create()
settings.onboarding_step = data.current_step
settings.onboarding_state = dict(data.state)
settings.updated_at = utc_now()
await session.commit()
return await _status(session, service, can_setup=True)
@router.post("/complete", response_model=OnboardingStatus)
async def complete_onboarding(
current_user: CurrentSuperuser,
session: Annotated[AsyncSession, Depends(get_session)],
service: Annotated[InstanceSettingsService, Depends(get_service)],
):
settings = await service.get_or_create()
if not settings.onboarding_completed:
settings.onboarding_completed = True
settings.onboarding_completed_at = utc_now()
settings.onboarding_completed_by = current_user.id
settings.updated_at = utc_now()
await session.commit()
return await _status(session, service, can_setup=True)
@router.post("/first-project", response_model=ProjectRead)
async def create_first_project(
data: ProjectCreate,
current_user: CurrentSuperuser,
session: Annotated[AsyncSession, Depends(get_session)],
):
project = Project(
name=data.name,
description=data.description,
label=data.label,
created_by=current_user.id,
)
await add_with_unique_slug(session, project, data.name)
await ScanEngineService(session).ensure_builtin(project.id, current_user.id)
await session.commit()
await session.refresh(project)
return project