Skip to content

Commit 2c817b8

Browse files
committed
fix(auth): resolve hanging on connecting to organization
1 parent 9daf02a commit 2c817b8

File tree

7 files changed

+75
-22
lines changed

7 files changed

+75
-22
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to BluePLM will be documented in this file.
44

5+
## [2.9.4] - 2025-12-17
6+
7+
### Fixed
8+
- **Auth flow hanging on "Connecting to your organization"**: Removed `ensureUserOrgId()` RPC call that was causing the Supabase client to hang indefinitely. The `linkUserToOrganization()` function (which uses raw fetch) handles org_id setup correctly as a fallback.
9+
- **Added auth timeout safety net**: If organization connection takes longer than 30 seconds, the app will now timeout gracefully instead of hanging forever
10+
- **Added cancel button to connecting screen**: Users can now click "Cancel" to sign out and retry if the connection hangs
11+
12+
### Changed
13+
- **Online users indicator styling**: Changed from bright green notification badge to a subtle neutral badge that doesn't look like a notification
14+
15+
---
16+
517
## [2.9.3] - 2025-12-17
618

719
### Fixed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-plm",
3-
"version": "2.9.3",
3+
"version": "2.9.4",
44
"description": "Product Lifecycle Management for everyone who builds",
55
"main": "dist-electron/main.js",
66
"scripts": {
@@ -113,6 +113,12 @@
113113
"arch": [
114114
"universal"
115115
]
116+
},
117+
{
118+
"target": "zip",
119+
"arch": [
120+
"universal"
121+
]
116122
}
117123
],
118124
"category": "public.app-category.developer-tools",

