Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New/ticket security enhancements #125

Merged
merged 11 commits into from
Jun 13, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ <h2>Observe Challenges</h2>
</div>
<div class="input-group mt-2 mb-0 ml-auto col-5">
<div id="feedbackType-input" #selectType class="btn-group mb-2 ml-auto" btnRadioGroup name="sortType" tabindex="0"
[(ngModel)]="sort" [ngModelOptions]="{updateOn: 'change'}">
<label class="btn btn-outline-dark btn-md text-white" btnRadio="byName">
<fa-icon [icon]="faSortAlphaDown"></fa-icon>
Name
</label>
<label class="btn btn-outline-dark btn-md text-white" btnRadio="byRank">
<fa-icon [icon]="faSortAmountDown"></fa-icon>
Rank
</label>
[(ngModel)]="sort" [ngModelOptions]="{updateOn: 'change'}">
<label class="btn btn-outline-dark btn-md text-white" btnRadio="byName">
<fa-icon [icon]="faSortAlphaDown"></fa-icon>
Name
</label>
<label class="btn btn-outline-dark btn-md text-white" btnRadio="byRank">
<fa-icon [icon]="faSortAmountDown"></fa-icon>
Rank
</label>
</div>
</div>
</div> <!-- end row for search and filtering -->
Expand All @@ -49,24 +49,31 @@ <h4 class="col-2 text-right p-0">Challenge Score</h4>
<h4 class="col-2 text-right">Consoles</h4>
</div>
<div class="d-flex flex-column">
<ng-container *ngIf="(!!table.size || !isLoading) && {actors: fetchActors$ | async, term : (term$ | async) ?? ''} as updates; else loading">
<div *ngFor="let row of table | keyvalue:sortByName; trackBy:trackByChallengeId;" [class]="row.value.pinned ? 'pinned' : ''"
[ngStyle]="{'order': row.value.gameRank | observeorder:row.value.pinned:sort:maxRank }">
<ng-container
*ngIf="(!!table.size || !isLoading) && {actors: fetchActors$ | async, term : (term$ | async) ?? ''} as updates; else loading">
<div *ngFor="let row of table | keyvalue:sortByName; trackBy:trackByChallengeId;"
[class]="row.value.pinned ? 'pinned' : ''"
[ngStyle]="{'order': row.value.gameRank | observeorder:row.value.pinned:sort:maxRank }">
<ng-container *ngIf="row.value | matchesterm:updates.term:'playerName':'name':'tag':'id'">
<div class="row mx-0 border-top border-light py-2">
<div class="col-2 align-self-center pl-1 text-left">
<button class="mr-1 btn btn-sm border-0" [class]="row.value.pinned ? 'btn-outline-success' : 'btn-outline-light'"
<button class="mr-1 btn btn-sm border-0"
[class]="row.value.pinned ? 'btn-outline-success' : 'btn-outline-light'"
(click)="togglePinRow(row.value)">
<fa-icon class="pin-icon" [icon]="faAngleDoubleUp" [rotate]="row.value.pinned ? 90 : undefined"></fa-icon>
<fa-icon class="pin-icon" [icon]="faAngleDoubleUp"
[rotate]="row.value.pinned ? 90 : undefined"></fa-icon>
</button>
<span>{{row.value.playerName}}</span>
</div>
<div class="col-2 align-self-center text-right">{{row.value.gameScore | number}} {{""}}</div>
<div class="col-2 align-self-center text-left">{{row.value.name}}<span class="badge badge-light text-black ml-2">{{row.value.id | slice:0:8}} {{row.value.tag}}</span></div>
<div class="col-2 align-self-center text-left">{{row.value.name}}<span
class="badge badge-light text-black ml-2">{{row.value.id | slice:0:8}} {{row.value.tag}}</span></div>
<div class="col-2 align-self-center text-right">{{row.value.duration | clock }}</div>
<div class="col-2 align-self-center text-right">{{row.value.challengeScore | number}}</div>
<div class="col-2 align-self-center text-right">
<button *ngIf="row.value.consoles?.length" class="btn btn-sm" [class]="row.value.expanded ? 'btn-success' : 'btn-outline-success' "(click)="toggleShowConsoles(row.value)">
<button *ngIf="row.value.consoles?.length" class="btn btn-sm"
[class]="row.value.expanded ? 'btn-success' : 'btn-outline-success' "
(click)="toggleShowConsoles(row.value)">
<fa-icon [icon]="faGrid"></fa-icon>
</button>
</div>
Expand All @@ -84,22 +91,26 @@ <h4 class="col-2 text-right">Consoles</h4>
</button>
</div>
<div class="mx-0 px-0 text-right">
<button class="btn btn-outline-success btn-sm px-2 py-1 border-0 ml-1" (click)="minimizeAllOthers(vm, row.value)">
<button class="btn btn-outline-success btn-sm px-2 py-1 border-0 ml-1"
(click)="minimizeAllOthers(vm, row.value)">
<fa-icon [icon]="faWindowRestore"></fa-icon>
</button>
<button class="btn btn-outline-success btn-sm px-2 py-1 border-0 ml-1" (click)="toggleMinimize(vm)">
<button class="btn btn-outline-success btn-sm px-2 py-1 border-0 ml-1"
(click)="toggleMinimize(vm)">
<fa-icon [icon]="faMinusSquare"></fa-icon>
</button>
<button class="btn btn-outline-success btn-sm px-2 py-1 border-0 mr-1" (click)="toggleFullWidth(vm)">
<button class="btn btn-outline-success btn-sm px-2 py-1 border-0 mr-1"
(click)="toggleFullWidth(vm)">
<fa-icon [icon]="vm.fullWidth ? faCompressAlt : faExpandAlt"></fa-icon>
</button>
</div>
</div> <!-- end console header -->
<div *ngIf="updates.actors?.get(row.value.id+'#'+vm.name) as activeUsers" class="mx-1 mb-1 p-0 text-left" >
<span *ngFor="let user of activeUsers" class="badge badge-success m-1 text-break text-dark">
<fa-icon class="pr-1" [icon]="faUser"></fa-icon>
{{user.userName}}
</span>
<div *ngIf="updates.actors?.get(row.value.id+'#'+vm.name) as activeUsers"
class="mx-1 mb-1 p-0 text-left">
<span *ngFor="let user of activeUsers" class="badge badge-success m-1 text-break text-dark">
<fa-icon class="pr-1" [icon]="faUser"></fa-icon>
{{user.userName}}
</span>
</div> <!-- end users list header -->
<div class="iframe-wrapper mt-auto" style="padding: 0px;">
<iframe class="rounded-bottom w-100 h-100" frameborder="0"
Expand All @@ -115,7 +126,7 @@ <h4 class="col-2 text-right">Consoles</h4>
<div *ngIf="vm.minimized" class="m-0 p-2">
<button class="btn btn-outline-success btn-sm p-2 w-100" (click)="toggleMinimize(vm)">
<div *ngIf="updates.actors?.get(row.value.id+'#'+vm.name)?.length as userCount" class="float-left">
<fa-icon [icon]="faUser" ></fa-icon>
<fa-icon [icon]="faUser"></fa-icon>
<span>{{userCount}}</span>
</div>
<span class="mx-3">{{vm.name}}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ <h4>Challenge Specs</h4>
<!-- edit spec map locations -->
<div [hidden]="viewing!=='map'">


