Skip to content

Commit fa1c0bc

Browse files
committed
feat: Implement flexible hackathon retrieval by ID or slug and update API routes to use it, while restricting hackathon deletion to company owners or admins.
1 parent 3a00528 commit fa1c0bc

File tree

2 files changed

+71
-30
lines changed

2 files changed

+71
-30
lines changed

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)

lib/services/hackathons.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,39 @@ class HackathonsService {
141141
return hackathon
142142
}
143143

144+
async getHackathonByIdOrSlug(idOrSlug: string): Promise<Hackathon | null> {
145+
const cacheKey = `hackathon:${idOrSlug}`
146+
const cached = getCachedData(cacheKey)
147+
if (cached) {
148+
return cached as Hackathon
149+
}
150+
151+
const supabase = await createClient()
152+
153+
// Check if idOrSlug is a number (ID) or string (slug)
154+
const isNumeric = /^\d+$/.test(idOrSlug)
155+
156+
const { data: hackathon, error } = await supabase
157+
.from('hackathons')
158+
.select(`
159+
*,
160+
company:companies(*)
161+
`)
162+
.eq(isNumeric ? 'id' : 'slug', isNumeric ? parseInt(idOrSlug) : idOrSlug)
163+
.single()
164+
165+
if (error) {
166+
if (error.code === 'PGRST116') {
167+
return null // Hackathon not found
168+
}
169+
console.error('Error fetching hackathon:', error)
170+
throw new Error('Failed to fetch hackathon')
171+
}
172+
173+
setCachedData(cacheKey, hackathon)
174+
return hackathon
175+
}
176+
144177
async getFeaturedHackathons(limit: number = 5) {
145178
const cacheKey = `featured_hackathons:${limit}`
146179
const cached = getCachedData(cacheKey)
@@ -213,14 +246,14 @@ class HackathonsService {
213246
}
214247

215248
async updateHackathon(
216-
slug: string,
249+
idOrSlug: string,
217250
hackathonData: Partial<Omit<Hackathon, 'id' | 'created_at' | 'updated_at'>>,
218251
userId?: string
219252
): Promise<Hackathon> {
220253
const supabase = await createClient()
221254

222-
// Get existing hackathon first
223-
const existingHackathon = await this.getHackathonBySlug(slug)
255+
// Get existing hackathon first (handles both ID and slug)
256+
const existingHackathon = await this.getHackathonByIdOrSlug(idOrSlug)
224257
if (!existingHackathon) {
225258
throw new Error('Hackathon not found')
226259
}
@@ -245,7 +278,7 @@ class HackathonsService {
245278
const { data: hackathon, error } = await supabase
246279
.from('hackathons')
247280
.update(updatePayload)
248-
.eq('slug', slug)
281+
.eq('id', existingHackathon.id)
249282
.select(`
250283
*,
251284
company:companies(*)
@@ -322,15 +355,21 @@ class HackathonsService {
322355
return hackathon
323356
}
324357

325-
async deleteHackathon(slug: string) {
358+
async deleteHackathon(idOrSlug: string) {
326359
const supabase = await createClient()
327360

328-
console.log('🗑️ Deleting hackathon with slug:', slug)
361+
console.log('🗑️ Deleting hackathon with ID or slug:', idOrSlug)
362+
363+
// Get existing hackathon first to get the ID
364+
const existingHackathon = await this.getHackathonByIdOrSlug(idOrSlug)
365+
if (!existingHackathon) {
366+
throw new Error('Hackathon not found')
367+
}
329368

330369
const { data, error } = await supabase
331370
.from('hackathons')
332371
.delete()
333-
.eq('slug', slug)
372+
.eq('id', existingHackathon.id)
334373
.select()
335374

336375
if (error) {

0 commit comments

Comments
 (0)