diff --git a/src/web/app/app.routes.ts b/src/web/app/app.routes.ts index 2f38c199afa..9540187fe45 100644 --- a/src/web/app/app.routes.ts +++ b/src/web/app/app.routes.ts @@ -115,6 +115,19 @@ const routes: Routes = [ canActivate: [RoleGuard], canActivateChild: [RoleGuard], }, + { + path: 'role-selection', + component: PageComponent, + children: [ + { + path: '', + loadComponent: () => + import('./page-role-selection/role-selection-page.component').then((m) => m.RoleSelectionPageComponent), + }, + ], + canActivate: [RoleGuard], + canActivateChild: [RoleGuard], + }, { path: 'login', component: PageComponent, diff --git a/src/web/app/page-login/login-page.component.ts b/src/web/app/page-login/login-page.component.ts index fd32ac3a1b3..86bcb67592a 100644 --- a/src/web/app/page-login/login-page.component.ts +++ b/src/web/app/page-login/login-page.component.ts @@ -17,7 +17,7 @@ export class LoginPageComponent implements OnInit { private readonly configService = inject(ConfigService); private readonly statusMessageService = inject(StatusMessageService); - readonly nextUrl = input('/'); + readonly nextUrl = input('/web/role-selection'); readonly isLoadingLoginMethods = signal(true); readonly loginMethods = signal>(new Set()); diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html new file mode 100644 index 00000000000..67abdf3d13d --- /dev/null +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -0,0 +1,31 @@ +
+
+
+

Welcome to TEAMMATES!

+
+
+ @if (hasRolePages()) { +

Choose a page to continue.

+
+ @for (rolePage of rolePages(); track rolePage.url) { +
+
+ {{ rolePage.role }} +
+ Continue +
+ } +
+ } @else { +

You do not currently have access to any instructor or student pages in TEAMMATES.

+

+ If you believe you should have access, contact your instructor or + contact TEAMMATES support. +

+ + } +
+
+
diff --git a/src/web/app/page-role-selection/role-selection-page.component.spec.ts b/src/web/app/page-role-selection/role-selection-page.component.spec.ts new file mode 100644 index 00000000000..5dbc597f04b --- /dev/null +++ b/src/web/app/page-role-selection/role-selection-page.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; +import { AuthInfo } from '../../types/api-output'; +import { RoleSelectionPageComponent } from './role-selection-page.component'; + +const authInfoFor = (roles: { + isStudent?: boolean; + isInstructor?: boolean; + isAdmin?: boolean; + isMaintainer?: boolean; +}): AuthInfo => ({ + masquerade: false, + user: { + accountId: 'account-id', + accountEmail: 'user@example.com', + isStudent: !!roles.isStudent, + isInstructor: !!roles.isInstructor, + isAdmin: !!roles.isAdmin, + isMaintainer: !!roles.isMaintainer, + }, +}); + +describe('RoleSelectionPageComponent', () => { + let fixture: ComponentFixture; + let authService: AuthService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + fixture = TestBed.createComponent(RoleSelectionPageComponent); + authService = TestBed.inject(AuthService); + }); + + it('should show available role pages', () => { + vi.spyOn(authService, 'getAuthUser').mockReturnValue(of(authInfoFor({ isStudent: true, isInstructor: true }))); + + fixture.detectChanges(); + + const pageText = fixture.nativeElement.textContent; + expect(pageText).toContain('Student'); + expect(pageText).toContain('Instructor'); + expect(pageText).not.toContain('Return to main page'); + }); + + it('should prompt users with no roles to return to the main page', () => { + vi.spyOn(authService, 'getAuthUser').mockReturnValue(of(authInfoFor({}))); + + fixture.detectChanges(); + + const pageText = fixture.nativeElement.textContent; + expect(pageText).toContain('You do not currently have access to any instructor or student pages in TEAMMATES.'); + expect(pageText).toContain('Return to main page'); + }); +}); diff --git a/src/web/app/page-role-selection/role-selection-page.component.ts b/src/web/app/page-role-selection/role-selection-page.component.ts new file mode 100644 index 00000000000..afc09e9018c --- /dev/null +++ b/src/web/app/page-role-selection/role-selection-page.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { map, finalize } from 'rxjs/operators'; +import { AuthService } from '../../services/auth.service'; +import { AuthInfo } from '../../types/api-output'; +import { LoadingSpinnerDirective } from '../components/loading-spinner/loading-spinner.directive'; +import { ConfigService } from '../../services/config.service'; +import { toSignal } from '@angular/core/rxjs-interop'; + +interface RolePage { + role: string; + url: string; +} + +@Component({ + selector: 'tm-role-selection-page', + templateUrl: './role-selection-page.component.html', + imports: [RouterLink, LoadingSpinnerDirective], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoleSelectionPageComponent implements OnInit { + private readonly authService = inject(AuthService); + private readonly configService = inject(ConfigService); + + readonly isLoadingRoles = signal(true); + readonly rolePages = signal([]); + readonly hasRolePages = computed(() => this.rolePages().length > 0); + readonly supportEmail = toSignal(this.configService.getConfig().pipe(map((config) => config.supportEmail)), { + initialValue: '', + }); + + ngOnInit(): void { + this.isLoadingRoles.set(true); + this.authService + .getAuthUser() + .pipe( + finalize(() => { + this.isLoadingRoles.set(false); + }), + ) + .subscribe({ + next: (authInfo: AuthInfo) => { + const user = authInfo.user; + const rolePages: RolePage[] = []; + + if (user?.isStudent) { + rolePages.push({ + role: 'Student', + url: '/web/student', + }); + } + + if (user?.isInstructor) { + rolePages.push({ + role: 'Instructor', + url: '/web/instructor', + }); + } + + if (user?.isAdmin) { + rolePages.push({ + role: 'Admin', + url: '/web/admin', + }); + } + + if (user?.isMaintainer) { + rolePages.push({ + role: 'Maintainer', + url: '/web/maintainer', + }); + } + + this.rolePages.set(rolePages); + }, + error: () => { + this.rolePages.set([]); + }, + }); + } +} diff --git a/src/web/app/page.component.html b/src/web/app/page.component.html index a2167435ccd..f354ef57bec 100644 --- a/src/web/app/page.component.html +++ b/src/web/app/page.component.html @@ -69,7 +69,7 @@ class="nav-link" id="login-btn" routerLink="/web/login" - [queryParams]="{ nextUrl: '/' }" + [queryParams]="{ nextUrl: '/web/role-selection' }" (click)="toggleCollapse(); closeModal()" > Sign in