<div #mapbox class="mapbox" [class.mapbox-active]="!!this.specDrag" [class.mapbox-hover]="!!specHover">
<div #callout class="callout bg-primary m-0 p-4" [hidden]="!this.specHover">
<span>{{this.specHover?.name}}</span><br />
Expand All @@ -125,9 +124,6 @@ <h4>Challenge Specs</h4>
</ng-container>
</svg>
</div>

</div>

</div>

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ <h6>You've got a new API key!</h6>
<th></th>
</tr>
<tr *ngFor="let key of apiKeys || []">
<td [innerHTML]="key.name || '<em>Unnamed</em>' | safeHtml"></td>
<td>
<span *ngIf="key.name else unnamedKey">{{key.name}}</span>
<ng-template #unnamedKey><em>Unnamed</em></ng-template>
</td>
<td>{{ key.generatedOn | shortdate }}</td>
<td [innerHTML]="(!!key.expiresOn ? (key.expiresOn | shortdate) : '<em>Doesn\'t expire</em>')"></td>
<td>
<span *ngIf="!!key.expiresOn">{{key.expiresOn | shortdate}}</span>
<ng-template #noExpiresKey><em>Doesn't expire</em></ng-template>
</td>
<td>
<app-confirm-button btnClass="btn btn-danger" (confirm)="delete(key.id)">
<span>Delete</span>
Expand Down
4 changes: 2 additions & 2 deletions projects/gameboard-ui/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PlayerAvatarListComponent } from './components/player-avatar-list/playe
import { ModalConfirmComponent } from './components/modal/modal-confirm.component';
import { ModalConfirmDirective } from './directives/modal-confirm.directive';
import { PlayerStatusComponent } from './components/player-status/player-status.component';
import { SafeHtmlPipe } from './pipes/safe-html.pipe';
import { SanitizeHtmlPipe } from './pipes/sanitize-html.pipe';
import { YamlBlockComponent } from './components/yaml-block/yaml-block.component';
import { YamlPipe } from './pipes/yaml.pipe';
import { CustomInputComponent } from './components/custom-input/custom-input.component';
Expand All @@ -25,7 +25,7 @@ const PUBLIC_DECLARATIONS = [
PlayerAvatarListComponent,
PlayerStatusComponent,
RelativeUrlsPipe,
SafeHtmlPipe,
SanitizeHtmlPipe,
UrlRewritePipe,
YamlBlockComponent,
YamlPipe
Expand Down
6 changes: 3 additions & 3 deletions projects/gameboard-ui/src/app/core/pipes/linkify-html.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Pipe, PipeTransform, SecurityContext } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import linkifyHtml from 'linkify-html';

