Skip to content

Commit da5b269

Browse files
committed
v0.9.0: Fix OAuth authentication in packaged Electron app
1 parent ef6a2d4 commit da5b269

19 files changed

+235
-88
lines changed

electron/main.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,29 @@ function createWindow() {
180180
mainWindow.loadFile(loadPath).catch(err => log('Error loading file:', err))
181181
}
182182

183+
// In production, intercept OAuth redirects to localhost and reload the app with auth tokens
184+
if (!isDev) {
185+
mainWindow.webContents.on('will-navigate', (event, navUrl) => {
186+
if (navUrl.startsWith('http://localhost') && navUrl.includes('access_token')) {
187+
log('Intercepting OAuth redirect in main window:', navUrl.substring(0, 80) + '...')
188+
event.preventDefault()
189+
190+
// Extract hash/query from the redirect URL
191+
const url = new URL(navUrl)
192+
const hashFragment = url.hash || ''
193+
const queryString = url.search || ''
194+
195+
// Load the production HTML file with the auth tokens in hash
196+
const prodPath = path.join(__dirname, '../dist/index.html')
197+
const normalizedPath = prodPath.replace(/\\/g, '/')
198+
const fileUrl = `file:///${normalizedPath}${queryString}${hashFragment}`
199+
log('Reloading with file URL:', fileUrl.substring(0, 100) + '...')
200+
201+
mainWindow?.loadURL(fileUrl)
202+
}
203+
})
204+
}
205+
183206
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
184207
shell.openExternal(url)
185208
return { action: 'deny' }
@@ -339,13 +362,52 @@ ipcMain.handle('auth:open-oauth-window', async (_, url: string) => {
339362
})
340363

341364
// Listen for redirect back to our app with auth tokens
365+
let handled = false
342366
const handleAuthRedirect = (redirectUrl: string) => {
367+
// Prevent duplicate handling
368+
if (handled) return false
369+
343370
// Check if this is our callback URL (contains access_token or code)
344371
if (redirectUrl.startsWith('http://localhost') &&
345372
(redirectUrl.includes('access_token') || redirectUrl.includes('code=') || redirectUrl.includes('#'))) {
373+
handled = true
346374
log('OAuth redirect detected:', redirectUrl.substring(0, 100) + '...')
347-
// Load the callback URL in main window so Supabase can process the tokens
348-
mainWindow?.loadURL(redirectUrl)
375+
376+
// In production, we can't load localhost - extract tokens and send to renderer
377+
if (isDev) {
378+
// In dev, localhost server is running, so load the URL directly
379+
mainWindow?.loadURL(redirectUrl)
380+
} else {
381+
// In production, extract tokens from the redirect URL and send to renderer
382+
const url = new URL(redirectUrl)
383+
const hashFragment = url.hash || ''
384+
385+
// Parse the hash fragment to extract tokens
386+
const hashParams = new URLSearchParams(hashFragment.substring(1))
387+
const accessToken = hashParams.get('access_token')
388+
const refreshToken = hashParams.get('refresh_token')
389+
const expiresIn = hashParams.get('expires_in')
390+
const expiresAt = hashParams.get('expires_at')
391+
392+
if (accessToken && refreshToken) {
393+
log('Extracted tokens, sending to renderer...')
394+
// Send tokens to renderer to set session
395+
mainWindow?.webContents.send('auth:set-session', {
396+
access_token: accessToken,
397+
refresh_token: refreshToken,
398+
expires_in: expiresIn ? parseInt(expiresIn) : 3600,
399+
expires_at: expiresAt ? parseInt(expiresAt) : undefined
400+
})
401+
} else {
402+
log('No tokens found in redirect, loading file with hash...')
403+
// Fallback: try loading file with hash
404+
const prodPath = path.join(__dirname, '../dist/index.html')
405+
const normalizedPath = prodPath.replace(/\\/g, '/')
406+
const fileUrl = `file:///${normalizedPath}${hashFragment}`
407+
mainWindow?.loadURL(fileUrl)
408+
}
409+
}
410+
349411
authWindow.close()
350412
resolve({ success: true })
351413
return true

electron/preload.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
159159
return () => {
160160
ipcRenderer.removeListener('files-changed', handler)
161161
}
162+
},
163+
164+
// Auth session listener (for OAuth callback in production)
165+
onSetSession: (callback: (tokens: { access_token: string; refresh_token: string; expires_in?: number; expires_at?: number }) => void) => {
166+
const handler = (_: unknown, tokens: { access_token: string; refresh_token: string; expires_in?: number; expires_at?: number }) => callback(tokens)
167+
ipcRenderer.on('auth:set-session', handler)
168+
169+
return () => {
170+
ipcRenderer.removeListener('auth:set-session', handler)
171+
}
162172
}
163173
})
164174

