Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions app/api/companies/[slug]/members/[userId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,15 @@ 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'

// Send role change notification email
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,
Expand Down Expand Up @@ -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 }
)
Expand All @@ -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(
Expand All @@ -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'

Expand All @@ -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,
Expand Down
48 changes: 25 additions & 23 deletions app/api/hackathons/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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' },
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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(
Expand All @@ -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')
Expand All @@ -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)
Expand Down
27 changes: 19 additions & 8 deletions app/dashboard/company/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand All @@ -54,7 +64,7 @@ export default function CompanyDashboardLayout({
)
}

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

if (!user || !isAuthorized) {
// Skip authorization check for accept-invitation page
if (!user || (!isAcceptInvitationPage && !isAuthorized)) {
return (
<div className="flex items-center justify-center min-h-screen px-4 bg-black">
<div className="text-center max-w-md">
Expand Down Expand Up @@ -130,23 +141,23 @@ function CompanyDashboardContent({
useEffect(() => {
async function fetchProfile() {
if (!user?.id) return

try {
const supabase = createClient()
const { data, error } = await supabase
.from('profiles')
.select('id, email, first_name, last_name, avatar_url')
.eq('id', user.id)
.single()

if (!error && data) {
setUserProfile(data)
}
} catch (error) {
console.error('Error fetching user profile:', error)
}
}

fetchProfile()
}, [user?.id])

Expand Down Expand Up @@ -187,7 +198,7 @@ function CompanyDashboardContent({
// Generate sidebar items with dynamic company slug
// Use currentCompany.slug as fallback when companySlug from params is undefined
const effectiveSlug = companySlug || currentCompany.slug

const sidebarItems: SidebarGroupType[] = [
{
title: 'Dashboard',
Expand Down
14 changes: 8 additions & 6 deletions components/dashboard/TeamManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ export function TeamManagement({
// Check if current user can manage team
const canManageTeam = ['owner', 'admin'].includes(currentUserRole)
const canUpdateRoles = currentUserRole === 'owner'
const canRemoveMembers = currentUserRole === 'owner'

// Admins can remove members (except owner), owners can remove anyone
const canRemoveMembers = ['owner', 'admin'].includes(currentUserRole)

// Debug logging
console.log('TeamManagement - currentUserRole:', currentUserRole)
console.log('TeamManagement - canManageTeam:', canManageTeam)
Expand Down Expand Up @@ -141,7 +142,7 @@ export function TeamManagement({

if (!response.ok) {
console.error('Invite error response:', data)

// Check if it's a team limit error
if (data.upgrade_required) {
toast.error('Team Member Limit Reached', {
Expand Down Expand Up @@ -299,8 +300,8 @@ export function TeamManagement({
<div>
<CardTitle className="text-white">Team Members</CardTitle>
<CardDescription>
{canManageTeam
? 'Manage your team members and their roles'
{canManageTeam
? 'Manage your team members and their roles'
: 'View your team members and their roles'}
</CardDescription>
</div>
Expand Down Expand Up @@ -411,7 +412,8 @@ export function TeamManagement({
Update Role
</DropdownMenuItem>
)}
{canRemoveMembers && (
{/* Admins can remove members except owner, owners can remove anyone */}
{canRemoveMembers && (currentUserRole === 'owner' || member.role !== 'owner') && (
<DropdownMenuItem
onClick={() => openRemoveDialog(member)}
className="text-red-500 hover:bg-zinc-800 hover:text-red-400"
Expand Down
Loading
Loading