Skip to content

Commit 5016b0c

Browse files
authored
Merge pull request #354 from codeunia-dev/fix/rbac
fix(rbac): Improve member removal permissions & enhance hackathon retrieval logic
2 parents f797ec9 + fa1c0bc commit 5016b0c

6 files changed

Lines changed: 139 additions & 63 deletions

File tree

app/api/companies/[slug]/members/[userId]/route.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,15 @@ export async function PUT(
122122
.eq('id', user.id)
123123
.single()
124124

125-
const changedByName = requestingUserProfile?.first_name
125+
const changedByName = requestingUserProfile?.first_name
126126
? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim()
127127
: 'a team administrator'
128128

129129
// Send role change notification email
130130
if (memberProfile?.email && oldRole !== role) {
131131
const memberName = memberProfile.first_name || memberProfile.email.split('@')[0]
132132
const dashboardUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'https://codeunia.com'}/dashboard/company/${company.slug}`
133-
133+
134134
const emailContent = getRoleChangeEmail({
135135
memberName,
136136
companyName: company.name,
@@ -226,14 +226,14 @@ export async function DELETE(
226226
)
227227
}
228228

229-
// Check if user is owner (only owners can remove members)
229+
// Check if user is owner or admin (both can remove members)
230230
const requestingMember = await companyMemberService.checkMembership(user.id, company.id)
231231

232-
if (!requestingMember || requestingMember.role !== 'owner') {
232+
if (!requestingMember || !['owner', 'admin'].includes(requestingMember.role)) {
233233
return NextResponse.json(
234234
{
235235
success: false,
236-
error: 'Insufficient permissions: Owner role required to remove members',
236+
error: 'Insufficient permissions: Owner or Admin role required to remove members',
237237
},
238238
{ status: 403 }
239239
)
@@ -252,6 +252,17 @@ export async function DELETE(
252252
)
253253
}
254254

255+
// Prevent admins from removing owners (only owners can remove owners)
256+
if (requestingMember.role === 'admin' && targetMember.role === 'owner') {
257+
return NextResponse.json(
258+
{
259+
success: false,
260+
error: 'Insufficient permissions: Only owners can remove other owners',
261+
},
262+
{ status: 403 }
263+
)
264+
}
265+
255266
// Prevent owner from removing themselves if they're the last owner
256267
if (userId === user.id && targetMember.role === 'owner') {
257268
return NextResponse.json(
@@ -277,7 +288,7 @@ export async function DELETE(
277288
.eq('id', user.id)
278289
.single()
279290

280-
const removedByName = requestingUserProfile?.first_name
291+
const removedByName = requestingUserProfile?.first_name
281292
? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim()
282293
: 'a team administrator'
283294

@@ -287,7 +298,7 @@ export async function DELETE(
287298
// Send removal notification email
288299
if (memberProfile?.email) {
289300
const memberName = memberProfile.first_name || memberProfile.email.split('@')[0]
290-
301+
291302
const emailContent = getMemberRemovedEmail({
292303
memberName,
293304
companyName: company.name,

app/api/hackathons/[id]/route.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ export async function GET(request: NextRequest, { params }: RouteContext) {
1616
try {
1717
const { id } = await params
1818
const hackathon = await hackathonsService.getHackathonBySlug(id)
19-
19+
2020
if (!hackathon) {
2121
return NextResponse.json(
2222
{ error: 'Hackathon not found' },
2323
{ status: 404 }
2424
)
2525
}
26-
26+
2727
return NextResponse.json(hackathon)
2828
} catch (error) {
2929
console.error('Error in GET /api/hackathons/[id]:', error)
@@ -39,7 +39,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
3939
try {
4040
const { id } = await params
4141
const hackathonData = await request.json()
42-
42+
4343
// Check authentication
4444
const supabase = await createClient()
4545
const { data: { user } } = await supabase.auth.getUser()
@@ -52,8 +52,8 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
5252
}
5353

5454
// Get the existing hackathon to check company_id
55-
const existingHackathon = await hackathonsService.getHackathonBySlug(id)
56-
55+
const existingHackathon = await hackathonsService.getHackathonByIdOrSlug(id)
56+
5757
if (!existingHackathon) {
5858
return NextResponse.json(
5959
{ error: 'Hackathon not found' },
@@ -69,7 +69,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
6969
.select('is_admin')
7070
.eq('id', user.id)
7171
.single()
72-
72+
7373
if (profile?.is_admin) {
7474
isAuthorized = true
7575
}
@@ -83,7 +83,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
8383
.eq('user_id', user.id)
8484
.eq('status', 'active')
8585
.single()
86-
86+
8787
if (membership) {
8888
isAuthorized = true
8989
}
@@ -95,9 +95,9 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
9595
{ status: 401 }
9696
)
9797
}
98-
98+
9999
const hackathon = await hackathonsService.updateHackathon(id, hackathonData, user.id)
100-
100+
101101
return NextResponse.json({ hackathon })
102102
} catch (error) {
103103
console.error('Error in PUT /api/hackathons/[id]:', error)
@@ -112,9 +112,9 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
112112
export async function DELETE(_request: NextRequest, { params }: RouteContext) {
113113
try {
114114
const { id } = await params
115-
115+
116116
console.log('🗑️ DELETE request for hackathon:', id)
117-
117+
118118
// Check authentication
119119
const supabase = await createClient()
120120
const { data: { user }, error: authError } = await supabase.auth.getUser()
@@ -130,8 +130,8 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) {
130130
console.log('✅ User authenticated:', user.id)
131131

132132
// Get the existing hackathon to check company_id
133-
const existingHackathon = await hackathonsService.getHackathonBySlug(id)
134-
133+
const existingHackathon = await hackathonsService.getHackathonByIdOrSlug(id)
134+
135135
if (!existingHackathon) {
136136
console.error('❌ Hackathon not found:', id)
137137
return NextResponse.json(
@@ -150,13 +150,13 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) {
150150
.select('is_admin')
151151
.eq('id', user.id)
152152
.single()
153-
153+
154154
if (profile?.is_admin) {
155155
isAuthorized = true
156156
console.log('✅ User is admin')
157157
}
158158

159-
// If not admin, check if user is a member of the company
159+
// If not admin, check if user is a company owner or admin (not editor/viewer)
160160
if (!isAuthorized && existingHackathon.company_id) {
161161
const { data: membership } = await supabase
162162
.from('company_members')
@@ -165,28 +165,30 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) {
165165
.eq('user_id', user.id)
166166
.eq('status', 'active')
167167
.single()
168-
169-
if (membership) {
168+
169+
if (membership && ['owner', 'admin'].includes(membership.role)) {
170170
isAuthorized = true
171-
console.log('✅ User is company member with role:', membership.role)
171+
console.log('✅ User is company owner/admin with role:', membership.role)
172+
} else if (membership) {
173+
console.log('❌ User has insufficient role:', membership.role)
172174
}
173175
}
174176

175177
if (!isAuthorized) {
176178
console.error('❌ User not authorized to delete hackathon')
177179
return NextResponse.json(
178-
{ error: 'Unauthorized: You must be a company member or admin to delete this hackathon' },
180+
{ error: 'Insufficient permissions: Owner or Admin role required to delete hackathons' },
179181
{ status: 403 }
180182
)
181183
}
182-
184+
183185
console.log('🗑️ Attempting to delete hackathon...')
184186
await hackathonsService.deleteHackathon(id)
185187
console.log('✅ Hackathon deleted successfully')
186-
187-
return NextResponse.json({
188+
189+
return NextResponse.json({
188190
success: true,
189-
message: 'Hackathon deleted successfully'
191+
message: 'Hackathon deleted successfully'
190192
})
191193
} catch (error) {
192194
console.error('❌ Error in DELETE /api/hackathons/[id]:', error)

app/dashboard/company/layout.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React, { useState, useEffect } from 'react'
44
import Link from 'next/link'
5-
import { useParams } from 'next/navigation'
5+
import { useParams, usePathname } from 'next/navigation'
66
import { Button } from '@/components/ui/button'
77
import { CompanySidebar } from '@/components/dashboard/CompanySidebar'
88
import { CompanyHeader } from '@/components/dashboard/CompanyHeader'
@@ -35,10 +35,20 @@ export default function CompanyDashboardLayout({
3535
children: React.ReactNode
3636
}) {
3737
const { user, loading, error } = useAuth()
38-
const { isChecking, isAuthorized } = useRoleProtection('company_member')
3938
const params = useParams()
39+
const pathname = usePathname()
4040
const companySlug = params?.slug as string | undefined
4141

42+
// Check if this is the accept-invitation page
43+
// Users with pending invitations should be able to access this page
44+
const isAcceptInvitationPage = pathname?.includes('/accept-invitation') ?? false
45+
46+
// Only apply role protection if NOT on the accept-invitation page
47+
// Skip redirects on accept-invitation page to allow pending users to access it
48+
const { isChecking, isAuthorized } = useRoleProtection('company_member', {
49+
skipRedirect: isAcceptInvitationPage
50+
})
51+
4252
// Prevent hydration mismatch by using a consistent initial state
4353
const [mounted, setMounted] = useState(false)
4454

@@ -54,7 +64,7 @@ export default function CompanyDashboardLayout({
5464
)
5565
}
5666

57-
if (loading || isChecking) {
67+
if (loading || (!isAcceptInvitationPage && isChecking)) {
5868
return (
5969
<div className="flex items-center justify-center min-h-screen bg-black">
6070
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
@@ -78,7 +88,8 @@ export default function CompanyDashboardLayout({
7888
)
7989
}
8090

81-
if (!user || !isAuthorized) {
91+
// Skip authorization check for accept-invitation page
92+
if (!user || (!isAcceptInvitationPage && !isAuthorized)) {
8293
return (
8394
<div className="flex items-center justify-center min-h-screen px-4 bg-black">
8495
<div className="text-center max-w-md">
@@ -130,23 +141,23 @@ function CompanyDashboardContent({
130141
useEffect(() => {
131142
async function fetchProfile() {
132143
if (!user?.id) return
133-
144+
134145
try {
135146
const supabase = createClient()
136147
const { data, error } = await supabase
137148
.from('profiles')
138149
.select('id, email, first_name, last_name, avatar_url')
139150
.eq('id', user.id)
140151
.single()
141-
152+
142153
if (!error && data) {
143154
setUserProfile(data)
144155
}
145156
} catch (error) {
146157
console.error('Error fetching user profile:', error)
147158
}
148159
}
149-
160+
150161
fetchProfile()
151162
}, [user?.id])
152163

@@ -187,7 +198,7 @@ function CompanyDashboardContent({
187198
// Generate sidebar items with dynamic company slug
188199
// Use currentCompany.slug as fallback when companySlug from params is undefined
189200
const effectiveSlug = companySlug || currentCompany.slug
190-
201+
191202
const sidebarItems: SidebarGroupType[] = [
192203
{
193204
title: 'Dashboard',

components/dashboard/TeamManagement.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ export function TeamManagement({
9090
// Check if current user can manage team
9191
const canManageTeam = ['owner', 'admin'].includes(currentUserRole)
9292
const canUpdateRoles = currentUserRole === 'owner'
93-
const canRemoveMembers = currentUserRole === 'owner'
94-
93+
// Admins can remove members (except owner), owners can remove anyone
94+
const canRemoveMembers = ['owner', 'admin'].includes(currentUserRole)
95+
9596
// Debug logging
9697
console.log('TeamManagement - currentUserRole:', currentUserRole)
9798
console.log('TeamManagement - canManageTeam:', canManageTeam)
@@ -141,7 +142,7 @@ export function TeamManagement({
141142

142143
if (!response.ok) {
143144
console.error('Invite error response:', data)
144-
145+
145146
// Check if it's a team limit error
146147
if (data.upgrade_required) {
147148
toast.error('Team Member Limit Reached', {
@@ -299,8 +300,8 @@ export function TeamManagement({
299300
<div>
300301
<CardTitle className="text-white">Team Members</CardTitle>
301302
<CardDescription>
302-
{canManageTeam
303-
? 'Manage your team members and their roles'
303+
{canManageTeam
304+
? 'Manage your team members and their roles'
304305
: 'View your team members and their roles'}
305306
</CardDescription>
306307
</div>
@@ -411,7 +412,8 @@ export function TeamManagement({
411412
Update Role
412413
</DropdownMenuItem>
413414
)}
414-
{canRemoveMembers && (
415+
{/* Admins can remove members except owner, owners can remove anyone */}
416+
{canRemoveMembers && (currentUserRole === 'owner' || member.role !== 'owner') && (
415417
<DropdownMenuItem
416418
onClick={() => openRemoveDialog(member)}
417419
className="text-red-500 hover:bg-zinc-800 hover:text-red-400"

0 commit comments

Comments
 (0)