src/App.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { registerModule, unregisterModule } from '@/lib/telemetry'
33
import { usePDMStore } from './stores/pdmStore'
44
import { SettingsContent } from './components/SettingsContent'
55
import type { SettingsTab } from './types/settings'
6-
import { supabase, getCurrentSession, isSupabaseConfigured, getFilesLightweight, getCheckedOutUsers, linkUserToOrganization, getUserProfile, setCurrentAccessToken, registerDeviceSession, startSessionHeartbeat, stopSessionHeartbeat, signOut, syncUserSessionsOrgId, ensureUserOrgId } from './lib/supabase'
6+
import { supabase, getCurrentSession, isSupabaseConfigured, getFilesLightweight, getCheckedOutUsers, linkUserToOrganization, getUserProfile, setCurrentAccessToken, registerDeviceSession, startSessionHeartbeat, stopSessionHeartbeat, signOut, syncUserSessionsOrgId } from './lib/supabase'
77
import { subscribeToFiles, subscribeToActivity, subscribeToOrganization, unsubscribeAll } from './lib/realtime'
88
import { getBackupStatus, isThisDesignatedMachine, updateHeartbeat } from './lib/backup'
99
import { MenuBar } from './components/MenuBar'
@@ -317,12 +317,8 @@ function App() {
317317
setCurrentAccessToken(session.access_token)
318318

319319
try {
320-
// FIRST: Ensure user's org_id is correct in the database
321-
// This fixes issues where users have NULL or wrong org_id
322-
const orgIdResult = await ensureUserOrgId()
323-
if (orgIdResult.fixed) {
324-
console.log('[Auth] Fixed user org_id in database')
325-
}
320+
// NOTE: ensureUserOrgId() removed - it used client.rpc() which hangs
321+
// linkUserToOrganization() handles org_id setup correctly as fallback
326322

327323
// Fetch user profile from database to get role
328324
const { profile, error: profileError } = await getUserProfile(session.user.id)
@@ -385,20 +381,24 @@ function App() {
385381

386382
if ((event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') && session?.user) {
387383
// Show connecting state while loading organization
384+
// Add timeout to prevent infinite hanging if network/db is slow
385+
let connectingTimeout: ReturnType<typeof setTimeout> | null = null
388386
if (event === 'SIGNED_IN') {
389387
setIsConnecting(true)
388+
// Safety timeout: clear isConnecting after 30s to prevent infinite hang
389+
connectingTimeout = setTimeout(() => {
390+
console.warn('[Auth] Organization loading timeout - clearing connecting state')
391+
setIsConnecting(false)
392+
addToast('warning', 'Connection timed out. You may need to sign in again.')
393+
}, 30000)
390394
}
391395

392396
// Store access token for raw fetch calls (Supabase client methods hang)
393397
setCurrentAccessToken(session.access_token)
394398

395399
try {
396-
// FIRST: Ensure user's org_id is correct in the database
397-
// This fixes issues where users have NULL or wrong org_id
398-
const orgIdResult = await ensureUserOrgId()
399-
if (orgIdResult.fixed) {
400-
console.log('[Auth] Fixed user org_id in database during', event)
401-
}
400+
// NOTE: ensureUserOrgId() removed - it used client.rpc() which hangs
401+
// linkUserToOrganization() handles org_id setup correctly as fallback
402402

403403
// Fetch user profile from database to get role
404404
console.log('[Auth] Fetching user profile...')
@@ -434,6 +434,7 @@ function App() {
434434
window.electronAPI?.log?.('info', `[Auth] Organization loaded: ${(org as any).name}`)
435435
window.electronAPI?.log?.('info', `[Auth] Organization settings keys: ${Object.keys((org as any).settings || {}).join(', ')}`)
436436
window.electronAPI?.log?.('info', `[Auth] DM License key in settings: ${(org as any).settings?.solidworks_dm_license_key ? 'PRESENT (' + (org as any).settings.solidworks_dm_license_key.length + ' chars)' : 'NOT PRESENT'}`)
437+
if (connectingTimeout) clearTimeout(connectingTimeout)
437438
setOrganization(org as any)
438439

439440
// Update user's org_id in store if it wasn't set (triggers session re-registration with correct org_id)
@@ -448,10 +449,12 @@ function App() {
448449
syncUserSessionsOrgId(session.user.id, (org as any).id)
449450
} else {
450451
console.log('[Auth] No organization found:', orgError)
452+
if (connectingTimeout) clearTimeout(connectingTimeout)
451453
setIsConnecting(false)
452454
}
453455
} catch (err) {
454456
console.error('[Auth] Error in auth state handler:', err)
457+
if (connectingTimeout) clearTimeout(connectingTimeout)
455458
setIsConnecting(false)
456459
}
457460
} else if (event === 'SIGNED_OUT') {

src/components/OnlineUsersIndicator.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ export function OnlineUsersIndicator({ orgLogoUrl }: OnlineUsersIndicatorProps)
117117
<Users size={16} className="text-plm-fg-muted group-hover:text-plm-fg transition-colors" />
118118
)}
119119

120-
{/* Online count badge */}
120+
{/* Online count - visible but not like a notification */}
121121
{onlineUsers.length > 0 && (
122-
<div className="absolute -top-1 -right-1 min-w-[14px] h-[14px] flex items-center justify-center bg-plm-success rounded-full">
123-
<span className="text-[9px] font-bold text-white px-0.5">
122+
<div className="absolute -top-1 -right-1.5 min-w-[14px] h-[14px] flex items-center justify-center bg-plm-bg-lighter rounded-full border border-plm-border">
123+
<span className="text-[9px] font-semibold text-plm-fg-dim group-hover:text-plm-fg transition-colors px-0.5">
124124
{onlineUsers.length > 99 ? '99+' : onlineUsers.length}
125125
</span>
126126
</div>

src/components/WelcomeScreen.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,12 @@ export function WelcomeScreen({ onOpenRecentVault }: WelcomeScreenProps) {
588588
// CONNECTING SCREEN (shown after sign-in while loading organization)
589589
// ============================================
590590
if (isAuthConnecting) {
591+
const handleCancelConnecting = async () => {
592+
uiLog('info', 'User cancelled connecting - signing out')
593+
const { signOut: supabaseSignOut } = await import('../lib/supabase')
594+
await supabaseSignOut()
595+
}
596+
591597
return (
592598
<div className="flex-1 flex items-center justify-center bg-plm-bg overflow-auto">
593599
<div className="max-w-md w-full p-8 text-center">
@@ -620,6 +626,14 @@ export function WelcomeScreen({ onOpenRecentVault }: WelcomeScreenProps) {
620626

621627
<Loader2 size={40} className="animate-spin text-plm-accent mx-auto mb-4" />
622628
<p className="text-plm-fg-muted">{t('welcome.connectingToOrg')}</p>
629+
630+
{/* Cancel button - allows users to escape if connection hangs */}
631+
<button
632+
onClick={handleCancelConnecting}
633+
className="mt-6 text-sm text-plm-fg-muted hover:text-plm-fg transition-colors underline"
634+
>
635+
{t('common.cancel')}
636+
</button>
623637
</div>
624638
</div>
625639
)

src/lib/supabase.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3984,14 +3984,26 @@ export async function syncUserSessionsOrgId(userId: string, orgId: string): Prom
39843984
* Ensure the current user has the correct org_id in the database
39853985
* This calls a database RPC that checks and fixes org_id based on email domain
39863986
* Should be called on every app boot to prevent org_id mismatch issues
3987+
*
3988+
* NOTE: This uses Supabase client.rpc() which can sometimes hang. We add a timeout
3989+
* to prevent blocking the auth flow. If it times out, we just skip it - the
3990+
* linkUserToOrganization function will handle setting org_id as a fallback.
39873991
*/
39883992
export async function ensureUserOrgId(): Promise<{ success: boolean; fixed: boolean; org_id?: string; error?: string }> {
39893993
const client = getSupabaseClient()
39903994

39913995
console.log('[Auth] Ensuring user org_id is correct...')
39923996

3997+
// Wrap in a timeout since client.rpc() can hang
3998+
const timeoutMs = 5000 // 5 second timeout
3999+
39934000
try {
3994-
const { data, error } = await client.rpc('ensure_user_org_id' as never)
4001+
const rpcPromise = client.rpc('ensure_user_org_id' as never)
4002+
const timeoutPromise = new Promise<never>((_, reject) =>
4003+
setTimeout(() => reject(new Error('RPC timeout after 5s')), timeoutMs)
4004+
)
4005+
4006+
const { data, error } = await Promise.race([rpcPromise, timeoutPromise]) as { data: unknown; error: { message: string } | null }
39954007

39964008
if (error) {
39974009
// RPC might not exist yet (before migration runs)
@@ -4014,8 +4026,14 @@ export async function ensureUserOrgId(): Promise<{ success: boolean; fixed: bool
40144026
error: result.error
40154027
}
40164028
} catch (err) {
4017-
console.error('[Auth] ensureUserOrgId failed:', err)
4018-
return { success: false, fixed: false, error: String(err) }
4029+
// This catches both RPC errors and timeout
4030+
const errorMsg = String(err)
4031+
if (errorMsg.includes('timeout')) {
4032+
console.warn('[Auth] ensureUserOrgId timed out - skipping (not critical)')
4033+
} else {
4034+
console.error('[Auth] ensureUserOrgId failed:', err)
4035+
}
4036+
return { success: false, fixed: false, error: errorMsg }
40194037
}
40204038
}
40214039

0 commit comments

Comments
 (0)