Skip to content

Commit eaebae3

Browse files
committed
feat(access-control): Add role-based route protection hook and integrate into layouts
- Create useRoleProtection hook to enforce role-based access control for student and company member routes - Integrate role protection into company dashboard layout to restrict access to company members only - Integrate role protection into protected layout to restrict access to students only - Add loading state handling during role verification to prevent premature redirects - Redirect unauthorized users to appropriate dashboard based on their role (students to /protected, company members to /dashboard/company) - Query company_members table to determine user role and authorization status - Prevents role confusion and ensures users can only access routes appropriate for their account type
1 parent 4ee5f3b commit eaebae3

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-4
lines changed

app/dashboard/company/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
CreditCard,
1818
} from 'lucide-react'
1919
import { useAuth } from '@/lib/hooks/useAuth'
20+
import { useRoleProtection } from '@/lib/hooks/useRoleProtection'
2021

2122
export type SidebarGroupType = {
2223
title: string
@@ -33,6 +34,7 @@ export default function CompanyDashboardLayout({
3334
children: React.ReactNode
3435
}) {
3536
const { user, loading, error } = useAuth()
37+
const { isChecking, isAuthorized } = useRoleProtection('company_member')
3638
const params = useParams()
3739
const companySlug = params?.slug as string | undefined
3840

@@ -51,7 +53,7 @@ export default function CompanyDashboardLayout({
5153
)
5254
}
5355

54-
if (loading) {
56+
if (loading || isChecking) {
5557
return (
5658
<div className="flex items-center justify-center min-h-screen bg-black">
5759
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
@@ -75,7 +77,7 @@ export default function CompanyDashboardLayout({
7577
)
7678
}
7779

78-
if (!user) {
80+
if (!user || !isAuthorized) {
7981
return (
8082
<div className="flex items-center justify-center min-h-screen px-4 bg-black">
8183
<div className="text-center max-w-md">

app/protected/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Star,
2525
} from "lucide-react"
2626
import { useAuth } from "@/lib/hooks/useAuth"
27+
import { useRoleProtection } from "@/lib/hooks/useRoleProtection"
2728

2829
export type SidebarGroupType = {
2930
title: string;
@@ -210,16 +211,17 @@ const sidebarItems: SidebarGroupType[] = [
210211

211212
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
212213
const { user, loading } = useAuth()
214+
const { isChecking, isAuthorized } = useRoleProtection('student')
213215

214-
if (loading) {
216+
if (loading || isChecking) {
215217
return (
216218
<div className="flex items-center justify-center min-h-screen">
217219
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
218220
</div>
219221
)
220222
}
221223

222-
if (!user) {
224+
if (!user || !isAuthorized) {
223225
return (
224226
<div className="flex items-center justify-center min-h-screen px-4">
225227
<div className="text-center max-w-md">

lib/hooks/useRoleProtection.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { createClient } from '@/lib/supabase/client'
6+
7+
export function useRoleProtection(requiredRole: 'student' | 'company_member') {
8+
const router = useRouter()
9+
const [isChecking, setIsChecking] = useState(true)
10+
const [isAuthorized, setIsAuthorized] = useState(false)
11+
12+
useEffect(() => {
13+
async function checkRole() {
14+
try {
15+
const supabase = createClient()
16+
const { data: { user } } = await supabase.auth.getUser()
17+
18+
if (!user) {
19+
router.push('/auth/signin')
20+
return
21+
}
22+
23+
// Check if user is a company member (FIXED: changed from 'company_users' to 'company_members')
24+
const { data: companyMembership } = await supabase
25+
.from('company_members')
26+
.select('id')
27+
.eq('user_id', user.id)
28+
.maybeSingle()
29+
30+
const isCompanyMember = !!companyMembership
31+
32+
// Determine if user has the required role
33+
if (requiredRole === 'company_member') {
34+
if (!isCompanyMember) {
35+
// User is a student trying to access company routes
36+
router.push('/protected')
37+
return
38+
}
39+
} else if (requiredRole === 'student') {
40+
if (isCompanyMember) {
41+
// User is a company member trying to access student routes
42+
router.push('/dashboard/company')
43+
return
44+
}
45+
}
46+
47+
setIsAuthorized(true)
48+
} catch (error) {
49+
console.error('Error checking role:', error)
50+
router.push('/auth/signin')
51+
} finally {
52+
setIsChecking(false)
53+
}
54+
}
55+
56+
checkRole()
57+
}, [requiredRole, router])
58+
59+
return { isChecking, isAuthorized }
60+
}

0 commit comments

Comments
 (0)