Skip to content

Commit

Permalink
Development: Restrict course detail page access (#9834)
Browse files Browse the repository at this point in the history
  • Loading branch information
cremertim authored Dec 20, 2024
1 parent 8fafdee commit 794f57d
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 83 deletions.

This file was deleted.

99 changes: 99 additions & 0 deletions src/main/webapp/app/overview/course-overview-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable, of, switchMap } from 'rxjs';
import { CourseStorageService } from 'app/course/manage/course-storage.service';
import { CourseManagementService } from 'app/course/manage/course-management.service';
import { Course, isCommunicationEnabled } from 'app/entities/course.model';
import dayjs from 'dayjs/esm';
import { ArtemisServerDateService } from 'app/shared/server-date.service';
import { CourseOverviewRoutePath } from 'app/overview/courses-routing.module';

@Injectable({
providedIn: 'root',
})
export class CourseOverviewGuard implements CanActivate {
private courseStorageService = inject(CourseStorageService);
private courseManagementService = inject(CourseManagementService);
private router = inject(Router);
private serverDateService = inject(ArtemisServerDateService);

/**
* Check if the client can activate a course overview route.
* @return true if the client is allowed to access the route, false otherwise
*/
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const courseIdString = route.parent?.paramMap.get('courseId');
if (!courseIdString) {
return of(false);
}
const courseIdNumber = parseInt(courseIdString, 10);

const path = route.routeConfig?.path;
if (!path) {
return of(false);
}
//we need to load the course from the server to check if the user has access to the requested route. The course in the cache might not be sufficient (e.g. misses exams or lectures)
return this.courseManagementService.findOneForDashboard(courseIdNumber).pipe(
switchMap((res) => {
if (res.body) {
// Store course in cache
this.courseStorageService.updateCourse(res.body);
}
// Flatten the result to return Observable<boolean> directly
return this.handleReturn(this.courseStorageService.getCourse(courseIdNumber), path);
}),
);
}

handleReturn = (course?: Course, type?: string): Observable<boolean> => {
let hasAccess: boolean;
switch (type) {
// Should always be accessible
case CourseOverviewRoutePath.EXERCISES:
hasAccess = true;
break;
case CourseOverviewRoutePath.LECTURES:
hasAccess = !!course?.lectures;
break;
case CourseOverviewRoutePath.EXAMS:
hasAccess = this.hasVisibleExams(course);
break;
case CourseOverviewRoutePath.COMPETENCIES:
hasAccess = !!(course?.numberOfCompetencies || course?.numberOfPrerequisites);
break;
case CourseOverviewRoutePath.TUTORIAL_GROUPS:
hasAccess = !!course?.numberOfTutorialGroups;
break;
case CourseOverviewRoutePath.DASHBOARD:
hasAccess = course?.studentCourseAnalyticsDashboardEnabled ?? false;
break;
case CourseOverviewRoutePath.FAQ:
hasAccess = course?.faqEnabled ?? false;
break;
case CourseOverviewRoutePath.LEARNING_PATH:
hasAccess = course?.learningPathsEnabled ?? false;
break;
case CourseOverviewRoutePath.COMMUNICATION:
hasAccess = isCommunicationEnabled(course);
break;
default:
hasAccess = false;
}
if (!hasAccess) {
// Default route, redirect to exercises if the user does not have access to the requested route
this.router.navigate([`/courses/${course?.id}/exercises`]);
}
return of(hasAccess);
};

hasVisibleExams(course?: Course): boolean {
if (course?.exams) {
for (const exam of course.exams) {
if (exam.visibleDate && dayjs(exam.visibleDate).isBefore(this.serverDateService.now())) {
return true;
}
}
}
return false;
}
}
54 changes: 36 additions & 18 deletions src/main/webapp/app/overview/courses-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,23 @@ import { CourseTutorialGroupsComponent } from './course-tutorial-groups/course-t
import { CourseTutorialGroupDetailComponent } from './tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component';
import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component';
import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard';
import { CourseDashboardGuard } from 'app/overview/course-dashboard/course-dashboard-guard.service';
import { CourseArchiveComponent } from './course-archive/course-archive.component';
import { CourseOverviewGuard } from 'app/overview/course-overview-guard';

export enum CourseOverviewRoutePath {
DASHBOARD = 'dashboard',
EXERCISES = 'exercises',
EXAMS = 'exams',
COMPETENCIES = 'competencies',
TUTORIAL_GROUPS = 'tutorial-groups',
FAQ = 'faq',
LEARNING_PATH = 'learning-path',
LECTURES = 'lectures',
ENROLL = 'enroll',
ARCHIVE = 'archive',
STATISTICS = 'statistics',
COMMUNICATION = 'communication',
}

