Skip to content

Commit d8850c2

Browse files
fix(courses): use admin client for course mutations to bypass stale JWT RLS
Fixes LMS-FRONT-7C Root cause: RLS policy on courses table checks get_tenant_role() from JWT claims, but subdomain tenant users may have stale/missing tenant_role in their JWT. The server action already validates auth and role server-side via getUser() + getUserRole() (queries tenant_users directly), so using createAdminClient() for the actual DB mutation is safe and correct. Applied to createCourse, updateCourse, archiveCourse, and deleteCourse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cafb39b commit d8850c2

1 file changed

Lines changed: 25 additions & 9 deletions

File tree

app/actions/teacher/courses.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,17 @@ export async function createCourse(courseData: CourseFormData) {
128128
)
129129
}
130130

131-
// Create course
132-
const { data: course, error } = await supabase
131+
// Use admin client for insert — auth and role are already validated above.
132+
// The user's JWT may have stale tenant_role claims that don't match the
133+
// RLS policy on courses (requires tenant_role = teacher|admin in JWT).
134+
const adminClient = createAdminClient()
135+
const { data: course, error } = await adminClient
133136
.from('courses')
134137
.insert({
135-
...courseData,
138+
title: courseData.title,
139+
description: courseData.description || null,
140+
thumbnail_url: courseData.thumbnail_url || null,
141+
category_id: courseData.category_id || null,
136142
author_id: user.id,
137143
tenant_id: tenantId,
138144
status: courseData.status || 'draft',
@@ -182,10 +188,17 @@ export async function updateCourse(courseId: number, courseData: CourseFormData)
182188
throw new Error('Unauthorized: You can only update your own courses')
183189
}
184190

185-
// Update course
186-
const { error } = await supabase
191+
// Use admin client — auth and ownership validated above, JWT tenant_role may be stale
192+
const adminClient = createAdminClient()
193+
const { error } = await adminClient
187194
.from('courses')
188-
.update(courseData)
195+
.update({
196+
title: courseData.title,
197+
description: courseData.description || null,
198+
thumbnail_url: courseData.thumbnail_url || null,
199+
category_id: courseData.category_id || null,
200+
status: courseData.status || undefined,
201+
})
189202
.eq('course_id', courseId)
190203
.eq('tenant_id', tenantId)
191204

@@ -239,7 +252,8 @@ export async function archiveCourse(courseId: number) {
239252
if (!existingCourse) throw new Error('Course not found')
240253
if (role !== 'admin' && existingCourse.author_id !== user.id) throw new Error('Unauthorized')
241254

242-
const { error } = await supabase
255+
const adminClient = createAdminClient()
256+
const { error } = await adminClient
243257
.from('courses')
244258
.update({ status: 'archived' })
245259
.eq('course_id', courseId)
@@ -276,9 +290,10 @@ export async function deleteCourse(courseId: number) {
276290
if (!course) throw new Error('Course not found')
277291
if (role !== 'admin' && course.author_id !== user.id) throw new Error('Unauthorized')
278292

293+
const adminClient = createAdminClient()
294+
279295
// Notify enrolled students before deleting
280296
try {
281-
const adminClient = createAdminClient()
282297
const { data: enrollments } = await adminClient
283298
.from('enrollments')
284299
.select('user_id')
@@ -309,7 +324,8 @@ export async function deleteCourse(courseId: number) {
309324
}
310325

311326
// Delete the course (cascade will handle lessons, exams, etc.)
312-
const { error } = await supabase
327+
// adminClient already created above for email notifications
328+
const { error } = await adminClient
313329
.from('courses')
314330
.delete()
315331
.eq('course_id', courseId)

0 commit comments

Comments
 (0)