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)
This commit is contained in:
maziggy
2026-06-04 15:50:44 +02:00
parent 764eb58540
commit 18d534c945
45 changed files with 4891 additions and 630 deletions
+79 -2
View File
@@ -1185,6 +1185,51 @@ export interface CloudLoginResponse {
tfa_key?: string | null;
}
// Orca Cloud types — paste-flow PKCE handshake against auth.orcaslicer.com.
// See backend/app/services/orca_cloud.py for the deep dive on why this
// flow is paste-based rather than callback-based.
export type OrcaOAuthProvider = 'google' | 'apple' | 'github';
export interface OrcaAuthStartResponse {
auth_url: string;
}
export interface OrcaAuthStatusResponse {
connected: boolean;
email: string | null;
user_id: string | null;
}
// Orca profiles are shaped to match Bambu Cloud's SlicerSetting on the wire
// so the frontend can use the same visual components for both surfaces (cards,
// grouped sections, filter bar). Backend handles the source-specific
// transformation in routes/orca_cloud.py::_orca_to_setting.
export interface OrcaProfileMeta {
setting_id: string;
name: string;
type: string;
version: string | null;
user_id: string | null;
updated_time: string | null;
is_custom: boolean;
}
export interface OrcaProfileListResponse {
filament: OrcaProfileMeta[];
printer: OrcaProfileMeta[];
process: OrcaProfileMeta[];
}
export interface OrcaProfileDetail {
setting_id: string;
name: string;
type: string;
version: string | null;
base_id: string | null;
update_time: string | null;
setting: Record<string, unknown>;
}
// MakerWorld integration. Full metadata/instance shapes come back as
// Record<string, unknown> — MakerWorld's API adds fields over time, so we
// pass them through verbatim rather than maintaining a brittle mirror.
@@ -1305,7 +1350,7 @@ export interface BuiltinFilament {
// - Source-aware refs (`*_preset: PresetRef`) — new SliceModal that picks
// across cloud / local / standard tiers. Source-aware refs win when both
// are present in the same payload.
export type PresetSource = 'cloud' | 'local' | 'standard';
export type PresetSource = 'orca_cloud' | 'cloud' | 'local' | 'standard';
export interface PresetRef {
source: PresetSource;
id: string;
@@ -1384,10 +1429,14 @@ export interface UnifiedPresetsBySlot {
filament: UnifiedPreset[];
}
export interface UnifiedPresetsResponse {
// Priority order: orca_cloud > cloud > local > standard. Dedup is applied
// backend-side so each name appears in only one tier.
orca_cloud: UnifiedPresetsBySlot;
cloud: UnifiedPresetsBySlot;
local: UnifiedPresetsBySlot;
standard: UnifiedPresetsBySlot;
cloud_status: SlicerCloudStatus;
orca_cloud_status: SlicerCloudStatus;
}
export interface SliceResponse {
@@ -2814,7 +2863,7 @@ export type Permission =
| 'system:read'
| 'settings:read' | 'settings:update' | 'settings:backup' | 'settings:restore'
| 'github:backup' | 'github:restore'
| 'cloud:auth'
| 'cloud:auth' | 'orca_cloud:auth'
| 'makerworld:view' | 'makerworld:import'
| 'api_keys:read' | 'api_keys:create' | 'api_keys:update' | 'api_keys:delete'
| 'users:read' | 'users:create' | 'users:update' | 'users:delete'
@@ -4341,6 +4390,34 @@ export const api = {
}),
cloudLogout: () =>
request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
// Orca Cloud — paste-based PKCE flow for OAuth (Google/Apple/GitHub),
// direct credentials for email+password. start() returns an auth URL the
// user opens in their browser; after sign-in they paste the callback URL
// back via finish(). password() skips the dance entirely.
orcaCloudStartAuth: (provider: OrcaOAuthProvider = 'google') =>
request<OrcaAuthStartResponse>('/orca-cloud/auth/start', {
method: 'POST',
body: JSON.stringify({ provider }),
}),
orcaCloudFinishAuth: (callback_url: string) =>
request<OrcaAuthStatusResponse>('/orca-cloud/auth/finish', {
method: 'POST',
body: JSON.stringify({ callback_url }),
}),
orcaCloudPasswordLogin: (email: string, password: string) =>
request<OrcaAuthStatusResponse>('/orca-cloud/auth/password', {
method: 'POST',
body: JSON.stringify({ email, password }),
}),
orcaCloudStatus: () =>
request<OrcaAuthStatusResponse>('/orca-cloud/status'),
orcaCloudLogout: () =>
request<{ success: boolean }>('/orca-cloud/logout', { method: 'POST' }),
orcaCloudListProfiles: () =>
request<OrcaProfileListResponse>('/orca-cloud/profiles'),
orcaCloudGetProfile: (id: string) =>
request<OrcaProfileDetail>(`/orca-cloud/profiles/${id}`),
getCloudSettings: (version = '02.04.00.70') =>
request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
getBuiltinFilaments: () =>