Skip to content

Commit 9daf02a

Browse files
committed
fix(online-users): complete fix for RLS policy and org_id sync
1 parent 430f653 commit 9daf02a

File tree

9 files changed

+593
-23
lines changed

9 files changed

+593
-23
lines changed

CHANGELOG.md

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

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

5+
## [2.9.3] - 2025-12-17
6+
7+
### Fixed
8+
- **Online users visibility (complete fix)**: Fixed RLS policy that was preventing users from seeing other organization members online. The policy now properly handles NULL org_id comparisons. Added database index on `user_sessions.org_id` for better query performance. Improved session sync to update all sessions unconditionally.
9+
10+
---
11+
512
## [2.9.2] - 2025-12-17
613

714
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-plm",
3-
"version": "2.9.2",
3+
"version": "2.9.3",
44
"description": "Product Lifecycle Management for everyone who builds",
55
"main": "dist-electron/main.js",
66
"scripts": {

src/App.tsx

Lines changed: 21 additions & 3 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 } from './lib/supabase'
6+
import { supabase, getCurrentSession, isSupabaseConfigured, getFilesLightweight, getCheckedOutUsers, linkUserToOrganization, getUserProfile, setCurrentAccessToken, registerDeviceSession, startSessionHeartbeat, stopSessionHeartbeat, signOut, syncUserSessionsOrgId, ensureUserOrgId } 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,6 +317,13 @@ 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+
}
326+
320327
// Fetch user profile from database to get role
321328
const { profile, error: profileError } = await getUserProfile(session.user.id)
322329
if (profileError) {
@@ -386,6 +393,13 @@ function App() {
386393
setCurrentAccessToken(session.access_token)
387394

388395
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+
}
402+
389403
// Fetch user profile from database to get role
390404
console.log('[Auth] Fetching user profile...')
391405
const { profile, error: profileError } = await getUserProfile(session.user.id)
@@ -1965,11 +1979,15 @@ function App() {
19651979

19661980
// Register this device's session
19671981
// Use user.org_id first, fall back to organization.id if not set
1968-
const orgIdForSession = user.org_id || usePDMStore.getState().organization?.id || null
1982+
const orgIdForSession = user.org_id || organization?.id || null
1983+
console.log('[Session] Registering session with org_id:', orgIdForSession?.substring(0, 8) || 'NULL',
1984+
'(user.org_id:', user.org_id?.substring(0, 8) || 'NULL',
1985+
', organization?.id:', organization?.id?.substring(0, 8) || 'NULL', ')')
1986+
19691987
registerDeviceSession(user.id, orgIdForSession)
19701988
.then(result => {
19711989
if (result.success) {
1972-
console.log('[Session] Device session registered')
1990+
console.log('[Session] Device session registered successfully with org_id:', orgIdForSession?.substring(0, 8) || 'NULL')
19731991
// Start heartbeat to keep session alive
19741992
// Pass callbacks: one for remote sign out, one to get current org_id
19751993
startSessionHeartbeat(

src/lib/supabase.ts

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3957,24 +3957,65 @@ export async function registerDeviceSession(
39573957

39583958
/**
39593959
* Sync all sessions for a user to use the correct org_id
3960-
* Call this after org is loaded to fix sessions that were created with null org_id
3960+
* Call this after org is loaded to fix sessions that were created with null or wrong org_id
39613961
*/
39623962
export async function syncUserSessionsOrgId(userId: string, orgId: string): Promise<void> {
39633963
const client = getSupabaseClient()
39643964

39653965
console.log('[Session] Syncing all user sessions to org_id:', orgId?.substring(0, 8) + '...')
39663966

3967-
// Update all active sessions for this user to have the correct org_id
3968-
const { error } = await client
3967+
// Update ALL active sessions for this user to have the correct org_id
3968+
// Use unconditional update - simpler and ensures all sessions have correct org_id
3969+
// The update only affects this user's sessions, so it's safe
3970+
const { data, error } = await client
39693971
.from('user_sessions')
39703972
.update({ org_id: orgId })
39713973
.eq('user_id', userId)
3972-
.is('org_id', null) // Only update sessions with NULL org_id
3974+
.select('id')
39733975

39743976
if (error) {
39753977
console.error('[Session] Failed to sync session org_ids:', error.message)
39763978
} else {
3977-
console.log('[Session] Session org_ids synced successfully')
3979+
console.log('[Session] Session org_ids synced successfully, updated:', data?.length || 0, 'sessions')
3980+
}
3981+
}
3982+
3983+
/**
3984+
* Ensure the current user has the correct org_id in the database
3985+
* This calls a database RPC that checks and fixes org_id based on email domain
3986+
* Should be called on every app boot to prevent org_id mismatch issues
3987+
*/
3988+
export async function ensureUserOrgId(): Promise<{ success: boolean; fixed: boolean; org_id?: string; error?: string }> {
3989+
const client = getSupabaseClient()
3990+
3991+
console.log('[Auth] Ensuring user org_id is correct...')
3992+
3993+
try {
3994+
const { data, error } = await client.rpc('ensure_user_org_id' as never)
3995+
3996+
if (error) {
3997+
// RPC might not exist yet (before migration runs)
3998+
console.warn('[Auth] ensure_user_org_id RPC failed (run migration if not done):', error.message)
3999+
return { success: false, fixed: false, error: error.message }
4000+
}
4001+
4002+
const result = data as { success: boolean; fixed: boolean; org_id?: string; previous_org_id?: string; new_org_id?: string; error?: string }
4003+
4004+
if (result.fixed) {
4005+
console.log('[Auth] Fixed user org_id:', result.previous_org_id?.substring(0, 8) + '... ->', result.new_org_id?.substring(0, 8) + '...')
4006+
} else {
4007+
console.log('[Auth] User org_id is correct:', result.org_id?.substring(0, 8) + '...')
4008+
}
4009+
4010+
return {
4011+
success: result.success,
4012+
fixed: result.fixed,
4013+
org_id: result.new_org_id || result.org_id,
4014+
error: result.error
4015+
}
4016+
} catch (err) {
4017+
console.error('[Auth] ensureUserOrgId failed:', err)
4018+
return { success: false, fixed: false, error: String(err) }
39784019
}
39794020
}
39804021

@@ -4232,24 +4273,29 @@ export async function getOrgOnlineUsers(orgId: string): Promise<{ users: OnlineU
42324273
// Get sessions active within the last 5 minutes
42334274
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString()
42344275

4235-
console.log('[OnlineUsers] Fetching online users for org:', orgId, 'since:', fiveMinutesAgo)
4276+
console.log('[OnlineUsers] Fetching online users for org:', orgId?.substring(0, 8) + '...', 'since:', fiveMinutesAgo)
42364277

4237-
// First, let's debug by fetching ALL active sessions for this org (without RLS filtering)
4278+
// First, let's debug by fetching ALL active sessions visible to current user (RLS applies)
4279+
// This helps diagnose if the RLS policy is working correctly
42384280
const { data: debugData, error: debugError } = await client
42394281
.from('user_sessions')
42404282
.select('user_id, org_id, machine_name, is_active, last_seen')
42414283
.eq('is_active', true)
42424284
.gte('last_seen', fiveMinutesAgo)
42434285

4244-
console.log('[OnlineUsers] DEBUG - All active sessions visible to current user:',
4245-
debugData?.map(s => ({
4246-
user_id: s.user_id?.substring(0, 8),
4247-
org_id: s.org_id?.substring(0, 8) || 'NULL',
4248-
machine: s.machine_name,
4249-
last_seen: s.last_seen
4250-
})) || [],
4251-
'Error:', debugError?.message || 'none'
4252-
)
4286+
console.log('[OnlineUsers] DEBUG - All active sessions visible to current user (RLS filtered):',
4287+
debugData?.length || 0, 'sessions')
4288+
if (debugData && debugData.length > 0) {
4289+
debugData.forEach(s => {
4290+
console.log('[OnlineUsers] -', s.machine_name,
4291+
'| user:', s.user_id?.substring(0, 8) + '...',
4292+
'| org:', s.org_id?.substring(0, 8) || 'NULL',
4293+
'| last_seen:', new Date(s.last_seen).toLocaleTimeString())
4294+
})
4295+
}
4296+
if (debugError) {
4297+
console.error('[OnlineUsers] DEBUG query error:', debugError.message)
4298+
}
42534299

42544300
const { data, error } = await client
42554301
.from('user_sessions')
@@ -4310,6 +4356,8 @@ export function subscribeToOrgOnlineUsers(
43104356
): () => void {
43114357
const client = getSupabaseClient()
43124358

4359+
console.log('[OnlineUsers] Subscribing to realtime updates for org:', orgId?.substring(0, 8) + '...')
4360+
43134361
const channel = client
43144362
.channel(`org_sessions:${orgId}`)
43154363
.on<UserSession>(
@@ -4320,15 +4368,22 @@ export function subscribeToOrgOnlineUsers(
43204368
table: 'user_sessions',
43214369
filter: `org_id=eq.${orgId}`
43224370
},
4323-
async () => {
4371+
async (payload) => {
4372+
console.log('[OnlineUsers] Realtime event received:', payload.eventType,
4373+
'| user:', (payload.new as UserSession)?.user_id?.substring(0, 8) || (payload.old as UserSession)?.user_id?.substring(0, 8) || 'unknown')
4374+
43244375
// When any org session changes, fetch all online users
43254376
const { users } = await getOrgOnlineUsers(orgId)
4377+
console.log('[OnlineUsers] Refreshed online users after realtime event:', users.length)
43264378
onUsersChange(users)
43274379
}
43284380
)
4329-
.subscribe()
4381+
.subscribe((status) => {
4382+
console.log('[OnlineUsers] Subscription status:', status)
4383+
})
43304384

43314385
return () => {
4386+
console.log('[OnlineUsers] Unsubscribing from realtime updates for org:', orgId?.substring(0, 8) + '...')
43324387
channel.unsubscribe()
43334388
}
43344389
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
-- Quick diagnostic for Odoo config visibility issue
2+
-- Run this in Supabase SQL editor to identify the problem
3+
4+
-- 1. Show the Odoo config that exists but isn't loading
5+
SELECT
6+
osc.id,
7+
osc.name,
8+
osc.url,
9+
osc.org_id as config_org_id,
10+
o.name as config_org_name
11+
FROM odoo_saved_configs osc
12+
JOIN organizations o ON osc.org_id = o.id
13+
WHERE osc.name = 'BR Production Server' -- The config name from the error
14+
OR osc.name ILIKE '%production%'; -- Or similar
15+
16+
-- 2. Show all users and their org_ids
17+
SELECT
18+
email,
19+
role,
20+
org_id as user_org_id,
21+
(SELECT name FROM organizations WHERE id = users.org_id) as org_name
22+
FROM users
23+
ORDER BY email;
24+
25+
-- 3. Quick visibility check - do ANY users have matching org_id?
26+
SELECT
27+
osc.name as config_name,
28+
osc.org_id as config_org_id,
29+
COUNT(*) FILTER (WHERE u.org_id = osc.org_id) as users_who_can_see,
30+
COUNT(*) FILTER (WHERE u.org_id IS NULL) as users_with_null_org,
31+
COUNT(*) FILTER (WHERE u.org_id != osc.org_id AND u.org_id IS NOT NULL) as users_with_different_org
32+
FROM odoo_saved_configs osc
33+
CROSS JOIN users u
34+
WHERE osc.is_active = true
35+
GROUP BY osc.id, osc.name, osc.org_id;
36+
37+
-- 4. If the problem is NULL org_ids, fix them:
38+
-- (Run this to see what WOULD be fixed)
39+
SELECT
40+
u.email,
41+
u.org_id as current_org_id,
42+
o.id as should_be_org_id,
43+
o.name as org_name
44+
FROM users u
45+
JOIN organizations o ON SPLIT_PART(u.email, '@', 2) = ANY(o.email_domains)
46+
WHERE u.org_id IS NULL OR u.org_id != o.id;
47+
48+
-- 5. THE FIX: Update users to correct org_id based on email domain
49+
UPDATE users u
50+
SET org_id = o.id
51+
FROM organizations o
52+
WHERE SPLIT_PART(u.email, '@', 2) = ANY(o.email_domains)
53+
AND (u.org_id IS NULL OR u.org_id != o.id);
54+

0 commit comments

Comments
 (0)