Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

class AttendanceEventController extends Controller
{
public function __construct() {
$this->middleware('can:list attendance events')->only(['index', 'show']);
}

public function index(Request $request) {
$request->validate([
'student_id' => 'exists:students,id',
Expand Down
1 change: 1 addition & 0 deletions attendance-api/app/Http/Controllers/StudentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
class StudentController extends Controller
{
public function __construct() {
$this->middleware('can:list students')->only(['index', 'show']);
$this->middleware('can:add students')->only('store');
$this->middleware('can:modify students')->only('update');
$this->middleware('can:remove students')->only('destroy');
Expand Down
17 changes: 17 additions & 0 deletions attendance-api/database/seeders/RolesSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ public function run()

app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

Permission::create(['name' => 'list students']);
Permission::create(['name' => 'add students']);
Permission::create(['name' => 'modify students']);
Permission::create(['name' => 'remove students']);
Permission::create(['name' => 'view student images']);
Permission::create(['name' => 'modify student images']);

Permission::create(['name' => 'list attendance events']);
Permission::create(['name' => 'student check in']);
Permission::create(['name' => 'student check out']);
Permission::create(['name' => 'undo attendance event']);
Expand All @@ -44,11 +46,13 @@ public function run()

Role::create(['name' => 'mentor'])
->givePermissionTo([
'list students',
'add students',
'view student images',
'modify students',
'modify student images',
'remove students',
'list attendance events',
'student check in',
'student check out',
'undo attendance event',
Expand All @@ -64,6 +68,8 @@ public function run()

Role::create(['name' => 'student-lead'])
->givePermissionTo([
'list students',
'list attendance events',
'student check in',
'view student images',
'undo attendance event',
Expand All @@ -74,6 +80,17 @@ public function run()
'view stats'
]);

Role::create(['name' => 'read-only'])
->givePermissionTo([
'list students',
'list attendance events',
'view student images',
'list users',
'list roles',
'list meeting events',
'view stats'
]);

// Here I hard-code the initial admin user (who will then add the rest of the admins)
// It'd probably be best to get this from the app config or the .env environment
if(config('app.debug', false)) {
Expand Down
9 changes: 5 additions & 4 deletions attendance-api/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use App\Http\Controllers\PollController;
use App\Http\Controllers\StudentProfileImageController;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

/*
|--------------------------------------------------------------------------
Expand Down Expand Up @@ -47,14 +48,14 @@
'index', 'show', 'update'
]);

Route::middleware('can:list roles')->get('roles', function() {
return Role::all()->pluck('name');
});

Route::get('reports/list-meetings', [ReportController::class, 'listMeetings']);
Route::get('reports/meeting-attendance', [ReportController::class, 'meetingAttendance']);
Route::get('poll', [PollController::class, 'poll']);

Route::get('roles', fn () => collect([
"roles" => Role::all()->map(fn ($role) => ['name'=>$role->name, 'permissions'=>$role->permissions->pluck("name")]),
"permissions" => Permission::all()->pluck("name")
]));
});

Route::get('info', fn () => ['git_hash' => config('app.git_hash')]);
Expand Down
35 changes: 19 additions & 16 deletions attendance-web/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LoginComponent } from './components/login/login.component';
import { UpdateOrCreateStudentComponent } from './components/students/update-or-create-student/update-or-create-student.component';
import { MustBeLoggedInGuard } from './guards/must-be-logged-in.guard';
import { MustNotBeLoggedInGuard } from './guards/must-not-be-logged-in.guard';
import { MustHaveRoleGuard } from './guards/must-have-role.guard';
import { MustHavePermissionGuard } from './guards/must-have-role.guard';
import { StudentsComponent } from './components/students/students.component';
import { ListStudentsComponent } from './components/students/list-students/list-students.component';
import { ImportStudentsComponent } from './components/students/import-students/import-students.component';
Expand All @@ -22,24 +22,27 @@ import { MeetingAttendanceReportComponent } from './components/reports/meeting-a

const routes: Routes = [
{ path: 'students', component: StudentsComponent,
canActivate: [MustBeLoggedInGuard, MustHaveRoleGuard],
data: { roleOptions: ['mentor', 'student-lead']},
canActivate: [MustBeLoggedInGuard, MustHavePermissionGuard],
data: { permissions: ['list students', 'view student images']},
children: [
{path: '', redirectTo: 'list', pathMatch: 'full'},
{path: 'detail/:studentId', component: ShowStudentComponent},
{ path: 'edit/:studentId', component: UpdateOrCreateStudentComponent,
canActivate: [MustHaveRoleGuard],
data: { roleOptions: ['mentor'] }},
canActivate: [MustHavePermissionGuard],
data: { permissions: ['modify students', 'modify student images', 'remove students'] }},
{path: 'list', component: ListStudentsComponent},
{path: 'add', component: UpdateOrCreateStudentComponent,
canActivate: [MustHaveRoleGuard],
data: { roleOptions: ['mentor'] }},
{path: 'import', component: ImportStudentsComponent}
canActivate: [MustHavePermissionGuard],
data: { permissions: ['add students', 'modify student images'] }},
{path: 'import', component: ImportStudentsComponent,
canActivate: [MustHavePermissionGuard],
data: { permissions: ['add students', 'modify student images'] }
}
]
},
{ path: 'reports', component: ReportsComponent,
canActivate: [MustBeLoggedInGuard, MustHaveRoleGuard],
data: { roleOptions: ['mentor', 'student-lead']},
canActivate: [MustBeLoggedInGuard, MustHavePermissionGuard],
data: { permissions: ['list students', 'view stats'] },
children: [
{ path: '', redirectTo: 'meetings', pathMatch: 'full' },
{ path: 'meetings', component: MeetingsReportComponent },
Expand All @@ -50,19 +53,19 @@ const routes: Routes = [
]
},
{ path: 'meetings', component: MeetingEventsComponent,
canActivate: [MustBeLoggedInGuard, MustHaveRoleGuard],
data: { roleOptions: ['mentor', 'student-lead']}
canActivate: [MustBeLoggedInGuard, MustHavePermissionGuard],
data: { permissions: ['list meeting events', 'add meeting events']}
},
{ path: 'users', component: ElevateUsersComponent, canActivate: [MustBeLoggedInGuard, MustHaveRoleGuard],
data: { roleOptions: ['mentor'] }},
{ path: 'users', component: ElevateUsersComponent, canActivate: [MustBeLoggedInGuard, MustHavePermissionGuard],
data: { permissions: ['elevate users'] }},
{ path: 'login', component: LoginComponent, canActivate: [MustNotBeLoggedInGuard] },
{ path: '', component: HomeComponent,
canActivate: [MustBeLoggedInGuard],
children: [
{ path: 'check-in', component: AddAttendanceEventListComponent,
canActivate: [MustHaveRoleGuard], data: { roleOptions: [ 'mentor', 'student-lead' ]}},
canActivate: [MustHavePermissionGuard], data: { permissions: [ 'list attendance events' ]}},
{ path: 'check-out', component: AddAttendanceEventListComponent,
canActivate: [MustHaveRoleGuard], data: { roleOptions: [ 'mentor', 'student-lead' ]}}
canActivate: [MustHavePermissionGuard], data: { permissions: [ 'list attendance events' ]}}
]
},
{ path: 'error', component: ErrorComponent },
Expand Down
12 changes: 6 additions & 6 deletions attendance-web/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<button mat-icon-button *ngIf="authService.checkLoggedIn() | async" aria-label="Button to open the navigation menu" (click)="sidenav.toggle()">
<mat-icon>menu</mat-icon>
</button>
<a routerLink="/check-in" id="title">Attendance Manager</a>
<a routerLink="/" id="title">Attendance Manager</a>
</div>
<span id="userName" *ngIf="getFirstName() | async as name">Welcome, {{name}}</span>
</div>
Expand All @@ -15,30 +15,30 @@
<div id="sidebar-container">
<mat-action-list>
<mat-list-item>
<a class="nav-list-item" (click)="sidenav.close()" routerLink="/check-in">
<a class="nav-list-item" (click)="sidenav.close()" routerLink="/">
<mat-icon class="nav-icon">home</mat-icon>
<p class="nav-label">Home</p>
</a>
</mat-list-item>
<mat-list-item *ngIf="authService.checkHasAnyRole(['mentor', 'student-lead']) | async" >
<mat-list-item *ngIf="permissionsService.checkPermissions(['list meeting events', 'add meeting events']) | async" >
<a class="nav-list-item" (click)="sidenav.close()" routerLink="/meetings">
<mat-icon class="nav-icon">date_range</mat-icon>
<p class="nav-label">Meetings</p>
</a>
</mat-list-item>
<mat-list-item *ngIf="authService.checkHasAnyRole(['mentor', 'student-lead']) | async">
<mat-list-item *ngIf="permissionsService.checkPermissions(['list students', 'view stats']) | async">
<a class="nav-list-item" (click)="sidenav.close()" routerLink="/reports">
<mat-icon class="nav-icon">description</mat-icon>
<p class="nav-label">Reports</p>
</a>
</mat-list-item>
<mat-list-item *ngIf="authService.checkHasAnyRole(['mentor', 'student-lead']) | async" >
<mat-list-item *ngIf="permissionsService.checkPermissions(['list students', 'view student images']) | async" >
<a class="nav-list-item" (click)="sidenav.close()" routerLink="/students">
<mat-icon class="nav-icon">groups</mat-icon>
<p class="nav-label">Students</p>
</a>
</mat-list-item>
<mat-list-item *ngIf="authService.checkHasAnyRole(['mentor']) | async">
<mat-list-item *ngIf="permissionsService.checkPermissions(['elevate users']) | async">
<a class="nav-list-item" (click)="sidenav.close()" routerLink="/users">
<mat-icon class="nav-icon">person</mat-icon>
<p class="nav-label">Users</p>
Expand Down
4 changes: 3 additions & 1 deletion attendance-web/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AuthService } from './services/auth.service';
import { StudentsService } from './services/students.service';
import { HttpClient } from '@angular/common/http';
import { ServerInfoService } from './services/server-info.service';
import { PermissionsService } from './services/permissions.service';

@Component({
selector: 'app-root',
Expand All @@ -25,7 +26,8 @@ export class AppComponent {
protected authService: AuthService,
protected studentsService: StudentsService,
protected snackbar: MatSnackBar,
private serverInfo: ServerInfoService
protected permissionsService: PermissionsService,
serverInfo: ServerInfoService,
) {
this.server_hash = serverInfo.getServerHash();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PollService } from 'src/app/services/poll.service';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorService } from 'src/app/services/error.service';
import { FormControl } from '@angular/forms';
import { PermissionsService } from 'src/app/services/permissions.service';

@Component({
selector: 'app-add-attendance-event-list',
Expand Down Expand Up @@ -46,7 +47,7 @@ export class AddAttendanceEventListComponent implements OnInit, AfterViewInit, O
private studentsService : StudentsService,
private attendanceService : AttendanceService,
private meetingsService : MeetingsService,
private authService : AuthService,
private permissionsService: PermissionsService,
private errorService: ErrorService,
private snackbar: MatSnackBar,
route: ActivatedRoute
Expand Down Expand Up @@ -154,12 +155,12 @@ export class AddAttendanceEventListComponent implements OnInit, AfterViewInit, O
this.unsubscribe.next(true);
}

private getValidRoles(): string[] {
let validRoles = ['mentor'];
private getRequiredPermission(): string {
if(this.mode == AttendanceEventType.CHECK_IN) {
validRoles.push('student-lead');
return 'student check in';
} else {
return 'student check out';
}
return validRoles;
}

private attendance(student: Student, action: AttendanceEventType) : void {
Expand All @@ -170,14 +171,7 @@ export class AddAttendanceEventListComponent implements OnInit, AfterViewInit, O

this.pendingStudentIds.next(this.pendingStudentIds.getValue().concat([student.id]));

this.authService.getUser().pipe(map(user => {
if(!user) {
throw new Error("Not authenticated");
}
return user;
})).subscribe(user => {
let validRoles = this.getValidRoles();
let authorized = user.role_names.find(it => validRoles.includes(it)) != undefined;
this.permissionsService.checkPermissions([this.getRequiredPermission()]).subscribe(authorized => {
if(!authorized) {
this.snackbar.open(
"You are not authorized to perform this action",
Expand Down Expand Up @@ -293,7 +287,7 @@ export class AddAttendanceEventListComponent implements OnInit, AfterViewInit, O
}

protected notAuthorized(): Observable<boolean> {
return this.authService.checkHasAnyRole(this.getValidRoles()).pipe(map(it => !it));
return this.permissionsService.checkPermissions([this.getRequiredPermission()]).pipe(map(it => !it));
}

getProfileImageSrc(student: Student): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { BehaviorSubject, map } from 'rxjs';
import { User } from 'src/app/models/user.model';
import { AuthService } from 'src/app/services/auth.service';
import { PermissionsService } from 'src/app/services/permissions.service';
import { UsersService } from 'src/app/services/users.service';

@Component({
Expand All @@ -18,12 +19,13 @@ export class UserComponent implements OnInit {

protected selectedRole! : FormControl<string|null>

protected roles = this.usersService.getAllRoles().pipe(map(roles => roles.concat(['none'])));
protected roles = this.permissionsService.getAllRoles().pipe(map(roles => roles.map(role => role.name).concat(['none'])));
protected loggedInUser = this.authService.getUser();

constructor(
private authService: AuthService,
private usersService: UsersService,
private permissionsService: PermissionsService,
private snackbar: MatSnackBar
) { }

Expand Down
2 changes: 1 addition & 1 deletion attendance-web/src/app/components/home/home.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ng-container *ngIf="authService.checkHasAnyRole(allowedRoles) | async; else notMentorHome">
<ng-container *ngIf="shouldShowAttendanceEvents() | async; else notMentorHome">
<nav mat-tab-nav-bar [tabPanel]="eventOptionsTabPanel" mat-stretch-tabs="false" mat-align-tabs="start" id="navTabs">
<a mat-tab-link *ngFor="let tab of tabs"
[routerLink]="tab.path"
Expand Down
49 changes: 39 additions & 10 deletions attendance-web/src/app/components/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterEvent } from '@angular/router';
import { filter, map, Observable, Subscription, take } from 'rxjs';
import { AuthService } from 'src/app/services/auth.service';
import { PermissionsService } from 'src/app/services/permissions.service';
import { UsersService } from 'src/app/services/users.service';

@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: false
})
export class HomeComponent implements OnInit {
export class HomeComponent implements OnInit, OnDestroy {
tabs = [
{
path: './check-in',
Expand All @@ -19,19 +22,45 @@ export class HomeComponent implements OnInit {
}
];

allowedRoles = ['mentor', 'student-lead'];
requiredPermission = 'list attendance events';

routersub!: Subscription;

constructor(
protected authService: AuthService,
route: ActivatedRoute,
router: Router
protected permissionsService: PermissionsService,
protected route: ActivatedRoute,
protected router: Router
) {
if(route.snapshot.url.length == 0 && authService.checkHasAnyRole(this.allowedRoles)) {
router.navigate(['check-in']);
}
}

protected shouldShowAttendanceEvents(): Observable<boolean> {
return this.permissionsService.checkPermissions(['list attendance events']);
}

ngOnInit(): void {
if(this.route.snapshot.url.length == 0) {
this.permissionsService.checkPermissions([this.requiredPermission]).subscribe(authorized => {
if(authorized) {
this.router.navigate(['check-in']);
}
})
}

this.routersub = this.router.events.pipe(
filter(e => e instanceof RouterEvent)
).subscribe(event => {
if(event.url == '/') {
this.permissionsService.checkPermissions([this.requiredPermission]).subscribe(authorized => {
if(authorized) {
this.router.navigate(['check-in']);
}
})
}
});
}

ngOnDestroy(): void {
this.routersub.unsubscribe();
}

}
Loading