Skip to content
Merged

0.8.2 #171

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
3 changes: 2 additions & 1 deletion apps/backend/src/app/database/entities/bookletInfo.entity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
Entity, Column, PrimaryGeneratedColumn, Unique
Entity, Column, PrimaryGeneratedColumn, Unique, Index
} from 'typeorm';

@Entity('bookletinfo')
@Unique('bookletinfo_pk', ['name'])
@Index(['name'])

export class BookletInfo {
@PrimaryGeneratedColumn()
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/app/database/entities/persons.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Booklet } from './booklet.entity';
@Unique('persons_pk', ['code', 'group', 'login'])
@Index(['workspace_id', 'code']) // Composite index for common query patterns
@Index(['workspace_id', 'group']) // Composite index for filtering by group within workspace
@Index(['login', 'code', 'workspace_id']) // Composite index for findUnitResponse query

class Persons {
@PrimaryGeneratedColumn()
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/app/database/entities/response.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Unit } from './unit.entity';
@Index(['unitid', 'variableid']) // Composite index for common query patterns
@Index(['unitid', 'status']) // Composite index for filtering by status
@Index(['codedstatus']) // Index for filtering by coded status
@Index(['value']) // Index for searching by value
export class ResponseEntity {
@PrimaryGeneratedColumn()
id: number;
Expand Down
10 changes: 3 additions & 7 deletions apps/frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Routes } from '@angular/router';

import { canActivateAuth } from './auth/auth.guard';
import { canActivateWithToken } from './auth/token.guard';

export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
Expand All @@ -11,22 +10,19 @@ export const routes: Routes = [
},
{
path: 'replay/:testPerson/:unitId/:page/:anchor',
canActivate: [canActivateWithToken],
loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent)
},
{
path: 'replay/:testPerson/:unitId/:page',
canActivate: [canActivateWithToken],
loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent)
},
{
path: 'replay/:testPerson/:unitId',
canActivate: [canActivateAuth],
loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent)
},
{ path: 'print-view/:unitId', canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) },
{ path: 'replay/:testPerson', canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) },
{ path: 'replay', canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) },
{ path: 'print-view/:unitId', loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) },
{ path: 'replay/:testPerson', loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) },
{ path: 'replay', loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) },
{ path: 'coding-manual', canActivate: [canActivateAuth], loadComponent: () => import('./coding/coding-management-manual/coding-management-manual.component').then(m => m.CodingManagementManualComponent) },
{
path: 'admin',
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/components/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[appTitle]="'Web application for coding'"
[introHtml]="'appService.appConfig.introHtml'"
[appName]="'IQB-Kodierbox'"
[appVersion]="'0.8.1'"
[appVersion]="'0.8.2'"
[userName]="authData.userName"
[userLongName]="appService.userProfile.firstName + ' ' + appService.userProfile.lastName"
[isUserLoggedIn]="Number(authData.userId) > 0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ interface ErrorMessages {
notInList: string;
notCurrent: string;
unknown: string;
tokenExpired: string;
tokenInvalid: string;
}

@Component({
Expand Down Expand Up @@ -98,14 +100,55 @@ export class ReplayComponent implements OnInit, OnDestroy, OnChanges {
return auth;
}

private validateToken(token: string): { isValid: boolean; errorType?: 'token_expired' | 'token_invalid' } {
if (!token) {
return { isValid: false, errorType: 'token_invalid' };
}

try {
// Decode the token to verify it's a valid JWT
const decoded: JwtPayload & { workspace: string } = jwtDecode(token);

// Check if the token has expired
const currentTime = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp < currentTime) {
return { isValid: false, errorType: 'token_expired' };
}

// Check if the token has the required workspace claim
if (!decoded.workspace) {
return { isValid: false, errorType: 'token_invalid' };
}

// Token is valid
return { isValid: true };
} catch (error) {
// Token is invalid (couldn't be decoded)
return { isValid: false, errorType: 'token_invalid' };
}
}

private subscribeRouter(): void {
this.routerSubscription = this.route.params
?.subscribe(async params => {
this.resetSnackBars();
this.resetUnitData();
this.authToken = await this.getAuthToken();

if (this.authToken) {
const tokenValidation = this.validateToken(this.authToken);
if (!tokenValidation.isValid) {
this.setIsLoaded();
if (tokenValidation.errorType === 'token_expired') {
this.openErrorSnackBar(this.getErrorMessages().tokenExpired, 'Schließen');
} else {
this.openErrorSnackBar(this.getErrorMessages().tokenInvalid, 'Schließen');
}
return;
}
}

try {
// Check if we're in print-view mode
const url = this.route.snapshot.url;
this.isPrintMode = url.length > 0 && url[0].path === 'print-view';

Expand Down Expand Up @@ -194,6 +237,22 @@ export class ReplayComponent implements OnInit, OnDestroy, OnChanges {
}
this.resetUnitData();
this.resetSnackBars();

// Validate the token if it exists
if (this.authToken) {
const tokenValidation = this.validateToken(this.authToken);
if (!tokenValidation.isValid) {
this.setIsLoaded();
// Show appropriate error message based on validation result
if (tokenValidation.errorType === 'token_expired') {
this.openErrorSnackBar(this.getErrorMessages().tokenExpired, 'Schließen');
} else {
this.openErrorSnackBar(this.getErrorMessages().tokenInvalid, 'Schließen');
}
return Promise.resolve();
}
}

const { unitIdInput } = changes;
try {
this.unitId = unitIdInput.currentValue;
Expand Down Expand Up @@ -326,6 +385,8 @@ export class ReplayComponent implements OnInit, OnDestroy, OnChanges {
ResponsesError: `Keine Antworten für Aufgabe "${this.unitId}" von Testperson "${this.testPerson}" gefunden`,
notInList: `Keine valide Seite mit ID "${this.page}" gefunden`,
notCurrent: `Seite mit ID "${this.page}" kann nicht ausgewählt werden`,
tokenExpired: 'Das Authentisierungs-Token ist abgelaufen',
tokenInvalid: 'Das Authentisierungs-Token ist ungültig',
unknown: 'Unbekannter Fehler'
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ <h3 class="units-title">Aufgaben</h3>
</div>
}
</div>
<button mat-icon-button
color="warn"
(click)="$event.stopPropagation(); deleteUnit(unit, booklet)"
class="delete-unit-button"
matTooltip="Unit löschen">
<mat-icon>delete</mat-icon>
</button>
}
</mat-list-item>
}
Expand Down Expand Up @@ -262,19 +269,25 @@ <h2 class="section-title">Antworten</h2>
<span class="variable-id">{{ response.variableid }}</span>
<span class="response-status">{{ response.status }}</span>
</div>
<button mat-icon-button (click)="response.expanded = !response.expanded" class="expand-button">
<mat-icon>{{ response.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
<div class="response-buttons">
<button mat-icon-button color="warn" (click)="$event.stopPropagation(); deleteResponse(response)"
class="delete-response-button" matTooltip="Antwort löschen">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button (click)="response.expanded = !response.expanded" class="expand-button">
<mat-icon>{{ response.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
</div>
</div>
@if (response.expanded) {
<div class="response-details">
@if (response.code) {
@if (response.code && response.codedstatus) {
<div class="detail-row">
<span class="detail-label">Code:</span>
<span class="detail-value">{{ response.code }}</span>
</div>
}
@if (response.score) {
@if (response.score && response.codedstatus) {
<div class="detail-row">
<span class="detail-label">Score:</span>
<span class="detail-value">{{ response.score }}</span>
Expand Down
Loading