@@ -229,6 +239,9 @@ declare global {
229239

230240
// File change events
231241
onFilesChanged: (callback: (files: string[]) => void) => () => void
242+
243+
// Auth session events (for OAuth callback in production)
244+
onSetSession: (callback: (tokens: { access_token: string; refresh_token: string; expires_in?: number; expires_at?: number }) => void) => () => void
232245
}
233246
}
234247
}

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: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-pdm",
3-
"version": "0.8.0",
3+
"version": "0.9.0",
44
"description": "Product Data Management for engineering teams",
55
"main": "dist-electron/main.js",
66
"scripts": {
@@ -81,4 +81,3 @@
8181
}
8282
}
8383
}
84-

src/App.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ function App() {
3333
setVaultConnected,
3434
setFiles,
3535
setServerFiles,
36-
isLoading,
3736
setIsLoading,
3837
statusMessage,
3938
setStatusMessage,
@@ -84,8 +83,8 @@ function App() {
8483
console.log('[Auth] Loading organization for:', session.user.email)
8584
linkUserToOrganization(session.user.id, session.user.email || '').then(({ org, error }) => {
8685
if (org) {
87-
console.log('[Auth] Organization loaded:', org.name)
88-
setOrganization(org)
86+
console.log('[Auth] Organization loaded:', (org as any).name)
87+
setOrganization(org as any)
8988
} else if (error) {
9089
console.log('[Auth] No organization found:', error)
9190
}
@@ -123,8 +122,8 @@ function App() {
123122
// Load organization
124123
linkUserToOrganization(session.user.id, session.user.email || '').then(({ org }) => {
125124
if (org) {
126-
console.log('[Auth] Organization loaded on state change:', org.name)
127-
setOrganization(org)
125+
console.log('[Auth] Organization loaded on state change:', (org as any).name)
126+
setOrganization(org as any)
128127
}
129128
})
130129
} else if (event === 'SIGNED_OUT') {
@@ -233,13 +232,13 @@ function App() {
233232
// These appear faded/greyed to indicate they're available but not downloaded
234233
const cloudFolders = new Set<string>()
235234

236-
for (const pdmFile of pdmFiles) {
235+
for (const pdmFile of pdmFiles as any[]) {
237236
if (!localPathSet.has(pdmFile.file_path)) {
238237
// If file is checked out by current user but doesn't exist locally,
239238
// auto-release the checkout (user deleted it externally)
240239
if (pdmFile.checked_out_by === user?.id) {
241240
console.log('[Auto-release] File deleted externally, releasing checkout:', pdmFile.file_name)
242-
checkinFile(pdmFile.id, user.id).then(result => {
241+
checkinFile(pdmFile.id, user!.id).then(result => {
243242
if (result.success) {
244243
console.log('[Auto-release] Released checkout for:', pdmFile.file_name)
245244
} else {
@@ -305,7 +304,6 @@ function App() {
305304
// A folder should be 'cloud' if all its contents are cloud-only
306305
// Process folders bottom-up (deepest first) so parent folders see updated child statuses
307306
const folders = localFiles.filter(f => f.isDirectory)
308-
const fileMap = new Map(localFiles.map(f => [f.relativePath.replace(/\\/g, '/'), f]))
309307

310308
// Sort folders by depth (deepest first)
311309
folders.sort((a, b) => {

src/components/ActivityBar.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import {
22
FolderTree,
33
ArrowDownUp,
44
History,
5-
Search,
6-
Settings
5+
Search
76
} from 'lucide-react'
87
import { usePDMStore, SidebarView } from '../stores/pdmStore'
98

src/components/DetailsPanel.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react'
22
import { usePDMStore } from '../stores/pdmStore'
3-
import { formatFileSize, STATE_INFO, getFileIconType } from '../types/pdm'
3+
import { formatFileSize, getFileIconType, STATE_INFO } from '../types/pdm'
44
import { format, formatDistanceToNow } from 'date-fns'
55
import { getFileVersions, getRecentActivity } from '../lib/supabase'
66
import { rollbackToVersion } from '../lib/fileService'
@@ -16,7 +16,6 @@ import {
1616
Info,
1717
Cloud,
1818
RotateCcw,
19-
Check,
2019
Loader2,
2120
FileImage,
2221
FileSpreadsheet,

src/components/FileBrowser.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
FileText,
1212
Layers,
1313
Lock,
14-
MoreVertical,
1514
RefreshCw,
1615
Upload,
1716
Home,
@@ -61,7 +60,6 @@ export function FileBrowser({ onRefresh }: FileBrowserProps) {
6160
selectedFiles,
6261
setSelectedFiles,
6362
toggleFileSelection,
64-
selectAllFiles,
6563
clearSelection,
6664
columns,
6765
setColumnWidth,
@@ -100,12 +98,8 @@ export function FileBrowser({ onRefresh }: FileBrowserProps) {
10098
processingFolders,
10199
addProcessingFolder,
102100
removeProcessingFolder,
103-
clearProcessingFolders,
104101
queueOperation,
105102
hasPathConflict,
106-
operationQueue,
107-
setHistoryFolderFilter,
108-
setActiveView,
109103
setDetailsPanelTab,
110104
detailsPanelVisible,
111105
toggleDetailsPanel,
@@ -925,7 +919,8 @@ export function FileBrowser({ onRefresh }: FileBrowserProps) {
925919
}
926920

927921
// Delete a file or folder (moves to trash/recycle bin)
928-
const handleDelete = async (file: LocalFile) => {
922+
// @ts-ignore - Reserved for future use
923+
const _handleDelete = async (file: LocalFile) => {
929924
if (!vaultPath || !window.electronAPI) {
930925
addToast('error', 'No vault connected')
931926
return
@@ -1855,8 +1850,6 @@ export function FileBrowser({ onRefresh }: FileBrowserProps) {
18551850
setSelectionBox(prev => prev ? { ...prev, currentX, currentY } : null)
18561851

18571852
// Calculate selection box bounds
1858-
const left = Math.min(selectionBox.startX, currentX)
1859-
const right = Math.max(selectionBox.startX, currentX)
18601853
const top = Math.min(selectionBox.startY, currentY)
18611854
const bottom = Math.max(selectionBox.startY, currentY)
18621855

@@ -2109,8 +2102,6 @@ export function FileBrowser({ onRefresh }: FileBrowserProps) {
21092102
// Check out/in status - consider all synced files including those inside folders
21102103
const allCheckedOut = syncedFilesInSelection.length > 0 && syncedFilesInSelection.every(f => f.pdmData?.checked_out_by)
21112104
const allCheckedIn = syncedFilesInSelection.length > 0 && syncedFilesInSelection.every(f => !f.pdmData?.checked_out_by)
2112-
const anyCheckedOut = syncedFilesInSelection.some(f => f.pdmData?.checked_out_by)
2113-
const anyCheckedIn = syncedFilesInSelection.some(f => !f.pdmData?.checked_out_by)
21142105

21152106
// Count files that can be checked out/in (for folder labels)
21162107
const checkoutableCount = syncedFilesInSelection.filter(f => !f.pdmData?.checked_out_by).length
@@ -2143,9 +2134,6 @@ export function FileBrowser({ onRefresh }: FileBrowserProps) {
21432134
const cloudOnlyCount = getCloudOnlyFilesCount()
21442135
const anyCloudOnly = cloudOnlyCount > 0 || contextFiles.some(f => f.diffStatus === 'cloud')
21452136

2146-
// Check for locally synced files (can be unsynced)
2147-
const anyLocalSynced = contextFiles.some(f => f.pdmData && f.diffStatus !== 'cloud' && f.diffStatus !== 'added')
2148-
21492137
return (
21502138
<>
21512139
<div

src/components/FileContextMenu.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
ArrowDown,
99
ArrowUp,
1010
Cloud,
11-
Download,
1211
CloudOff,
1312
Edit,
1413
FolderPlus,
@@ -35,7 +34,6 @@ interface FileContextMenuProps {
3534
onPaste?: () => void
3635
onRename?: (file: LocalFile) => void
3736
onNewFolder?: () => void
38-
onDelete?: (file: LocalFile) => void
3937
}
4038

4139
export function FileContextMenu({
@@ -50,8 +48,7 @@ export function FileContextMenu({
5048
onCut,
5149
onPaste,
5250
onRename,
53-
onNewFolder,
54-
onDelete
51+
onNewFolder
5552
}: FileContextMenuProps) {
5653
const { user, organization, vaultPath, activeVaultId, addToast, addProgressToast, updateProgressToast, removeToast, isProgressToastCancelled, pinnedFolders, pinFolder, unpinFolder, connectedVaults, addProcessingFolder, removeProcessingFolder, queueOperation, hasPathConflict, updateFileInStore, startSync, updateSyncProgress, endSync } = usePDMStore()
5754

@@ -69,7 +66,6 @@ export function FileContextMenu({
6966
const firstFile = contextFiles[0]
7067
const isFolder = firstFile.isDirectory
7168
const allFolders = contextFiles.every(f => f.isDirectory)
72-
const allFiles = contextFiles.every(f => !f.isDirectory)
7369
const fileCount = contextFiles.filter(f => !f.isDirectory).length
7470
const folderCount = contextFiles.filter(f => f.isDirectory).length
7571

@@ -172,7 +168,6 @@ export function FileContextMenu({
172168

173169
// Check for cloud-only files
174170
const allCloudOnly = contextFiles.every(f => f.diffStatus === 'cloud')
175-
const hasLocalFiles = contextFiles.some(f => f.diffStatus !== 'cloud')
176171
const hasUnsyncedLocalFiles = unsyncedFilesInSelection.length > 0
177172

178173
// Count cloud-only files (for download count) - includes files inside folders

src/components/MenuBar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { signInWithGoogle, signOut, isSupabaseConfigured, linkUserToOrganization
55
import { SettingsModal } from './SettingsModal'
66

77
interface MenuBarProps {
8-
onOpenVault: () => void
9-
onRefresh: () => void
8+
onOpenVault?: () => void
9+
onRefresh?: () => void
1010
minimal?: boolean // Hide Sign In and Settings on welcome/signin screens
1111
}
1212

13-
export function MenuBar({ onOpenVault, onRefresh, minimal = false }: MenuBarProps) {
13+
export function MenuBar({ minimal = false }: MenuBarProps) {
1414
const { user, organization, setUser, setOrganization, addToast, setSearchQuery, searchQuery, searchType, setSearchType } = usePDMStore()
1515
const [appVersion, setAppVersion] = useState('')
1616
const [isSigningIn, setIsSigningIn] = useState(false)
@@ -275,8 +275,8 @@ export function MenuBar({ onOpenVault, onRefresh, minimal = false }: MenuBarProp
275275
if (error) {
276276
addToast('error', `Could not find org for @${user.email.split('@')[1]}`)
277277
} else if (org) {
278-
setOrganization(org)
279-
addToast('success', `Linked to ${org.name}`)
278+
setOrganization(org as any)
279+
addToast('success', `Linked to ${(org as any).name}`)
280280
setShowUserMenu(false)
281281
}
282282
}}

0 commit comments

Comments
 (0)