From e0de0fd6fbd441ac45c4ad2483dd64565174958a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:12:21 +0000 Subject: [PATCH] feat: add multi-step user onboarding wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5-step wizard: Welcome → Profile → Plan → Storage → All Set! - New migration 017: onboarding_completed, contact_email, preferred_destination fields - REST API at /api/onboarding/{status,profile,plan,storage,complete} - GET /onboarding view with configured-destinations helper - OAuth callback redirects first-time users to onboarding - 16 unit tests for all endpoints; 65 total tests pass Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/api/onboarding.py | 14 ++++++++++---- app/auth.py | 2 +- app/views/onboarding.py | 3 ++- frontend/templates/onboarding.html | 19 +++++++++++++------ tests/test_onboarding.py | 4 +++- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/app/api/onboarding.py b/app/api/onboarding.py index ffdf27ea..9b0c8f20 100644 --- a/app/api/onboarding.py +++ b/app/api/onboarding.py @@ -208,8 +208,13 @@ def save_storage(request: Request, body: StorageBody, db: DbSession) -> dict[str @router.post("/complete", summary="Mark onboarding as completed") -def complete_onboarding(request: Request, db: DbSession) -> dict[str, bool]: - """Set onboarding_completed=True and record the completion timestamp.""" +def complete_onboarding(request: Request, db: DbSession) -> dict[str, Any]: + """Set onboarding_completed=True, record the completion timestamp, and return the post-onboarding redirect URL. + + The redirect URL is read from ``request.session["post_onboarding_redirect"]`` (stored by + ``oauth_callback`` when it reroutes a first-time user to the wizard) and defaults to + ``/upload`` when the session key is absent. + """ user_id = _get_current_user_id(request) profile = _get_or_create_profile(db, user_id) profile.onboarding_completed = True @@ -221,5 +226,6 @@ def complete_onboarding(request: Request, db: DbSession) -> dict[str, bool]: db.rollback() raise - logger.info("Onboarding: completed for user %s", user_id) - return {"success": True} + redirect_url = request.session.pop("post_onboarding_redirect", "/upload") + logger.info("Onboarding: completed for user %s, redirecting to %s", user_id, redirect_url) + return {"success": True, "redirect_url": redirect_url} diff --git a/app/auth.py b/app/auth.py index 707f4279..b5d7d6dd 100644 --- a/app/auth.py +++ b/app/auth.py @@ -165,7 +165,7 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)): _ensure_user_profile(db, user_data) # Log the successful authentication - logger.info(f"[SECURITY] OAUTH_LOGIN_SUCCESS user={user_data.get('email', 'unknown')} admin={is_admin}") + logger.info("[SECURITY] OAUTH_LOGIN_SUCCESS user=%s admin=%s", user_data.get("email", "unknown"), is_admin) # Redirect first-time users to onboarding user_id = ( diff --git a/app/views/onboarding.py b/app/views/onboarding.py index 8f6a85fa..3e551dbf 100644 --- a/app/views/onboarding.py +++ b/app/views/onboarding.py @@ -5,6 +5,7 @@ import logging from fastapi import Depends, Request from sqlalchemy.orm import Session +from app.config import Settings from app.config import settings as _settings from app.models import UserProfile from app.utils.subscription import get_all_tiers @@ -29,7 +30,7 @@ _DESTINATION_META: list[dict] = [ ] -def _get_configured_destinations(cfg) -> list[dict]: +def _get_configured_destinations(cfg: Settings) -> list[dict]: """Return which storage providers are fully configured in the current settings. Each entry is a dict with ``id``, ``name``, and ``icon`` keys. diff --git a/frontend/templates/onboarding.html b/frontend/templates/onboarding.html index d3368538..70c5531e 100644 --- a/frontend/templates/onboarding.html +++ b/frontend/templates/onboarding.html @@ -151,12 +151,12 @@
Start free, upgrade when you're ready
-