[dyad] Rol erişim davranışlarını test et
This commit is contained in:
1 parent
d59cbd4989
commit
fbf6424286
3 files changed
+108
-1
No files matched your search
@@ -121,6 +121,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useAppStore, type ViewName } from '@/lib/store';
|
import { useAppStore, type ViewName } from '@/lib/store';
|
||||||
|
import { canAccessView, filterSectionsForRole, getFallbackView } from '@/lib/role-access';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import { fetchSchoolYears, fetchStudents, fetchClasses, fetchDBNotifications, markAllNotificationsRead, markSingleNotificationRead, type SchoolYear, type Student, type ClassGroup, type DBNotification, type DBNotificationData } from '@/lib/api';
|
import { fetchSchoolYears, fetchStudents, fetchClasses, fetchDBNotifications, markAllNotificationsRead, markSingleNotificationRead, type SchoolYear, type Student, type ClassGroup, type DBNotification, type DBNotificationData } from '@/lib/api';
|
||||||
import { apiGet } from '@/lib/api';
|
import { apiGet } from '@/lib/api';
|
||||||
@@ -571,6 +572,16 @@ export default function AppLayout() {
|
|||||||
const isStudent = currentUser?.role === 'STUDENT';
|
const isStudent = currentUser?.role === 'STUDENT';
|
||||||
const isParent = currentUser?.role === 'PARENT';
|
const isParent = currentUser?.role === 'PARENT';
|
||||||
const activeNavSections = isStudent ? studentNavSections : isParent ? parentNavSections : navSections;
|
const activeNavSections = isStudent ? studentNavSections : isParent ? parentNavSections : navSections;
|
||||||
|
const visibleNavSections = useMemo(
|
||||||
|
() => filterSectionsForRole(currentUser?.role, activeNavSections),
|
||||||
|
[activeNavSections, currentUser?.role],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser && !canAccessView(currentUser.role, currentView)) {
|
||||||
|
setCurrentView(getFallbackView(currentUser.role));
|
||||||
|
}
|
||||||
|
}, [currentUser, currentView, setCurrentView]);
|
||||||
|
|
||||||
const isDemoUser = currentUser?.isDemo === true || (currentUser?.email?.endsWith('@competencetrack.org') && currentUser?.email?.startsWith('demo'));
|
const isDemoUser = currentUser?.isDemo === true || (currentUser?.email?.endsWith('@competencetrack.org') && currentUser?.email?.startsWith('demo'));
|
||||||
|
|
||||||
@@ -1016,7 +1027,7 @@ export default function AppLayout() {
|
|||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
{activeNavSections.map((section) => (
|
{visibleNavSections.map((section) => (
|
||||||
<SidebarGroup key={section.id}>
|
<SidebarGroup key={section.id}>
|
||||||
<SidebarGroupLabel className="text-emerald-600/70 dark:text-emerald-400/50 text-xs font-semibold uppercase tracking-wider inline-flex items-center gap-1.5">
|
<SidebarGroupLabel className="text-emerald-600/70 dark:text-emerald-400/50 text-xs font-semibold uppercase tracking-wider inline-flex items-center gap-1.5">
|
||||||
{section.id === 'analysis' && <TrendingUp className="h-3 w-3" />}
|
{section.id === 'analysis' && <TrendingUp className="h-3 w-3" />}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
import { canAccessView, filterSectionsForRole, filterViewsForRole, getFallbackView } from './role-access';
|
||||||
|
|
||||||
|
const mixedItems = [
|
||||||
|
{ key: 'dashboard' as const },
|
||||||
|
{ key: 'students' as const },
|
||||||
|
{ key: 'classes' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('role access', () => {
|
||||||
|
test('hides teacher-inaccessible views from a mixed menu', () => {
|
||||||
|
expect(filterViewsForRole('TEACHER', mixedItems).map(({ key }) => key)).toEqual(['dashboard', 'classes']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes empty sections for parent, student, and teacher', () => {
|
||||||
|
const sections = [
|
||||||
|
{ id: 'visible', items: [{ key: 'dashboard' as const }] },
|
||||||
|
{ id: 'hidden', items: [{ key: 'students' as const }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const role of ['PARENT', 'STUDENT', 'TEACHER'] as const) {
|
||||||
|
expect(filterSectionsForRole(role, sections).map(({ id }) => id)).toEqual(['visible']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to dashboard for an inaccessible active view', () => {
|
||||||
|
expect(canAccessView('PARENT', 'students')).toBeFalse();
|
||||||
|
expect(getFallbackView('PARENT')).toBe('dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps every view available to administrators', () => {
|
||||||
|
for (const view of mixedItems.map(({ key }) => key)) {
|
||||||
|
expect(canAccessView('SCHOOL_ADMIN', view)).toBeTrue();
|
||||||
|
expect(canAccessView('SUPER_ADMIN', view)).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { ViewName } from '@/lib/store';
|
||||||
|
|
||||||
|
export type AppRole = 'PARENT' | 'STUDENT' | 'TEACHER' | 'VICE_PRINCIPAL' | 'SCHOOL_ADMIN' | 'SUPER_ADMIN' | 'DPO';
|
||||||
|
|
||||||
|
const parentViews: ViewName[] = [
|
||||||
|
'dashboard', 'parent-portal', 'parents', 'grading', 'attendance', 'calendar',
|
||||||
|
'communication', 'illness', 'report-cards', 'school-events', 'school-newsletter',
|
||||||
|
'notification-center', 'announcements', 'settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
const studentViews: ViewName[] = [
|
||||||
|
'dashboard', 'student-portal', 'notebooks', 'flower', 'grading', 'grade-analytics',
|
||||||
|
'homework', 'attendance', 'portfolio', 'calendar', 'competitions', 'subjects',
|
||||||
|
'illness', 'communication', 'counseling', 'ai-tests', 'exam-calendar',
|
||||||
|
'peer-assessment', 'report-cards', 'student-achievements', 'student-study-planner',
|
||||||
|
'student-wellness', 'student-career', 'resources', 'school-library', 'school-events',
|
||||||
|
'school-newsletter', 'school-transport', 'notification-center', 'announcements', 'settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
const teacherViews: ViewName[] = [
|
||||||
|
'dashboard', 'classes', 'competencies', 'progress', 'flower', 'assessments', 'grading',
|
||||||
|
'reports', 'settings', 'student-detail', 'matrix', 'attendance', 'calendar', 'lesson-plans',
|
||||||
|
'parents', 'behavior', 'coverage', 'rubrics', 'comments', 'notebooks', 'drawing', 'homework',
|
||||||
|
'portfolio', 'timetable', 'resources', 'competitions', 'subjects', 'illness', 'communication',
|
||||||
|
'counseling', 'ai-tests', 'notification-center', 'announcements', 'tablet-grading',
|
||||||
|
'exam-calendar', 'peer-assessment', 'seating-chart', 'school-library', 'report-cards',
|
||||||
|
'student-wellness', 'student-career', 'school-events', 'school-newsletter',
|
||||||
|
];
|
||||||
|
|
||||||
|
const vicePrincipalViews: ViewName[] = [
|
||||||
|
...teacherViews,
|
||||||
|
'analytics', 'disciplinary', 'school-transport', 'substitute-teacher',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function canAccessView(role: string | undefined, view: ViewName): boolean {
|
||||||
|
if (role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN') return true;
|
||||||
|
const allowed = role === 'PARENT' ? parentViews
|
||||||
|
: role === 'STUDENT' ? studentViews
|
||||||
|
: role === 'VICE_PRINCIPAL' ? vicePrincipalViews
|
||||||
|
: role === 'DPO' ? ['dashboard', 'settings', 'reports']
|
||||||
|
: teacherViews;
|
||||||
|
return allowed.includes(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterViewsForRole<T extends { key: ViewName }>(role: string | undefined, items: T[]): T[] {
|
||||||
|
return items.filter((item) => canAccessView(role, item.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterSectionsForRole<T extends { items: { key: ViewName }[] }>(role: string | undefined, sections: T[]): T[] {
|
||||||
|
return sections
|
||||||
|
.map((section) => ({ ...section, items: filterViewsForRole(role, section.items) }))
|
||||||
|
.filter((section) => section.items.length > 0) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFallbackView(role: string | undefined): ViewName {
|
||||||
|
return role === 'PARENT' || role === 'STUDENT' || role === 'TEACHER' || role === 'VICE_PRINCIPAL' || role === 'SCHOOL_ADMIN' || role === 'SUPER_ADMIN' || role === 'DPO'
|
||||||
|
? 'dashboard'
|
||||||
|
: 'dashboard';
|
||||||
|
}
|
||||||
Reference in new issue
Block a user