Skip to content
Open
13 changes: 13 additions & 0 deletions src/web/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/web/app/page-login/login-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReadonlySet<LoginMethod>>(new Set());
Expand Down
31 changes: 31 additions & 0 deletions src/web/app/page-role-selection/role-selection-page.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div *tmIsLoading="isLoadingRoles()">
<div class="card bg-light mx-auto" style="max-width: 600px">
<div class="card-header bg-primary text-white">
<h1 class="mb-0 h3">Welcome to TEAMMATES!</h1>
</div>
<div class="card-body">
@if (hasRolePages()) {
<p>Choose a page to continue.</p>
<div style="margin-top: 20px">
@for (rolePage of rolePages(); track rolePage.url) {
<div class="d-sm-flex align-items-center justify-content-between border-top py-3">
<div class="mb-2 mb-sm-0">
<strong>{{ rolePage.role }}</strong>
</div>
<a class="btn btn-success" [routerLink]="rolePage.url">Continue</a>
</div>
}
</div>
} @else {
<p>You do not currently have access to any instructor or student pages in TEAMMATES.</p>
<p>
If you believe you should have access, contact your instructor or
<a href="mailto:{{ supportEmail() }}">contact TEAMMATES support</a>.
</p>
<div class="text-center" style="margin-top: 20px">
<a class="btn btn-primary" routerLink="/web/front/">Return to main page</a>
</div>
}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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<RoleSelectionPageComponent>;
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');
});
});
81 changes: 81 additions & 0 deletions src/web/app/page-role-selection/role-selection-page.component.ts
Original file line number Diff line number Diff line change
@@ -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<RolePage[]>([]);
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([]);
},
});
}
}
2 changes: 1 addition & 1 deletion src/web/app/page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading