diff --git a/app/api/companies/[slug]/members/[userId]/route.ts b/app/api/companies/[slug]/members/[userId]/route.ts index 030b58f9..32163f5b 100644 --- a/app/api/companies/[slug]/members/[userId]/route.ts +++ b/app/api/companies/[slug]/members/[userId]/route.ts @@ -122,7 +122,7 @@ export async function PUT( .eq('id', user.id) .single() - const changedByName = requestingUserProfile?.first_name + const changedByName = requestingUserProfile?.first_name ? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim() : 'a team administrator' @@ -130,7 +130,7 @@ export async function PUT( if (memberProfile?.email && oldRole !== role) { const memberName = memberProfile.first_name || memberProfile.email.split('@')[0] const dashboardUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'https://codeunia.com'}/dashboard/company/${company.slug}` - + const emailContent = getRoleChangeEmail({ memberName, companyName: company.name, @@ -226,14 +226,14 @@ export async function DELETE( ) } - // Check if user is owner (only owners can remove members) + // Check if user is owner or admin (both can remove members) const requestingMember = await companyMemberService.checkMembership(user.id, company.id) - if (!requestingMember || requestingMember.role !== 'owner') { + if (!requestingMember || !['owner', 'admin'].includes(requestingMember.role)) { return NextResponse.json( { success: false, - error: 'Insufficient permissions: Owner role required to remove members', + error: 'Insufficient permissions: Owner or Admin role required to remove members', }, { status: 403 } ) @@ -252,6 +252,17 @@ export async function DELETE( ) } + // Prevent admins from removing owners (only owners can remove owners) + if (requestingMember.role === 'admin' && targetMember.role === 'owner') { + return NextResponse.json( + { + success: false, + error: 'Insufficient permissions: Only owners can remove other owners', + }, + { status: 403 } + ) + } + // Prevent owner from removing themselves if they're the last owner if (userId === user.id && targetMember.role === 'owner') { return NextResponse.json( @@ -277,7 +288,7 @@ export async function DELETE( .eq('id', user.id) .single() - const removedByName = requestingUserProfile?.first_name + const removedByName = requestingUserProfile?.first_name ? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim() : 'a team administrator' @@ -287,7 +298,7 @@ export async function DELETE( // Send removal notification email if (memberProfile?.email) { const memberName = memberProfile.first_name || memberProfile.email.split('@')[0] - + const emailContent = getMemberRemovedEmail({ memberName, companyName: company.name, diff --git a/app/api/hackathons/[id]/route.ts b/app/api/hackathons/[id]/route.ts index 48e5112d..2081a2c5 100644 --- a/app/api/hackathons/[id]/route.ts +++ b/app/api/hackathons/[id]/route.ts @@ -16,14 +16,14 @@ export async function GET(request: NextRequest, { params }: RouteContext) { try { const { id } = await params const hackathon = await hackathonsService.getHackathonBySlug(id) - + if (!hackathon) { return NextResponse.json( { error: 'Hackathon not found' }, { status: 404 } ) } - + return NextResponse.json(hackathon) } catch (error) { console.error('Error in GET /api/hackathons/[id]:', error) @@ -39,7 +39,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { try { const { id } = await params const hackathonData = await request.json() - + // Check authentication const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() @@ -52,8 +52,8 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { } // Get the existing hackathon to check company_id - const existingHackathon = await hackathonsService.getHackathonBySlug(id) - + const existingHackathon = await hackathonsService.getHackathonByIdOrSlug(id) + if (!existingHackathon) { return NextResponse.json( { error: 'Hackathon not found' }, @@ -69,7 +69,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { .select('is_admin') .eq('id', user.id) .single() - + if (profile?.is_admin) { isAuthorized = true } @@ -83,7 +83,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { .eq('user_id', user.id) .eq('status', 'active') .single() - + if (membership) { isAuthorized = true } @@ -95,9 +95,9 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { { status: 401 } ) } - + const hackathon = await hackathonsService.updateHackathon(id, hackathonData, user.id) - + return NextResponse.json({ hackathon }) } catch (error) { console.error('Error in PUT /api/hackathons/[id]:', error) @@ -112,9 +112,9 @@ export async function PUT(request: NextRequest, { params }: RouteContext) { export async function DELETE(_request: NextRequest, { params }: RouteContext) { try { const { id } = await params - + console.log('🗑️ DELETE request for hackathon:', id) - + // Check authentication const supabase = await createClient() const { data: { user }, error: authError } = await supabase.auth.getUser() @@ -130,8 +130,8 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) { console.log('✅ User authenticated:', user.id) // Get the existing hackathon to check company_id - const existingHackathon = await hackathonsService.getHackathonBySlug(id) - + const existingHackathon = await hackathonsService.getHackathonByIdOrSlug(id) + if (!existingHackathon) { console.error('❌ Hackathon not found:', id) return NextResponse.json( @@ -150,13 +150,13 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) { .select('is_admin') .eq('id', user.id) .single() - + if (profile?.is_admin) { isAuthorized = true console.log('✅ User is admin') } - // If not admin, check if user is a member of the company + // If not admin, check if user is a company owner or admin (not editor/viewer) if (!isAuthorized && existingHackathon.company_id) { const { data: membership } = await supabase .from('company_members') @@ -165,28 +165,30 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) { .eq('user_id', user.id) .eq('status', 'active') .single() - - if (membership) { + + if (membership && ['owner', 'admin'].includes(membership.role)) { isAuthorized = true - console.log('✅ User is company member with role:', membership.role) + console.log('✅ User is company owner/admin with role:', membership.role) + } else if (membership) { + console.log('❌ User has insufficient role:', membership.role) } } if (!isAuthorized) { console.error('❌ User not authorized to delete hackathon') return NextResponse.json( - { error: 'Unauthorized: You must be a company member or admin to delete this hackathon' }, + { error: 'Insufficient permissions: Owner or Admin role required to delete hackathons' }, { status: 403 } ) } - + console.log('🗑️ Attempting to delete hackathon...') await hackathonsService.deleteHackathon(id) console.log('✅ Hackathon deleted successfully') - - return NextResponse.json({ + + return NextResponse.json({ success: true, - message: 'Hackathon deleted successfully' + message: 'Hackathon deleted successfully' }) } catch (error) { console.error('❌ Error in DELETE /api/hackathons/[id]:', error) diff --git a/app/dashboard/company/layout.tsx b/app/dashboard/company/layout.tsx index 8295d6fb..f530bfae 100644 --- a/app/dashboard/company/layout.tsx +++ b/app/dashboard/company/layout.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react' import Link from 'next/link' -import { useParams } from 'next/navigation' +import { useParams, usePathname } from 'next/navigation' import { Button } from '@/components/ui/button' import { CompanySidebar } from '@/components/dashboard/CompanySidebar' import { CompanyHeader } from '@/components/dashboard/CompanyHeader' @@ -35,10 +35,20 @@ export default function CompanyDashboardLayout({ children: React.ReactNode }) { const { user, loading, error } = useAuth() - const { isChecking, isAuthorized } = useRoleProtection('company_member') const params = useParams() + const pathname = usePathname() const companySlug = params?.slug as string | undefined + // Check if this is the accept-invitation page + // Users with pending invitations should be able to access this page + const isAcceptInvitationPage = pathname?.includes('/accept-invitation') ?? false + + // Only apply role protection if NOT on the accept-invitation page + // Skip redirects on accept-invitation page to allow pending users to access it + const { isChecking, isAuthorized } = useRoleProtection('company_member', { + skipRedirect: isAcceptInvitationPage + }) + // Prevent hydration mismatch by using a consistent initial state const [mounted, setMounted] = useState(false) @@ -54,7 +64,7 @@ export default function CompanyDashboardLayout({ ) } - if (loading || isChecking) { + if (loading || (!isAcceptInvitationPage && isChecking)) { return (