mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 00:38:38 +02:00
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:
@@ -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: () =>
|
||||
|
||||
Reference in New Issue
Block a user