Skip to content

Commit 495ae02

Browse files
authored
Merge pull request #355 from codeunia-dev/fix/rbac
feat(moderation): Add delete action for hackathons and events in admin panel
2 parents 5016b0c + 109b91b commit 495ae02

File tree

12 files changed

+329
-145
lines changed

12 files changed

+329
-145
lines changed

app/api/admin/moderation/events/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const GET = withPlatformAdmin(async (request: NextRequest) => {
1414
const limit = parseInt(searchParams.get('limit') || '20')
1515
const offset = parseInt(searchParams.get('offset') || '0')
1616

17-
// Get pending events from moderation service
17+
// Get pending and deleted events from moderation service
1818
const { events, total } = await moderationService.getPendingEvents({
1919
limit,
2020
offset,

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

Lines changed: 98 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -19,96 +19,114 @@ export async function POST(request: NextRequest, context: RouteContext) {
1919
return withPlatformAdmin(async () => {
2020
try {
2121
const { id } = await context.params
22-
const body = await request.json()
23-
const { action, reason } = body
22+
const body = await request.json()
23+
const { action, reason } = body
2424

25-
if (!action || !['approve', 'reject'].includes(action)) {
26-
return NextResponse.json(
27-
{ success: false, error: 'Invalid action' },
28-
{ status: 400 }
29-
)
30-
}
25+
if (!action || !['approve', 'reject', 'delete'].includes(action)) {
26+
return NextResponse.json(
27+
{ success: false, error: 'Invalid action' },
28+
{ status: 400 }
29+
)
30+
}
3131

32-
if (action === 'reject' && !reason) {
33-
return NextResponse.json(
34-
{ success: false, error: 'Rejection reason is required' },
35-
{ status: 400 }
36-
)
37-
}
32+
if (action === 'reject' && !reason) {
33+
return NextResponse.json(
34+
{ success: false, error: 'Rejection reason is required' },
35+
{ status: 400 }
36+
)
37+
}
3838

39-
const supabase = await createClient()
40-
const { data: { user } } = await supabase.auth.getUser()
39+
const supabase = await createClient()
40+
const { data: { user } } = await supabase.auth.getUser()
4141

42-
if (!user) {
43-
return NextResponse.json(
44-
{ success: false, error: 'Unauthorized' },
45-
{ status: 401 }
46-
)
47-
}
42+
if (!user) {
43+
return NextResponse.json(
44+
{ success: false, error: 'Unauthorized' },
45+
{ status: 401 }
46+
)
47+
}
4848

49-
// Get the hackathon
50-
const { data: hackathon, error: fetchError } = await supabase
51-
.from('hackathons')
52-
.select('*')
53-
.eq('id', id)
54-
.single()
49+
// Get the hackathon
50+
const { data: hackathon, error: fetchError } = await supabase
51+
.from('hackathons')
52+
.select('*')
53+
.eq('id', id)
54+
.single()
5555

56-
if (fetchError || !hackathon) {
57-
return NextResponse.json(
58-
{ success: false, error: 'Hackathon not found' },
59-
{ status: 404 }
60-
)
61-
}
56+
if (fetchError || !hackathon) {
57+
return NextResponse.json(
58+
{ success: false, error: 'Hackathon not found' },
59+
{ status: 404 }
60+
)
61+
}
6262

63-
// Update hackathon based on action
64-
const updateData: {
65-
updated_at: string
66-
approval_status?: string
67-
approved_by?: string
68-
approved_at?: string
69-
status?: string
70-
rejection_reason?: string | null
71-
} = {
72-
updated_at: new Date().toISOString(),
73-
}
63+
// Update hackathon based on action
64+
const updateData: {
65+
updated_at: string
66+
approval_status?: string
67+
approved_by?: string
68+
approved_at?: string
69+
status?: string
70+
rejection_reason?: string | null
71+
} = {
72+
updated_at: new Date().toISOString(),
73+
}
7474

75-
if (action === 'approve') {
76-
updateData.approval_status = 'approved'
77-
updateData.approved_by = user.id
78-
updateData.approved_at = new Date().toISOString()
79-
updateData.status = 'live'
80-
updateData.rejection_reason = null
81-
} else if (action === 'reject') {
82-
updateData.approval_status = 'rejected'
83-
updateData.rejection_reason = reason
84-
updateData.status = 'draft'
85-
}
75+
if (action === 'approve') {
76+
updateData.approval_status = 'approved'
77+
updateData.approved_by = user.id
78+
updateData.approved_at = new Date().toISOString()
79+
updateData.status = 'live'
80+
updateData.rejection_reason = null
81+
} else if (action === 'reject') {
82+
updateData.approval_status = 'rejected'
83+
updateData.rejection_reason = reason
84+
updateData.status = 'draft'
85+
} else if (action === 'delete') {
86+
// Permanently delete the hackathon
87+
const { error: deleteError } = await supabase
88+
.from('hackathons')
89+
.delete()
90+
.eq('id', id)
8691

87-
const { error: updateError } = await supabase
88-
.from('hackathons')
89-
.update(updateData)
90-
.eq('id', id)
92+
if (deleteError) {
93+
throw deleteError
94+
}
9195

92-
if (updateError) {
93-
throw updateError
94-
}
96+
// Invalidate caches
97+
await UnifiedCache.purgeByTags(['content', 'api'])
98+
99+
return NextResponse.json({
100+
success: true,
101+
message: 'Hackathon permanently deleted',
102+
})
103+
}
104+
105+
const { error: updateError } = await supabase
106+
.from('hackathons')
107+
.update(updateData)
108+
.eq('id', id)
95109

96-
// Invalidate caches
97-
await UnifiedCache.purgeByTags(['content', 'api'])
98-
99-
return NextResponse.json({
100-
success: true,
101-
message: `Hackathon ${action}d successfully`,
102-
})
103-
} catch (error) {
104-
console.error('Error in hackathon moderation action:', error)
105-
return NextResponse.json(
106-
{
107-
success: false,
108-
error: error instanceof Error ? error.message : 'Failed to execute action',
109-
},
110-
{ status: 500 }
111-
)
112-
}
110+
if (updateError) {
111+
throw updateError
112+
}
113+
114+
// Invalidate caches
115+
await UnifiedCache.purgeByTags(['content', 'api'])
116+
117+
return NextResponse.json({
118+
success: true,
119+
message: `Hackathon ${action}d successfully`,
120+
})
121+
} catch (error) {
122+
console.error('Error in hackathon moderation action:', error)
123+
return NextResponse.json(
124+
{
125+
success: false,
126+
error: error instanceof Error ? error.message : 'Failed to execute action',
127+
},
128+
{ status: 500 }
129+
)
130+
}
113131
})(request)
114132
}

app/api/admin/moderation/hackathons/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const GET = withPlatformAdmin(async (request: NextRequest) => {
2323
*,
2424
company:companies(*)
2525
`, { count: 'exact' })
26-
.eq('approval_status', 'pending')
26+
.in('approval_status', ['pending', 'deleted'])
2727
.order('created_at', { ascending: false })
2828
.range(offset, offset + limit - 1)
2929

app/api/events/[slug]/route.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function GET(
1818
// Try to get from cache first
1919
const cacheKey = `event:${slug}`;
2020
const cached = await UnifiedCache.get(cacheKey);
21-
21+
2222
if (cached) {
2323
return UnifiedCache.createResponse(cached, 'API_STANDARD');
2424
}
@@ -40,7 +40,7 @@ export async function GET(
4040

4141
} catch (error) {
4242
console.error('Error in GET /api/events/[slug]:', error);
43-
43+
4444
if (error instanceof EventError) {
4545
return NextResponse.json(
4646
{ error: error.message, code: error.code },
@@ -112,7 +112,7 @@ export async function PUT(
112112

113113
} catch (error) {
114114
console.error('Error in PUT /api/events/[slug]:', error);
115-
115+
116116
if (error instanceof EventError) {
117117
return NextResponse.json(
118118
{ error: error.message, code: error.code },
@@ -167,19 +167,26 @@ export async function DELETE(
167167
}
168168

169169
// Delete event using service
170-
await eventsService.deleteEvent(existingEvent.id, user.id);
170+
const result = await eventsService.deleteEvent(existingEvent.id, user.id)
171171

172172
// Invalidate caches
173-
await UnifiedCache.purgeByTags(['content', 'api']);
173+
await UnifiedCache.purgeByTags(['content', 'api'])
174174

175175
return NextResponse.json(
176-
{ message: 'Event deleted successfully' },
176+
{
177+
success: true,
178+
message: result.soft_delete
179+
? 'Event marked for deletion. Admin approval required.'
180+
: 'Event deleted successfully',
181+
soft_delete: result.soft_delete
182+
},
177183
{ status: 200 }
178-
);
184+
)
185+
;
179186

180187
} catch (error) {
181188
console.error('Error in DELETE /api/events/[slug]:', error);
182-
189+
183190
if (error instanceof EventError) {
184191
return NextResponse.json(
185192
{ error: error.message, code: error.code },

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,71 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) {
182182
)
183183
}
184184

185-
console.log('🗑️ Attempting to delete hackathon...')
185+
// Check if hackathon is approved (live) - use soft delete
186+
if (existingHackathon.approval_status === 'approved') {
187+
console.log('🔄 Hackathon is approved - marking for deletion (soft delete)')
188+
189+
// Mark as deleted instead of hard deleting
190+
const { error: updateError } = await supabase
191+
.from('hackathons')
192+
.update({
193+
approval_status: 'deleted',
194+
updated_at: new Date().toISOString()
195+
})
196+
.eq('id', existingHackathon.id)
197+
198+
if (updateError) {
199+
console.error('❌ Error marking hackathon for deletion:', updateError)
200+
throw new Error('Failed to mark hackathon for deletion')
201+
}
202+
203+
console.log('✅ Hackathon marked for deletion - requires admin approval')
204+
205+
// Notify admins about the deletion request
206+
const hackathonId = existingHackathon.id
207+
if (hackathonId) {
208+
const { data: adminUsers } = await supabase
209+
.from('profiles')
210+
.select('id')
211+
.eq('role', 'admin')
212+
213+
if (adminUsers && adminUsers.length > 0) {
214+
const notifications = adminUsers.map(admin => ({
215+
user_id: admin.id,
216+
company_id: existingHackathon.company_id,
217+
type: 'hackathon_deleted' as const,
218+
title: 'Hackathon Deletion Request',
219+
message: `"${existingHackathon.title}" has been marked for deletion and requires approval`,
220+
action_url: `/admin/moderation/hackathons/${hackathonId}`,
221+
action_label: 'Review Deletion',
222+
hackathon_id: hackathonId.toString(),
223+
metadata: {
224+
hackathon_title: existingHackathon.title,
225+
hackathon_slug: existingHackathon.slug
226+
}
227+
}))
228+
229+
await supabase.from('notifications').insert(notifications)
230+
console.log(`📧 Notified ${adminUsers.length} admin(s) about deletion request`)
231+
}
232+
}
233+
234+
return NextResponse.json({
235+
success: true,
236+
message: 'Hackathon marked for deletion. Admin approval required.',
237+
soft_delete: true
238+
})
239+
}
240+
241+
// For draft/pending hackathons, allow hard delete
242+
console.log('🗑️ Hackathon is draft/pending - performing hard delete')
186243
await hackathonsService.deleteHackathon(id)
187244
console.log('✅ Hackathon deleted successfully')
188245

189246
return NextResponse.json({
190247
success: true,
191-
message: 'Hackathon deleted successfully'
248+
message: 'Hackathon deleted successfully',
249+
soft_delete: false
192250
})
193251
} catch (error) {
194252
console.error('❌ Error in DELETE /api/hackathons/[id]:', error)

app/dashboard/company/[slug]/events/page.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,16 @@ export default function CompanyEventsPage() {
9999
}
100100
}
101101

102+
// Filter out deleted items for stats (summary cards should only show active items)
103+
const activeEvents = events.filter(e => e.approval_status !== 'deleted')
104+
102105
const stats = {
103-
total: events.length,
104-
approved: events.filter(e => e.approval_status === 'approved').length,
105-
pending: events.filter(e => e.approval_status === 'pending').length,
106-
draft: events.filter(e => e.status === 'draft').length,
107-
totalViews: events.reduce((sum, e) => sum + (e.views || 0), 0),
108-
totalRegistrations: events.reduce((sum, e) => sum + (e.registered || 0), 0),
106+
total: activeEvents.length,
107+
approved: activeEvents.filter(e => e.approval_status === 'approved').length,
108+
pending: activeEvents.filter(e => e.approval_status === 'pending').length,
109+
draft: activeEvents.filter(e => e.status === 'draft').length,
110+
totalViews: activeEvents.reduce((sum, e) => sum + (e.views || 0), 0),
111+
totalRegistrations: activeEvents.reduce((sum, e) => sum + (e.registered || 0), 0),
109112
}
110113

111114
const getApprovalBadge = (status: string) => {
@@ -138,6 +141,13 @@ export default function CompanyEventsPage() {
138141
Changes Requested
139142
</Badge>
140143
)
144+
case 'deleted':
145+
return (
146+
<Badge className="bg-gray-500/10 text-gray-600 border-gray-500/20 pointer-events-none">
147+
<Trash2 className="h-3 w-3 mr-1" />
148+
Deleted
149+
</Badge>
150+
)
141151
default:
142152
return <Badge variant="outline" className="pointer-events-none">{status}</Badge>
143153
}

0 commit comments

Comments
 (0)