Skip to content

Commit e05d03b

Browse files
Merge pull request #259 from guillermoscript/feature/mcp
Feature/mcp
2 parents aa0bc7e + 0644482 commit e05d03b

7 files changed

Lines changed: 618 additions & 4 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getUserRole } from '@/lib/supabase/get-user-role'
2+
import { redirect } from 'next/navigation'
3+
import { listMcpTokens } from '@/app/actions/mcp-tokens'
4+
import ApiTokensPage from '@/components/dashboard/api-tokens-page'
5+
6+
export default async function AdminApiTokensPage() {
7+
const role = await getUserRole()
8+
if (role !== 'admin') {
9+
redirect('/dashboard/admin')
10+
}
11+
12+
const { data: tokens } = await listMcpTokens()
13+
const domain = process.env.NEXT_PUBLIC_PLATFORM_DOMAIN || 'localhost:3000'
14+
15+
return (
16+
<div className="p-6 lg:p-8">
17+
<ApiTokensPage tokens={tokens ?? []} domain={domain} />
18+
</div>
19+
)
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getUserRole } from '@/lib/supabase/get-user-role'
2+
import { redirect } from 'next/navigation'
3+
import { listMcpTokens } from '@/app/actions/mcp-tokens'
4+
import ApiTokensPage from '@/components/dashboard/api-tokens-page'
5+
6+
export default async function TeacherApiTokensPage() {
7+
const role = await getUserRole()
8+
if (role !== 'teacher') {
9+
redirect('/dashboard/teacher')
10+
}
11+
12+
const { data: tokens } = await listMcpTokens()
13+
const domain = process.env.NEXT_PUBLIC_PLATFORM_DOMAIN || 'localhost:3000'
14+
15+
return (
16+
<div className="p-6 lg:p-8">
17+
<ApiTokensPage tokens={tokens ?? []} domain={domain} />
18+
</div>
19+
)
20+
}

app/actions/mcp-tokens.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use server'
2+
3+
import { createClient } from '@/lib/supabase/server'
4+
import { getUserRole } from '@/lib/supabase/get-user-role'
5+
import { revalidatePath } from 'next/cache'
6+
import { randomBytes, createHash } from 'crypto'
7+
8+
export interface McpToken {
9+
id: number
10+
name: string
11+
created_at: string
12+
last_used_at: string | null
13+
expires_at: string | null
14+
is_active: boolean
15+
}
16+
17+
function hashToken(token: string): string {
18+
return createHash('sha256').update(token).digest('hex')
19+
}
20+
21+
export async function createMcpToken(name: string, expiresInDays?: number) {
22+
const role = await getUserRole()
23+
if (role !== 'teacher' && role !== 'admin') {
24+
throw new Error('Only teachers and admins can create API tokens')
25+
}
26+
27+
const supabase = await createClient()
28+
const { data: { user } } = await supabase.auth.getUser()
29+
if (!user) throw new Error('Not authenticated')
30+
31+
const rawToken = randomBytes(32).toString('hex')
32+
const tokenHash = hashToken(rawToken)
33+
34+
const expiresAt = expiresInDays
35+
? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString()
36+
: null
37+
38+
const { error } = await supabase
39+
.from('mcp_api_tokens')
40+
.insert({
41+
user_id: user.id,
42+
token_hash: tokenHash,
43+
name,
44+
expires_at: expiresAt,
45+
is_active: true,
46+
})
47+
48+
if (error) throw new Error(error.message)
49+
50+
revalidatePath('/dashboard/admin/api-tokens')
51+
revalidatePath('/dashboard/teacher/api-tokens')
52+
53+
return { token: rawToken }
54+
}
55+
56+
export async function listMcpTokens(): Promise<{ data: McpToken[] | null; error: string | null }> {
57+
const role = await getUserRole()
58+
if (role !== 'teacher' && role !== 'admin') {
59+
return { data: null, error: 'Unauthorized' }
60+
}
61+
62+
const supabase = await createClient()
63+
const { data: { user } } = await supabase.auth.getUser()
64+
if (!user) return { data: null, error: 'Not authenticated' }
65+
66+
const { data, error } = await supabase
67+
.from('mcp_api_tokens')
68+
.select('id, name, created_at, last_used_at, expires_at, is_active')
69+
.eq('user_id', user.id)
70+
.order('created_at', { ascending: false })
71+
72+
if (error) return { data: null, error: error.message }
73+
return { data, error: null }
74+
}
75+
76+
export async function revokeMcpToken(tokenId: number) {
77+
const role = await getUserRole()
78+
if (role !== 'teacher' && role !== 'admin') {
79+
throw new Error('Unauthorized')
80+
}
81+
82+
const supabase = await createClient()
83+
84+
const { error } = await supabase
85+
.from('mcp_api_tokens')
86+
.update({ is_active: false })
87+
.eq('id', tokenId)
88+
89+
if (error) throw new Error(error.message)
90+
91+
revalidatePath('/dashboard/admin/api-tokens')
92+
revalidatePath('/dashboard/teacher/api-tokens')
93+
}
94+
95+
export async function deleteMcpToken(tokenId: number) {
96+
const role = await getUserRole()
97+
if (role !== 'teacher' && role !== 'admin') {
98+
throw new Error('Unauthorized')
99+
}
100+
101+
const supabase = await createClient()
102+
103+
const { error } = await supabase
104+
.from('mcp_api_tokens')
105+
.delete()
106+
.eq('id', tokenId)
107+
108+
if (error) throw new Error(error.message)
109+
110+
revalidatePath('/dashboard/admin/api-tokens')
111+
revalidatePath('/dashboard/teacher/api-tokens')
112+
}

components/app-sidebar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
IconSearch,
2525
IconSettings,
2626
IconSparkles,
27+
IconKey,
2728
IconTrophy,
2829
IconUsers,
2930
} from "@tabler/icons-react"
@@ -82,6 +83,7 @@ export function AppSidebar({ userRole, ...props }: AppSidebarProps) {
8283
{ title: t('transactions'), href: "/dashboard/admin/transactions", icon: IconCurrencyDollar },
8384
{ title: t('billing'), href: "/dashboard/admin/billing", icon: IconCreditCard },
8485
{ title: 'Landing Page', href: "/dashboard/admin/landing-page", icon: IconLayout },
86+
{ title: t('apiTokens'), href: "/dashboard/admin/api-tokens", icon: IconKey },
8587
],
8688
},
8789
teacher: {
@@ -91,6 +93,7 @@ export function AppSidebar({ userRole, ...props }: AppSidebarProps) {
9193
content: [
9294
{ title: t('myCourses'), href: "/dashboard/teacher/courses", icon: IconBook },
9395
{ title: t('createCourse'), href: "/dashboard/teacher/courses/new", icon: IconPlus },
96+
{ title: t('apiTokens'), href: "/dashboard/teacher/api-tokens", icon: IconKey },
9497
],
9598
business: [
9699
{ title: t('revenue'), href: "/dashboard/teacher/revenue", icon: IconCurrencyDollar },

0 commit comments

Comments
 (0)