const routes: Routes = [
{
Expand All @@ -25,11 +40,11 @@ const routes: Routes = [
canActivate: [UserRouteAccessService],
},
{
path: 'enroll',
path: CourseOverviewRoutePath.ENROLL,
loadChildren: () => import('./course-registration/course-registration.module').then((m) => m.CourseRegistrationModule),
},
{
path: 'archive',
path: CourseOverviewRoutePath.ARCHIVE,
component: CourseArchiveComponent,
data: {
authorities: [Authority.USER],
Expand All @@ -54,7 +69,7 @@ const routes: Routes = [
canActivate: [UserRouteAccessService],
children: [
{
path: 'exercises',
path: CourseOverviewRoutePath.EXERCISES,
component: CourseExercisesComponent,
data: {
authorities: [Authority.USER],
Expand Down Expand Up @@ -128,15 +143,15 @@ const routes: Routes = [
},

{
path: 'lectures',
path: CourseOverviewRoutePath.LECTURES,
component: CourseLecturesComponent,
data: {
authorities: [Authority.USER],
pageTitle: 'overview.lectures',
hasSidebar: true,
showRefreshButton: true,
},
canActivate: [UserRouteAccessService],
canActivate: [UserRouteAccessService, CourseOverviewGuard],
children: [
{
path: ':lectureId',
Expand All @@ -152,7 +167,7 @@ const routes: Routes = [
],
},
{
path: 'statistics',
path: CourseOverviewRoutePath.STATISTICS,
loadChildren: () => import('./course-statistics/course-statistics.module').then((m) => m.CourseStatisticsModule),
data: {
authorities: [Authority.USER],
Expand All @@ -161,12 +176,13 @@ const routes: Routes = [
},
},
{
path: 'competencies',
path: CourseOverviewRoutePath.COMPETENCIES,
data: {
authorities: [Authority.USER],
pageTitle: 'overview.competencies',
showRefreshButton: true,
},
canActivate: [CourseOverviewGuard],
children: [
{
path: '',
Expand All @@ -179,26 +195,27 @@ const routes: Routes = [
],
},
{
path: 'dashboard',
path: CourseOverviewRoutePath.DASHBOARD,
loadChildren: () => import('./course-dashboard/course-dashboard.module').then((m) => m.CourseDashboardModule),
data: {
authorities: [Authority.USER],
pageTitle: 'overview.dashboard',
},
canActivate: [UserRouteAccessService, CourseDashboardGuard],
canActivate: [UserRouteAccessService, CourseOverviewGuard],
},
{
path: 'learning-path',
path: CourseOverviewRoutePath.LEARNING_PATH,
loadComponent: () =>
import('app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component').then((c) => c.LearningPathStudentPageComponent),
data: {
authorities: [Authority.USER],
pageTitle: 'overview.learningPath',
showRefreshButton: true,
},
canActivate: [CourseOverviewGuard],
},
{
path: 'communication',
path: CourseOverviewRoutePath.COMMUNICATION,
loadChildren: () => import('./course-conversations/course-conversations.module').then((m) => m.CourseConversationsModule),
data: {
authorities: [Authority.USER],
Expand All @@ -208,15 +225,15 @@ const routes: Routes = [
},
},
{
path: 'tutorial-groups',
path: CourseOverviewRoutePath.TUTORIAL_GROUPS,
component: CourseTutorialGroupsComponent,
data: {
authorities: [Authority.USER],
pageTitle: 'overview.tutorialGroups',
hasSidebar: true,
showRefreshButton: true,
},
canActivate: [UserRouteAccessService],
canActivate: [UserRouteAccessService, CourseOverviewGuard],
children: [
{
path: ':tutorialGroupId',
Expand All @@ -233,15 +250,15 @@ const routes: Routes = [
],
},
{
path: 'exams',
path: CourseOverviewRoutePath.EXAMS,
component: CourseExamsComponent,
data: {
authorities: [Authority.USER],
pageTitle: 'overview.exams',
hasSidebar: true,
showRefreshButton: true,
},
canActivate: [UserRouteAccessService],
canActivate: [UserRouteAccessService, CourseOverviewGuard],
children: [
{
path: ':examId',
Expand All @@ -267,18 +284,19 @@ const routes: Routes = [
},
},
{
path: 'faq',
path: CourseOverviewRoutePath.FAQ,
loadComponent: () => import('../overview/course-faq/course-faq.component').then((m) => m.CourseFaqComponent),
data: {
authorities: [Authority.USER],
pageTitle: 'overview.faq',
hasSidebar: false,
showRefreshButton: true,
},
canActivate: [CourseOverviewGuard],
},
{
path: '',
redirectTo: 'dashboard', // dashboard will redirect to exercises if not enabled
redirectTo: CourseOverviewRoutePath.DASHBOARD, // dashboard will redirect to exercises if not enabled
pathMatch: 'full',
},
],
Expand Down
Loading

0 comments on commit 794f57d

Please sign in to comment.