Skip to content

Commit 031b5da

Browse files
fix(public): use admin client for public course pages on non-default tenants
Anon users have no tenant_id in JWT, so RLS get_tenant_id() defaults to the wrong tenant. Public read-only pages now use createAdminClient() with explicit tenant_id filter from the x-tenant-id header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 65d194a commit 031b5da

2 files changed

Lines changed: 16 additions & 8 deletions

File tree

app/[locale]/(public)/courses/[id]/page.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createClient } from "@/lib/supabase/server";
2+
import { createAdminClient } from "@/lib/supabase/admin";
23
import { Button } from "@/components/ui/button";
34
import Link from "next/link";
45
import { notFound } from "next/navigation";
@@ -22,7 +23,7 @@ import {
2223
} from "@/components/ui/accordion";
2324
import { Card, CardContent } from "@/components/ui/card";
2425
import { getTranslations } from 'next-intl/server';
25-
import { getCurrentUserId } from '@/lib/supabase/tenant'
26+
import { getCurrentUserId, getCurrentTenantId } from '@/lib/supabase/tenant'
2627

2728
export const dynamic = 'force-dynamic';
2829

@@ -36,11 +37,14 @@ interface Lesson {
3637
export default async function CourseDetailsPage(props: { params: Promise<{ id: string }> }) {
3738
const params = await props.params;
3839
const t = await getTranslations('coursePublicDetails');
39-
const supabase = await createClient();
40+
const tenantId = await getCurrentTenantId();
41+
// Use admin client for public reads — anon JWT has no tenant_id claim,
42+
// so RLS get_tenant_id() defaults to wrong tenant for non-default tenants.
43+
const adminClient = createAdminClient();
4044

4145
const userId = await getCurrentUserId()
4246
// Fetch course with lessons and category
43-
const { data: course, error } = await supabase
47+
const { data: course, error } = await adminClient
4448
.from("courses")
4549
.select(`
4650
*,
@@ -57,6 +61,7 @@ export default async function CourseDetailsPage(props: { params: Promise<{ id: s
5761
`)
5862
.eq("course_id", parseInt(params.id))
5963
.eq("status", "published")
64+
.eq("tenant_id", tenantId)
6065
.single();
6166

6267
if (error || !course) {
@@ -66,7 +71,7 @@ export default async function CourseDetailsPage(props: { params: Promise<{ id: s
6671
// Fetch author separately (profiles is global, no tenant_id)
6772
let author = null;
6873
if (course.author_id) {
69-
const { data: authorData } = await supabase
74+
const { data: authorData } = await adminClient
7075
.from("profiles")
7176
.select("id, full_name, avatar_url, bio")
7277
.eq("id", course.author_id)
@@ -80,9 +85,10 @@ export default async function CourseDetailsPage(props: { params: Promise<{ id: s
8085
const estimatedHours = Math.floor(totalLessons * 10 / 60);
8186
const estimatedMinutes = (totalLessons * 10) % 60;
8287

83-
// Check user's enrollment
88+
// Check user's enrollment (uses RLS client — only works for logged-in users)
8489
let hasAccess = false;
8590
if (userId) {
91+
const supabase = await createClient();
8692
const { data: enrollment } = await supabase
8793
.from('enrollments')
8894
.select('enrollment_id')
@@ -97,7 +103,7 @@ export default async function CourseDetailsPage(props: { params: Promise<{ id: s
97103
}
98104

99105
// Fetch product for pricing
100-
const { data: productCourses } = await supabase
106+
const { data: productCourses } = await adminClient
101107
.from('product_courses')
102108
.select('product:products(*)')
103109
.eq('course_id', parseInt(params.id))

app/[locale]/(public)/courses/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createClient } from "@/lib/supabase/server";
1+
import { createAdminClient } from "@/lib/supabase/admin";
22
import { getCurrentTenantId } from "@/lib/supabase/tenant";
33
import { CourseCard } from "@/components/public/course-card";
44
import { CourseSearchBar } from "@/components/shared/course-search-bar";
@@ -15,7 +15,9 @@ export default async function CoursesPage({
1515
const { search, category } = await searchParams;
1616
const t = await getTranslations('coursesCatalog');
1717
const tSearch = await getTranslations('courseSearch');
18-
const supabase = await createClient();
18+
// Use admin client for public reads — anon JWT has no tenant_id claim,
19+
// so RLS get_tenant_id() defaults to wrong tenant for non-default tenants.
20+
const supabase = createAdminClient();
1921
const tenantId = await getCurrentTenantId();
2022

2123
// Sanitize search input — strip special characters used in ilike patterns

0 commit comments

Comments
 (0)