Skip to content

Commit ecdf0cf

Browse files
Merge pull request #326 from guillermoscript/feat/chat-bearer-auth
feat(api): Bearer-token auth + JWT tenant resolution for chat routes (mobile app backend)
2 parents d4c710f + c589610 commit ecdf0cf

32 files changed

Lines changed: 971 additions & 131 deletions

File tree

app/[locale]/dashboard/student/browse/page.tsx

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import Link from 'next/link'
99
import { IconAlertCircle, IconSparkles, IconTrophy, IconSearch } from '@tabler/icons-react'
1010
import {getCurrentTenantId, getCurrentUserId } from '@/lib/supabase/tenant'
1111
import { fetchAccessibleCourseIds } from '@/lib/services/course-access'
12+
import {
13+
getPublishedCourses,
14+
getCourseCategories,
15+
getActiveSubscriptions,
16+
getPlanCourses,
17+
} from '@lms/core'
1218

1319
export default async function BrowseCoursesPage({
1420
searchParams,
@@ -28,50 +34,14 @@ export default async function BrowseCoursesPage({
2834
redirect('/auth/login')
2935
}
3036

31-
// Build course query with filters
32-
let query = supabase
33-
.from('courses')
34-
.select(`
35-
course_id,
36-
title,
37-
description,
38-
thumbnail_url,
39-
tags,
40-
category_id
41-
`)
42-
.eq('tenant_id', tenantId)
43-
.eq('status', 'published')
44-
.order('title', { ascending: true })
45-
46-
// Apply search filter
47-
if (sanitizedSearch) {
48-
query = query.or(`title.ilike.%${sanitizedSearch}%,description.ilike.%${sanitizedSearch}%`)
49-
}
50-
51-
// Apply category filter
52-
if (category) {
53-
query = query.eq('category_id', category)
54-
}
55-
56-
// Parallelize 4 independent queries
37+
// Parallelize 4 independent queries — shared query logic from @lms/core
5738
const [{ data: subscriptions }, { data: categories }, { data: courses }, accessibleCourseIds] = await Promise.all([
58-
supabase.from('subscriptions').select(`
59-
subscription_id,
60-
subscription_status,
61-
end_date,
62-
plan:plans!subscriptions_plan_id_fkey (
63-
plan_id,
64-
plan_name,
65-
price
66-
)
67-
`)
68-
.eq('user_id', userId).eq('tenant_id', tenantId)
69-
.eq('subscription_status', 'active')
70-
.gte('end_date', new Date().toISOString())
71-
.order('end_date', { ascending: false }),
72-
supabase.from('course_categories').select('id, name')
73-
.eq('tenant_id', tenantId).order('name'),
74-
query,
39+
getActiveSubscriptions(supabase, userId, tenantId),
40+
getCourseCategories(supabase, tenantId),
41+
getPublishedCourses(supabase, tenantId, {
42+
search: sanitizedSearch || undefined,
43+
categoryId: category ? Number(category) : undefined,
44+
}),
7545
// "Already enrolled" set — entitlements model (any active access source).
7646
fetchAccessibleCourseIds(supabase, userId),
7747
])
@@ -83,10 +53,7 @@ export default async function BrowseCoursesPage({
8353
if (activeSubscription) {
8454
const planId = (activeSubscription.plan as any)?.plan_id
8555
if (planId) {
86-
const { data: planCourses } = await supabase
87-
.from('plan_courses')
88-
.select('course_id')
89-
.eq('plan_id', planId)
56+
const { data: planCourses } = await getPlanCourses(supabase, planId)
9057

9158
if (planCourses && planCourses.length > 0) {
9259
allowedCourseIds = new Set(planCourses.map(pc => pc.course_id))

app/[locale]/dashboard/student/courses/[courseId]/exercises/[exerciseId]/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,6 @@ export default async function ExercisePage({ params }: PageProps) {
247247
exerciseId={exercise.id}
248248
isExerciseCompleted={isExerciseCompleted}
249249
userCode={lastSubmission?.submission_code}
250-
tenantId={tenantId}
251250
/>
252251

253252
{otherExercises && otherExercises.length > 0 && (

app/[locale]/dashboard/student/courses/[courseId]/lessons/[lessonId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export default async function LessonPage({ params }: PageProps) {
371371

372372
{/* Comments Section */}
373373
<section className="border-t pt-10">
374-
<LessonComments lessonId={lesson.id} userId={userId} tenantId={tenantId} initialComments={initialComments} />
374+
<LessonComments lessonId={lesson.id} userId={userId} initialComments={initialComments} />
375375
</section>
376376
</div>
377377
</div>

app/[locale]/dashboard/student/courses/page.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import Link from 'next/link'
99
import {getCurrentTenantId, getCurrentUserId } from '@/lib/supabase/tenant'
1010
import { fetchCourseAccessMap } from '@/lib/services/course-access'
1111
import type { CourseAccess } from '@/lib/services/enrollment-service'
12+
import {
13+
getCoursesByIds,
14+
getLessonsByCourseIds,
15+
getExamsByCourseIds,
16+
getLessonCompletions,
17+
getExamSubmissions,
18+
} from '@lms/core'
1219

1320
const NO_ACCESS: CourseAccess = {
1421
hasAccess: false,
@@ -62,16 +69,11 @@ export default async function MyCoursesPage({ searchParams }: PageProps) {
6269
{ data: lessonCompletions },
6370
{ data: examSubmissions },
6471
] = await Promise.all([
65-
supabase.from('courses').select('course_id, title, description, thumbnail_url, status')
66-
.in('course_id', courseIds).eq('tenant_id', tenantId),
67-
supabase.from('lessons').select('id, title, sequence, course_id')
68-
.in('course_id', courseIds).eq('tenant_id', tenantId),
69-
supabase.from('exams').select('exam_id, title, sequence, course_id')
70-
.in('course_id', courseIds).eq('tenant_id', tenantId),
71-
supabase.from('lesson_completions').select('lesson_id, completed_at')
72-
.eq('user_id', userId),
73-
supabase.from('exam_submissions').select('submission_id, exam_id, submission_date, score')
74-
.eq('student_id', userId).eq('tenant_id', tenantId),
72+
getCoursesByIds(supabase, courseIds, tenantId),
73+
getLessonsByCourseIds(supabase, courseIds, tenantId),
74+
getExamsByCourseIds(supabase, courseIds, tenantId),
75+
getLessonCompletions(supabase, userId),
76+
getExamSubmissions(supabase, userId, tenantId),
7577
])
7678

7779
// Build lookup maps

app/[locale]/dashboard/student/page.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MiniLeaderboard } from '@/components/gamification/mini-leaderboard'
1313
import { getCurrentTenantId, getSessionUser } from '@/lib/supabase/tenant'
1414
import { OnboardingChecklist } from '@/components/shared/onboarding-checklist'
1515
import { StudentDashboardTour } from '@/components/tours/student-dashboard-tour'
16+
import { getLessonCompletions } from '@lms/core'
1617

1718
async function getData(userId: string, tenantId: string) {
1819
const supabase = createAdminClient()
@@ -44,10 +45,7 @@ async function getData(userId: string, tenantId: string) {
4445
.order('submission_date', { ascending: false })
4546
.limit(5),
4647

47-
supabase
48-
.from('lesson_completions')
49-
.select('lesson_id')
50-
.eq('user_id', userId),
48+
getLessonCompletions(supabase, userId),
5149

5250
supabase
5351
.from('exams')

app/api/chat/aristotle/restart/route.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { createClient } from '@/lib/supabase/server'
2-
import { getCurrentTenantId } from '@/lib/supabase/tenant'
1+
import { getApiAuthContext } from '@/lib/supabase/api-auth'
32
import { generateSessionSummary } from '@/lib/ai/aristotle-summary'
43

54
export async function POST(req: Request) {
6-
const supabase = await createClient()
7-
const tenantId = await getCurrentTenantId()
8-
const { data: { user } } = await supabase.auth.getUser()
9-
10-
if (!user) return new Response('Unauthorized', { status: 401 })
5+
const auth = await getApiAuthContext(req)
6+
if (!auth) return new Response('Unauthorized', { status: 401 })
7+
const { supabase, user, tenantId } = auth
118

129
const { courseId } = await req.json()
1310
if (!courseId) return new Response('Course ID is required', { status: 400 })

app/api/chat/aristotle/route.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createClient } from '@/lib/supabase/server'
2-
import { getCurrentTenantId } from '@/lib/supabase/tenant'
1+
import { getApiAuthContext } from '@/lib/supabase/api-auth'
32
import { AI_CONFIG, AI_MODELS } from '@/lib/ai/config'
43
import { buildAristotlePrompt } from '@/lib/ai/aristotle-prompt'
54
import { convertToModelMessages, stepCountIs, streamText } from 'ai'
@@ -10,11 +9,9 @@ export const maxDuration = 120
109
const SESSION_IDLE_MINUTES = 30
1110

1211
export async function POST(req: Request) {
13-
const supabase = await createClient()
14-
const tenantId = await getCurrentTenantId()
15-
const { data: { user } } = await supabase.auth.getUser()
16-
17-
if (!user) return new Response('Unauthorized', { status: 401 })
12+
const auth = await getApiAuthContext(req)
13+
if (!auth) return new Response('Unauthorized', { status: 401 })
14+
const { supabase, user, tenantId } = auth
1815

1916
const body = await req.json()
2017
const { messages, courseId, contextPage } = body

app/api/chat/exercises/student/restart/route.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { createClient } from '@/lib/supabase/server'
2-
import { getCurrentTenantId } from '@/lib/supabase/tenant'
1+
import { getApiAuthContext } from '@/lib/supabase/api-auth'
32
import { NextResponse } from 'next/server'
43

54
export async function POST(req: Request) {
65
try {
7-
const supabase = await createClient()
8-
const tenantId = await getCurrentTenantId()
9-
const { data: { user } } = await supabase.auth.getUser()
10-
11-
if (!user) {
6+
const auth = await getApiAuthContext(req)
7+
if (!auth) {
128
return new NextResponse('Unauthorized', { status: 401 })
139
}
10+
const { supabase, user, tenantId } = auth
1411

1512
const { exerciseId } = await req.json()
1613

app/api/chat/exercises/student/route.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createClient } from '@/lib/supabase/server'
2-
import { getCurrentTenantId } from '@/lib/supabase/tenant'
1+
import { getApiAuthContext } from '@/lib/supabase/api-auth'
32
import { AI_CONFIG, AI_MODELS } from '@/lib/ai/config'
43
import { PROMPTS } from '@/lib/ai/prompts'
54
import { createAITools } from '@/lib/ai/tools'
@@ -8,11 +7,9 @@ import { convertToModelMessages, stepCountIs, streamText } from 'ai'
87
export const maxDuration = 120
98

109
export async function POST(req: Request) {
11-
const supabase = await createClient()
12-
const tenantId = await getCurrentTenantId()
13-
const { data: { user } } = await supabase.auth.getUser()
14-
15-
if (!user) return new Response('Unauthorized', { status: 401 })
10+
const auth = await getApiAuthContext(req)
11+
if (!auth) return new Response('Unauthorized', { status: 401 })
12+
const { supabase, user, tenantId } = auth
1613

1714
const { messages, exerciseId } = await req.json()
1815
if (!exerciseId) return new Response('Exercise ID is required', { status: 400 })
@@ -29,13 +26,21 @@ export async function POST(req: Request) {
2926
// 2. Save user message
3027
const lastUserMessage = messages[messages.length - 1]
3128
if (lastUserMessage?.role === 'user') {
32-
await supabase.from('exercise_messages').insert({
33-
exercise_id: exerciseId,
34-
user_id: user.id,
35-
role: 'user',
36-
message: lastUserMessage.content,
37-
tenant_id: tenantId,
38-
})
29+
// AI SDK v6 messages carry text in `parts`, not `content`
30+
const messageText = lastUserMessage.parts
31+
?.filter((part: any) => part.type === 'text')
32+
.map((part: any) => part.text)
33+
.join(' ') || lastUserMessage.content || ''
34+
35+
if (messageText) {
36+
// exercise_messages has NO tenant_id column — sending it silently fails the insert.
37+
await supabase.from('exercise_messages').insert({
38+
exercise_id: exerciseId,
39+
user_id: user.id,
40+
role: 'user',
41+
message: messageText,
42+
})
43+
}
3944
}
4045

4146
// 3. Stream Response
@@ -51,7 +56,6 @@ export async function POST(req: Request) {
5156
user_id: user.id,
5257
role: 'assistant',
5358
message: event.text,
54-
tenant_id: tenantId,
5559
})
5660
},
5761
stopWhen: stepCountIs(AI_CONFIG.maxSteps),

app/api/chat/lesson-task/restart/route.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { createClient } from '@/lib/supabase/server'
2-
import { getCurrentTenantId } from '@/lib/supabase/tenant'
1+
import { getApiAuthContext } from '@/lib/supabase/api-auth'
32
import { NextResponse } from 'next/server'
43

54
export async function POST(req: Request) {
65
try {
7-
const supabase = await createClient()
8-
const tenantId = await getCurrentTenantId()
9-
const { data: { user } } = await supabase.auth.getUser()
10-
11-
if (!user) {
6+
const auth = await getApiAuthContext(req)
7+
if (!auth) {
128
return new NextResponse('Unauthorized', { status: 401 })
139
}
10+
const { supabase, user, tenantId } = auth
1411

1512
const { lessonId } = await req.json()
1613

0 commit comments

Comments
 (0)