Files
bambuddy/backend/app/schemas/orca_cloud.py
T
maziggy 18d534c945 feat(orca-cloud): integrate Orca Cloud profile sync across UI, slicer and SpoolBuddy
Reads, lists, and slices with profiles from a user's Orca Cloud account
  (OrcaSlicer 2.4.0-alpha's Supabase-backed sync) alongside the existing
  Bambu Cloud integration. Four sign-in providers (Google / Apple / GitHub /
  email+password); password defaults. Paste-flow PKCE because Orca's
  Supabase project only allowlists localhost redirect_to — open feature
  request at OrcaSlicer/OrcaSlicer#14028.

  Surfaces:
  - Profiles tab: new "Orca Cloud" tab next to "Bambu Cloud" with the same
    rich layout (search + 5 filter dropdowns + 3-column grouped grid +
    read-only detail modal)
  - SliceModal: 4-tier preset picker (orca_cloud > local > bambu cloud >
    standard); separate status banner per cloud; metadata-aware pre-pick
    scores Orca filaments above local (Orca's sync_pull returns full
    content inline so filament_type / filament_colour come for free, no
    per-setting fetch rate-limit dance)
  - ConfigureAmsSlotModal: orca_cloud as a new preset source (prefixed
    orca_<UUID> to match local_/builtin_); generic Bambu filament-ID
    derivation from parsed material (printer firmware can't grok Orca
    UUIDs); slot mapping persists preset_source='orca_cloud'
  - SpoolForm / SpoolBuddyWriteTagPage: Orca filaments merge into the
    cloud preset list via Promise.allSettled (OrcaProfileMeta is
    structurally identical to SlicerSetting)

  Backend:
  - services/orca_cloud.py: OrcaCloudService with PKCE / token exchange /
    single-use refresh rotation / get_user_info / list_profiles via the
    bare /sync/pull bootstrap path
  - routes/orca_cloud.py: 7 endpoints (auth/start, auth/finish,
    auth/password, status, logout, profiles, profiles/{id}); router-level
    _cloud_api_key_gate + per-route cloud_caller() so API-keyed callers
    (SpoolBuddy kiosk) properly resolve their owner User; just-in-time
    refresh with atomic persist-before-API-call
  - routes/slicer_presets.py: _fetch_orca_cloud_presets mirrors the Bambu
    Cloud fetcher (status vocabulary, 5min cache, permission shortcut);
    _dedupe_by_name extended to 4 tiers; UnifiedPresetsResponse gains
    orca_cloud + orca_cloud_status
  - services/preset_resolver.py: PresetRef.source extended with
    "orca_cloud"; _resolve_orca_cloud walks list + filters
  - 8 columns on users table for tokens (5 persistent) + transient PKCE
    handshake state with 10-min TTL (3); dialect-branched DATETIME /
    TIMESTAMP; auth-disabled mode falls back to Settings table
  - orca_cloud:auth permission folded into can_access_cloud API-key scope
    (same trust dimension)
2026-06-04 15:50:44 +02:00

95 lines
3.5 KiB
Python

"""Schemas for Orca Cloud auth + profile sync endpoints."""
from typing import Literal
from pydantic import BaseModel, Field
# The three OAuth providers Orca's sign-in surface offers. Supabase
# accepts the bare lowercase provider name in the authorize query string.
OrcaOAuthProvider = Literal["google", "apple", "github"]
class OrcaAuthStartRequest(BaseModel):
"""Body for ``POST /orca-cloud/auth/start``. Provider defaults to
``google`` so existing clients that send an empty body keep working."""
provider: OrcaOAuthProvider = Field(default="google", description="OAuth provider to use for sign-in")
class OrcaAuthStartResponse(BaseModel):
"""Returned by ``POST /orca-cloud/auth/start``. The frontend opens
``auth_url`` in a new tab. After the user signs in to Orca, they copy the
redirected URL from their address bar and POST it to
``/orca-cloud/auth/finish`` to complete the handshake."""
auth_url: str = Field(..., description="URL to open for Orca Cloud sign-in")
class OrcaAuthFinishRequest(BaseModel):
"""Submitted by the frontend after the user pastes the callback URL from
their browser. The URL contains a Supabase ``code`` (and our ``state``)
that we exchange for tokens."""
callback_url: str = Field(..., description="The full URL the browser was redirected to after sign-in")
class OrcaAuthPasswordRequest(BaseModel):
"""Body for ``POST /orca-cloud/auth/password``. Whether this succeeds
depends on Orca's Supabase project — their desktop client refuses
password payloads, but the web sign-in offers email+password as one
option. We forward the credentials and surface the server's response.
``email`` is plain ``str`` rather than Pydantic's ``EmailStr`` to avoid
pulling in the optional ``email-validator`` dependency — Supabase will
reject malformed addresses with a clear error itself, and the existing
Bambu Cloud login schema uses the same approach."""
email: str = Field(..., min_length=1)
password: str = Field(..., min_length=1)
class OrcaAuthStatusResponse(BaseModel):
"""Connection status for the Orca Cloud tab."""
connected: bool
email: str | None = None
user_id: str | None = None
class OrcaProfileMeta(BaseModel):
"""A single profile, shaped to match the Bambu Cloud ``SlicerSetting``
schema so the frontend can render Orca profiles with the existing
Bambu Cloud visual components (cards, filter bar, grouping). Per-source
differences (Orca's IDs are UUIDs not Bambu's ``PFU...`` prefix; Orca
types are ``machine`` / ``process`` / ``filament`` whereas Bambu uses
``printer`` / ``process`` / ``filament``) are normalized at the route
layer before the response leaves the backend."""
setting_id: str
name: str
type: str
version: str | None = None
user_id: str | None = None
updated_time: str | None = None
is_custom: bool = True
class OrcaProfileListResponse(BaseModel):
"""Groups Orca profiles by type, matching ``SlicerSettingsResponse``."""
filament: list[OrcaProfileMeta] = []
printer: list[OrcaProfileMeta] = []
process: list[OrcaProfileMeta] = []
class OrcaProfileDetail(BaseModel):
"""Single profile's full content, shaped to match ``SlicerSettingDetail``
so the frontend's detail modal can render it without translation."""
setting_id: str
name: str
type: str
version: str | None = None
base_id: str | None = None
update_time: str | None = None
setting: dict