@Pipe({ name: 'linkifyHtml' })
export class LinkifyHtmlPipe implements PipeTransform {
constructor(private domSanitizer: DomSanitizer) { }

transform(value: string, ...args: unknown[]): SafeHtml {
return this.domSanitizer.bypassSecurityTrustHtml(linkifyHtml(value, { nl2br: true }));
transform(value: string, ...args: unknown[]): string {
return this.domSanitizer.sanitize(SecurityContext.HTML, linkifyHtml(value, { nl2br: true })) || "";
}
}
11 changes: 0 additions & 11 deletions projects/gameboard-ui/src/app/core/pipes/safe-html.pipe.ts

This file was deleted.

11 changes: 11 additions & 0 deletions projects/gameboard-ui/src/app/core/pipes/sanitize-html.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Pipe, PipeTransform, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({ name: 'sanitizeHtml' })
export class SanitizeHtmlPipe implements PipeTransform {
constructor(private domSanitizer: DomSanitizer) { }

transform(value: string): string {
return this.domSanitizer.sanitize(SecurityContext.HTML, value) || '';
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnInit, SecurityContext } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { faPrint } from '@fortawesome/free-solid-svg-icons';
import { Observable } from 'rxjs';
Expand All @@ -13,7 +13,7 @@ import { PlayerService } from '../../api/player.service';
})
export class CertificateComponent implements OnInit {
@Input() ctx!: GameContext;
cert$!: Observable<SafeHtml>;
cert$!: Observable<string>;
toPrint: string = "";
faPrint = faPrint;

Expand All @@ -26,7 +26,7 @@ export class CertificateComponent implements OnInit {
this.cert$ = this.apiPlayer.getCertificate(this.ctx.player.id).pipe(
tap(g => this.toPrint = g.html),
// sanitize html to render in iframe binding and add wrapper div to center certificate
map(g => this.sanitizer.bypassSecurityTrustHtml(`<div style="max-width: max-content; margin: auto;">${g.html}</div>`)),
map(g => this.sanitizer.sanitize(SecurityContext.HTML, `<div style="max-width: max-content; margin: auto;">${g.html}</div>`) || ''),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ <h2>Certificates</h2>
<div *ngFor="let cert of certs" class="row rounded bg-secondary mb-1 mx-0 py-2">
<div class="col-3 d-flex flex-wrap">
<div class="ml-2">
<a class="btn btn-link p-0 text-left" [routerLink]="['/', 'game', cert.game.id]"><span class="mr-2 h5">{{cert.game.name}}</span></a>
<a class="btn btn-link p-0 text-left" [routerLink]="['/', 'game', cert.game.id]"><span
class="mr-2 h5">{{cert.game.name}}</span></a>
<div class="text-muted">
<span> {{cert.game.gameEnd | shortdate}}</span>
</div>
Expand All @@ -24,15 +25,16 @@ <h2>Certificates</h2>
<span class="mr-4">{{cert.player.rank}} <fa-icon [icon]="faMedal"></fa-icon></span>
</div>
<div class="col-4 align-self-center">
<span class=""><fa-icon [icon]="cert.game.allowTeam ? faUsers : faUser"></fa-icon> {{cert.player.approvedName}}</span>
<span class=""><fa-icon [icon]="cert.game.allowTeam ? faUsers : faUser"></fa-icon>
{{cert.player.approvedName}}</span>
</div>
<div class="col-2 align-self-center text-right ml-auto">
<button *ngIf="true" class="btn btn-outline-success btn-sm ml-1" (click)="print(cert.html)">
<button *ngIf="true" class="btn btn-outline-success btn-sm ml-1" (click)="print(cert)">
<fa-icon [icon]="faPrint"></fa-icon>
<span>Print</span>
</button>
</div>
</div>
</div>
<div *ngIf="!certs?.length" class="text-center text-muted my-4">
No certificates yet
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { faArrowLeft, faAward, faPrint, faMedal, faUser, faUsers } from '@fortawesome/free-solid-svg-icons';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PlayerCertificate } from '../../api/player-models';
import { PlayerService } from '../../api/player.service';

Expand All @@ -19,29 +18,28 @@ export class CertificateListComponent implements OnInit {
faUser = faUser;
faUsers = faUsers;
certs$: Observable<PlayerCertificate[]>;

constructor(
private apiPlayer: PlayerService,
apiPlayer: PlayerService,
private sanitizer: DomSanitizer
) {
this.certs$ = apiPlayer.getUserCertificates().pipe(
map(c => c.map(a => ({...a, safeHtml: sanitizer.bypassSecurityTrustHtml(a.html)}))
));
) {
this.certs$ = apiPlayer.getUserCertificates();
}

ngOnInit(): void {
}

print(html: string): void {
print(cert: PlayerCertificate): void {
let printWindow = window.open('', '', '');
// make sure background is always there and no margins to print to pdf as is
printWindow?.document?.write(`<style type="text/css">* {-webkit-print-color-adjust: exact !important; color-adjust: exact !important; }</style>`);
printWindow?.document?.write(`<style type="text/css">@media print { body { margin: 0mm!important;} @page{ margin: 0mm!important; }}</style>`);
printWindow?.document?.write(`<style type="text/css" media="print"> @page { size: landscape; } </style>`);
printWindow?.document.write(html);
printWindow?.document.write(this.sanitizer.sanitize(SecurityContext.HTML, cert.html) || "");
printWindow?.document.close();
printWindow?.focus();
printWindow?.addEventListener('load', printWindow?.print, true); // wait until all content loads before printing
// don't close new tab automatically in case want to keep open for some reason [ printWindow?.close(); ]
}
}

}
5 changes: 4 additions & 1 deletion projects/gameboard-ui/src/app/services/router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class RouterService {

constructor(public router: Router) { }

public goHome(): void {
this.router.navigateByUrl("/");
}

public toSupportTickets(highlightTicketKey: string) {
return this.router.navigateByUrl(this.router.parseUrl(`/support/tickets/${highlightTicketKey}`));
}

public tryGoBack() {
const prevUrl = this.router.getCurrentNavigation()?.previousNavigation?.initialUrl;
if (prevUrl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<span>Back</span>
</a>

<h2 class="m-0 p-0 summary-break">
<h2 class="mb-1 p-0 summary-break">
<span class="text-muted">{{ctx.ticket.fullKey}}</span>&nbsp;
<span *ngIf="!editingContent">{{ctx.ticket.summary}}</span>
</h2>
Expand All @@ -22,8 +22,9 @@ <h2 class="m-0 p-0 summary-break">
<button class="btn btn-success float-right" (click)="saveEditedTicket()">Save</button>
</div>
</div>
<div class="d-flex flex-wrap align-items-center mb-2">
<h4 class="m-0 p-0"><span class="badge" [class.badge-info]="ctx.ticket.status == 'Open' || !ctx.ticket.status"
<div class="d-flex flex-wrap align-items-baseline justify-content-center mb-2">
<h4 class="p-0 mr-2">
<span class="badge" [class.badge-info]="ctx.ticket.status == 'Open' || !ctx.ticket.status"
[class.badge-success]="ctx.ticket.status == 'In Progress'"
[class.badge-dark]="ctx.ticket.status == 'Closed'">{{ctx.ticket.status}}</span>
</h4>
Expand Down Expand Up @@ -69,7 +70,7 @@ <h4 class="m-0 p-0"><span class="badge" [class.badge-info]="ctx.ticket.status ==
</div>
<div class="card-body">
<app-long-content-hider>
<p class="card-text" [innerHtml]="ctx.ticket.description | linkifyHtml"></p>
<p class="card-text" [innerHtml]="ctx.ticket.description"></p>
</app-long-content-hider>
<ng-container *ngIf="editingContent && changedTicket && !!changedTicket?.id">
<textarea type="text" class="form-control w-100 text-white" rows=8 autocomplete="off"
Expand Down Expand Up @@ -101,6 +102,7 @@ <h4 class="mt-2 d-flex justify-content-between">
<div class="mx-md-4 mx-0 mt-2">
<div class="card mx-3">
<div class="card-body rounded p-2">
<app-error-div [errors]="errors"></app-error-div>
<textarea class="form-control w-100 text-white border-0 dark-textarea" [rows]="newCommentFocus? 4 : 2"
placeholder="Add a comment..." (focus)="newCommentFocus = true" [(ngModel)]="newCommentText"
style="background-color: #111"></textarea>
Expand Down Expand Up @@ -131,11 +133,8 @@ <h4 class="mt-2 d-flex justify-content-between">
</div>
</div>
</div>

</div>



<div *ngIf="!!ctx.ticket.activity" class="mx-md-4 mx-0">
<div *ngFor="let activity of ctx.ticket.activity; let i = index;" class="border-dark1 border-top1 ">
<div class="card my-3 mx-3" *ngIf="activity.type == 0"> <!-- Comment -->
Expand All @@ -153,7 +152,7 @@ <h4 class="mt-2 d-flex justify-content-between">
</div>
<div class="card-body">
<app-long-content-hider>
<p class="card-text" [innerHtml]="activity.message | linkifyHtml"></p>
<p class="card-text" [innerHtml]="activity.message"></p>
</app-long-content-hider>
<div *ngIf="activity.attachmentFiles?.length" class="d-flex overflow-auto my-auto">
<ng-container *ngFor="let file of activity.attachmentFiles; let j = index;">
Expand Down Expand Up @@ -340,4 +339,4 @@ <h4 id="dialog-sizes-name1" class="modal-title pull-left">
</div>
</div>
</div>
</div>
</div>
Loading