Skip to content

Commit 6157ee2

Browse files
committed
feat: harden rbac workflows and user access management
1 parent d86a12b commit 6157ee2

11 files changed

Lines changed: 1199 additions & 424 deletions

File tree

src/components/AdminLayout.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Settings,
1616
ChevronDown,
1717
LogOut,
18+
UserCircle,
1819
Building2,
1920
Menu,
2021
X,
@@ -109,11 +110,11 @@ const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
109110
permission: 'view_dashboard' as const
110111
},
111112
{
112-
name: 'Users & Roles',
113+
name: 'Users & Access',
113114
href: '/agency/users',
114115
icon: Users,
115116
current: location.pathname.startsWith('/agency/users'),
116-
permission: 'invite_users' as const
117+
permission: 'view_dashboard' as const
117118
},
118119
{
119120
name: 'Agency Settings',
@@ -288,6 +289,16 @@ const AdminLayout: React.FC<AdminLayoutProps> = ({ children }) => {
288289
</div>
289290
)}
290291
</div>
292+
<button
293+
onClick={() => {
294+
setShowUserDropdown(false)
295+
navigate('/agency/users')
296+
}}
297+
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
298+
>
299+
<UserCircle className="w-4 h-4 mr-2" />
300+
My Profile & Access
301+
</button>
291302
<button
292303
onClick={handleSignOut}
293304
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"

src/hooks/useAgencyUsers.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,27 @@ export const useAgencyUsers = () => {
189189
}
190190
}
191191

192+
const transferOwnership = async (newOwnerUserId: string): Promise<void> => {
193+
if (!currentAgency) throw new Error('No agency selected')
194+
195+
try {
196+
const { error } = await supabase.rpc('transfer_agency_ownership', {
197+
p_agency_id: currentAgency.id,
198+
p_new_owner_id: newOwnerUserId
199+
})
200+
201+
if (error) {
202+
console.error('Error transferring ownership:', error)
203+
throw new Error(error.message)
204+
}
205+
206+
await fetchUsers()
207+
} catch (err) {
208+
console.error('Error in transferOwnership:', err)
209+
throw err
210+
}
211+
}
212+
192213
const changeUserStatus = async (memberId: string, status: 'active' | 'deactivated'): Promise<void> => {
193214
try {
194215
const { data, error } = await supabase.rpc('change_user_status', {
@@ -267,6 +288,7 @@ export const useAgencyUsers = () => {
267288
inviteUser,
268289
changeUserRole,
269290
changeUserStatus,
291+
transferOwnership,
270292
resendInvitation,
271293
revokeInvitation,
272294
refresh: fetchUsers

src/hooks/usePermissions.ts

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState } from 'react'
1+
import { useCallback, useEffect, useMemo, useState } from 'react'
22
import { useAuth } from '../contexts/AuthContext'
33
import {
44
AgencyRole,
@@ -16,65 +16,65 @@ export const usePermissions = (agencyId?: string) => {
1616
const [memberships, setMemberships] = useState<AgencyMembership[]>([])
1717
const [loading, setLoading] = useState(true)
1818

19-
useEffect(() => {
20-
const fetchMemberships = async () => {
21-
if (!user) {
19+
const fetchMemberships = useCallback(async () => {
20+
if (!user) {
21+
setMemberships([])
22+
setLoading(false)
23+
return
24+
}
25+
26+
setLoading(true)
27+
28+
try {
29+
const { data, error } = await supabase
30+
.from('agency_members')
31+
.select(`
32+
agency_id,
33+
role,
34+
joined_at,
35+
agencies!inner (
36+
name
37+
)
38+
`)
39+
.eq('user_id', user.id)
40+
.eq('status', 'active')
41+
.is('deleted_at', null)
42+
43+
if (error) {
44+
console.error('Error fetching memberships:', error)
2245
setMemberships([])
23-
setLoading(false)
2446
return
2547
}
2648

27-
setLoading(true)
28-
29-
try {
30-
const { data, error } = await supabase
31-
.from('agency_members')
32-
.select(`
33-
agency_id,
34-
role,
35-
joined_at,
36-
agencies!inner (
37-
name
38-
)
39-
`)
40-
.eq('user_id', user.id)
41-
.eq('status', 'active')
42-
.is('deleted_at', null)
43-
44-
if (error) {
45-
console.error('Error fetching memberships:', error)
46-
setMemberships([])
47-
return
48-
}
49-
50-
const mapped: AgencyMembership[] = (data || []).map((row: any) => ({
51-
agency_id: row.agency_id,
52-
agency_name: Array.isArray(row.agencies) ? row.agencies[0]?.name || 'Unknown Agency' : row.agencies?.name || 'Unknown Agency',
53-
role: row.role as AgencyRole,
54-
joined_at: row.joined_at || profile?.created_at || new Date().toISOString(),
55-
}))
56-
57-
if (mapped.length === 0 && profile?.role === 'agency' && profile?.agency_name) {
58-
setMemberships([{
59-
agency_id: user.id,
60-
agency_name: profile.agency_name,
61-
role: 'admin',
62-
joined_at: profile.created_at,
63-
}])
64-
return
65-
}
66-
67-
setMemberships(mapped)
68-
} catch (err) {
69-
console.error('Unexpected membership lookup error:', err)
70-
setMemberships([])
71-
} finally {
72-
setLoading(false)
49+
const mapped: AgencyMembership[] = (data || []).map((row: any) => ({
50+
agency_id: row.agency_id,
51+
agency_name: Array.isArray(row.agencies) ? row.agencies[0]?.name || 'Unknown Agency' : row.agencies?.name || 'Unknown Agency',
52+
role: row.role as AgencyRole,
53+
joined_at: row.joined_at || profile?.created_at || new Date().toISOString(),
54+
}))
55+
56+
if (mapped.length === 0 && profile?.role === 'agency' && profile?.agency_name) {
57+
setMemberships([{
58+
agency_id: user.id,
59+
agency_name: profile.agency_name,
60+
role: 'admin',
61+
joined_at: profile.created_at,
62+
}])
63+
return
7364
}
65+
66+
setMemberships(mapped)
67+
} catch (err) {
68+
console.error('Unexpected membership lookup error:', err)
69+
setMemberships([])
70+
} finally {
71+
setLoading(false)
7472
}
73+
}, [user, profile])
7574

75+
useEffect(() => {
7676
fetchMemberships()
77-
}, [user, profile])
77+
}, [fetchMemberships])
7878

7979
const currentMembership = useMemo(() => {
8080
if (!profile || !agencyId) return null
@@ -112,6 +112,7 @@ export const usePermissions = (agencyId?: string) => {
112112
hasPermission: checkPermission,
113113
canManageRole: checkCanManageRole,
114114
getPermissionTooltip: getTooltip,
115+
refresh: fetchMemberships,
115116
isLoggedIn: !!profile
116117
}
117118
}

src/hooks/usePlatformRoles.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { useState, useEffect } from 'react'
22
import { supabase } from '../lib/supabase'
33
import { useAuth } from '../contexts/AuthContext'
4-
import { PlatformRole, PlatformRoleInfo, PlatformUser, CreateAgencyRequest, InviteUserRequest } from '../types/platform'
4+
import {
5+
PlatformRole,
6+
PlatformUser,
7+
PlatformAgency,
8+
CreateAgencyRequest,
9+
InviteUserRequest,
10+
} from '../types/platform'
511

612
const asObject = <T,>(value: T | T[] | null | undefined): T | null => {
713
if (!value) return null
@@ -12,6 +18,8 @@ export const usePlatformRoles = () => {
1218
const { user } = useAuth()
1319
const [platformRole, setPlatformRole] = useState<PlatformRole | null>(null)
1420
const [platformUsers, setPlatformUsers] = useState<PlatformUser[]>([])
21+
const [agencies, setAgencies] = useState<PlatformAgency[]>([])
22+
const [agenciesLoading, setAgenciesLoading] = useState(true)
1523
const [loading, setLoading] = useState(true)
1624
const [error, setError] = useState<string | null>(null)
1725

@@ -85,6 +93,32 @@ export const usePlatformRoles = () => {
8593
}
8694
}
8795

96+
const fetchAgencies = async () => {
97+
if (!platformRole) return
98+
99+
setAgenciesLoading(true)
100+
try {
101+
const { data, error: fetchError } = await supabase
102+
.from('agencies')
103+
.select('id, name, jurisdiction, jurisdiction_type')
104+
.is('deleted_at', null)
105+
.order('name', { ascending: true })
106+
107+
if (fetchError) {
108+
console.error('Error fetching agencies:', fetchError)
109+
setError('Failed to load agencies')
110+
return
111+
}
112+
113+
setAgencies((data || []) as PlatformAgency[])
114+
} catch (err) {
115+
console.error('Error in fetchAgencies:', err)
116+
setError('Failed to load agencies')
117+
} finally {
118+
setAgenciesLoading(false)
119+
}
120+
}
121+
88122
const createAgency = async (request: CreateAgencyRequest): Promise<string> => {
89123
try {
90124
const { data, error } = await supabase.rpc('create_agency_with_owner', {
@@ -212,15 +246,23 @@ export const usePlatformRoles = () => {
212246
}
213247
}, [platformRole])
214248

249+
useEffect(() => {
250+
if (platformRole) {
251+
fetchAgencies()
252+
}
253+
}, [platformRole])
254+
215255
return {
216256
platformRole,
217257
platformUsers,
258+
agencies,
259+
agenciesLoading,
218260
loading,
219261
error,
220262
createAgency,
221263
inviteUserToAgency,
222264
inviteSuperUser,
223265
removeSuperUser,
224-
refresh: () => Promise.all([fetchPlatformRole(), fetchPlatformUsers()])
266+
refresh: () => Promise.all([fetchPlatformRole(), fetchPlatformUsers(), fetchAgencies()])
225267
}
226268
}

0 commit comments

Comments
 (0)