Adds adaptions to user user authentationc

parent 37bf3ffa
Pipeline #18225 failed with stages
in 3 minutes and 34 seconds
<div id="quizSummaryHeader"
<div *ngIf="questionGroupItem"
id="quizSummaryHeader"
class="quizSummary mt-md-3 px-md-0">
<div class="row no-gutters">
......
......@@ -14,6 +14,7 @@
[class.error]="elem.selectable && !elem.isActive"
[id]="elem.id"
[routerLink]="getLinkTarget(elem)"
[queryParams]="getQueryParams(elem)"
[title]="elem.textName | translate"
[attr.data-intro]="elem.showIntro ? (('region.footer.footer_bar.description.' + elem.id) | translate) : null"
(click)="toggleSetting(elem)">
......
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Component, Inject, Input, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { isPlatformServer } from '@angular/common';
import { Component, EventEmitter, Inject, Input, PLATFORM_ID } from '@angular/core';
import { IFooterBarElement } from '../../../lib/footerbar-element/interfaces';
import { CurrentQuizService } from '../../service/current-quiz/current-quiz.service';
import { FileUploadService } from '../../service/file-upload/file-upload.service';
......@@ -13,7 +11,7 @@ import { TrackingService } from '../../service/tracking/tracking.service';
templateUrl: './footer-bar.component.html',
styleUrls: ['./footer-bar.component.scss'],
})
export class FooterBarComponent implements OnInit, OnDestroy {
export class FooterBarComponent {
public static TYPE = 'FooterBarComponent';
private _footerElements: Array<IFooterBarElement> = [];
......@@ -22,9 +20,8 @@ export class FooterBarComponent implements OnInit, OnDestroy {
return this._footerElements;
}
@Input() set footerElements(value: Array<IFooterBarElement>) {
this.hasRightScrollElement = value.length > 1;
this._footerElements = value;
@Input() set footerElementEmitter(value: EventEmitter<Array<IFooterBarElement>>) {
value.subscribe(elements => this._footerElements = elements);
}
private _footerElemIndex = 1;
......@@ -47,11 +44,8 @@ export class FooterBarComponent implements OnInit, OnDestroy {
this._hasRightScrollElement = value;
}
private _routerSubscription: Subscription;
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
private router: Router,
private footerBarService: FooterBarService,
private currentQuizService: CurrentQuizService,
private trackingService: TrackingService,
......@@ -59,29 +53,14 @@ export class FooterBarComponent implements OnInit, OnDestroy {
) {
}
public ngOnInit(): void {
this._routerSubscription = this.router.events.subscribe((val) => {
if (isPlatformBrowser(this.platformId)) {
const navbarFooter = document.getElementById('navbar-footer-container');
if (navbarFooter) {
navbarFooter.scrollLeft = 0;
}
}
this.footerElemIndex = 1;
if (val.hasOwnProperty('url')) {
this.footerBarService.footerElemTheme.linkTarget = val['url'].indexOf('lobby') > -1 ? '/quiz/flow/theme' : '/themes';
}
});
}
public ngOnDestroy(): void {
this._routerSubscription.unsubscribe();
}
public getLinkTarget(elem: IFooterBarElement): Function | string {
return typeof elem.linkTarget === 'function' ? elem.linkTarget(elem) : elem.linkTarget;
}
public getQueryParams(elem: IFooterBarElement): object {
return elem.queryParams;
}
public toggleSetting(elem: IFooterBarElement): void {
this.currentQuizService.toggleSetting(elem);
elem.onClickCallback(elem);
......
<div class="row">
<div class="text-light col-12">
<h4>Organize members in teams</h4>
<h4>{{'component.membergroup-manager.title' | translate}}</h4>
<div class="row">
<div class="col-12 col-sm-5">
<form>
<div class="form-group">
<label for="max-users-per-group">Max users per team</label>
<label for="max-users-per-group">{{'component.membergroup-manager.max-users' | translate}}</label>
<input type="number"
class="form-control"
id="max-users-per-group"
placeholder="Max users per group"
[placeholder]="'component.membergroup-manager.max-users' translate"
[ngModelOptions]="{standalone: true}"
[(ngModel)]="maxMembersPerGroup"/>
......@@ -19,7 +19,7 @@
</div>
<div class="col-12 col-sm-7 mb-2">
<label class="d-block">Add users automatically to teams</label>
<label class="d-block">{{'component.membergroup-manager.autojoin-to-team' | translate}}</label>
<label class="btn pointer mb-0"
[class.btn-success]="autoJoinToGroup"
[class.btn-danger]="!autoJoinToGroup">
......@@ -40,10 +40,10 @@
<div class="input-group">
<input type="text"
class="form-control"
placeholder="Add member group"
[placeholder]="'component.membergroup-manager.create-team-placeholder' | translate"
(keyup.enter)="addMemberGroup()"
[(ngModel)]="memberGroupName"
aria-label="Add member group"
[attr.aria-label]="'component.membergroup-manager.create-team-placeholder' | translate"
aria-describedby="add-member-group">
<div class="input-group-append">
......@@ -52,16 +52,16 @@
(click)="addMemberGroup()"
[class.disabled]="memberGroups.indexOf(memberGroupName) > -1 || !memberGroupName.length"
[disabled]="memberGroups.indexOf(memberGroupName) > -1 || !memberGroupName.length">
<span>Add a new team</span>
<span>{{'component.membergroup-manager.create-team' | translate}}</span>
</button>
</div>
</div>
<h6 class="mt-4">Currently added teams:</h6>
<p *ngIf="!memberGroups.length">No teams added yet</p>
<h6 class="mt-4">{{'component.membergroup-manager.current-teams' | translate}}</h6>
<p *ngIf="!memberGroups.length">{{'component.membergroup-manager.no-current-teams' | translate}}</p>
<ul *ngFor="let group of memberGroups"
class="list-unstyled">
<li class="border-bottom p-2 mx-2">
<li class="border-bottom p-2">
<span>{{group}}</span>
<span class="pointer float-right"
(click)="removeMemberGroup(group)"><i class="fas fa-trash"></i></span>
......
<h4 class="text-light text-center mb-5 mt-sm-5">Please enter your login credentials to proceed to the requested resource</h4>
<ng-container *ngIf="!isLoading">
<h4 class="text-light text-center mb-5 mt-sm-5">Please enter your login credentials to proceed to the requested resource</h4>
<div class="input-group input-group-sm">
<div class="input-group input-group-sm">
<input type="text"
class="form-control"
name="username"
placeholder="Username"
(keypress)="trySubmit($event)"
[(ngModel)]="username"/>
<input type="text"
class="form-control"
name="username"
placeholder="Username"
(keypress)="trySubmit($event)"
[(ngModel)]="username"/>
<input type="password"
class="form-control my-2 my-sm-0"
name="password"
placeholder="Password"
(keypress)="trySubmit($event)"
[(ngModel)]="password"/>
<input type="password"
class="form-control my-2 my-sm-0"
name="password"
placeholder="Password"
(keypress)="trySubmit($event)"
[(ngModel)]="password"/>
<div class="input-group-append">
<div class="input-group-append">
<button class="btn btn-info"
(click)="login()">Login
</button>
<button class="btn btn-info"
(click)="login()">Login
</button>
</div>
</div>
</div>
<h4 *ngIf="authorizationFailed"
class="text-danger text-center mt-5">Authorization failed
</h4>
\ No newline at end of file
<h4 *ngIf="authorizationFailed"
class="text-danger text-center mt-5">Authorization failed
</h4>
</ng-container>
\ No newline at end of file
......@@ -20,6 +20,12 @@ export class LoginComponent implements OnInit {
return this._authorizationFailed;
}
private _isLoading = true;
get isLoading(): boolean {
return this._isLoading;
}
private return = '';
constructor(
......@@ -29,13 +35,20 @@ export class LoginComponent implements OnInit {
private headerLabelService: HeaderLabelService,
private footerBarService: FooterBarService,
) {
this.userService.isLoggedIn = false;
this.userService.logout();
this.headerLabelService.headerLabel = 'Login';
this.footerBarService.replaceFooterElements([]);
}
public ngOnInit(): void {
this.route.queryParams.subscribe(params => this.return = params['return'] || '/');
this.route.queryParams.subscribe(params => {
if (params['logout']) {
this.router.navigate(['/']);
return;
}
this._isLoading = false;
this.return = params['return'] || '/';
});
}
public async login(): Promise<void> {
......
......@@ -6,7 +6,7 @@
<div class="container-fluid">
<div class="row flex-sm-nowrap h-100">
<div class="footer-bar-wrapper flex-grow-0 flex-shrink-0 p-0 relative">
<app-footer-bar [footerElements]="getFooterBarElements()"></app-footer-bar>
<app-footer-bar [footerElementEmitter]="getFooterBarElements()"></app-footer-bar>
<div class="d-none d-md-block">
<router-outlet name="additionalData-md"
class="d-none d-md-block"></router-outlet>
......
import { isPlatformServer } from '@angular/common';
import { AfterViewInit, Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { AfterViewInit, Component, EventEmitter, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { NavigationEnd, RouteConfigLoadEnd, RouteConfigLoadStart, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import * as introJs from 'intro.js';
......@@ -11,6 +11,7 @@ import { I18nService } from '../../service/i18n/i18n.service';
import { StorageService } from '../../service/storage/storage.service';
import { ThemesService } from '../../service/themes/themes.service';
import { TrackingService } from '../../service/tracking/tracking.service';
import { UserService } from '../../service/user/user.service';
import { DB_TABLE, STORAGE_KEY } from '../../shared/enums';
// Update global window.* object interface (https://stackoverflow.com/a/12709880/7992104)
......@@ -53,15 +54,17 @@ export class RootComponent implements OnInit, AfterViewInit {
private translateService: TranslateService,
private router: Router,
private storageService: StorageService,
private userService: UserService,
) {
this.themesService.updateCurrentlyUsedTheme();
}
public getFooterBarElements(): Array<IFooterBarElement> {
public getFooterBarElements(): EventEmitter<Array<IFooterBarElement>> {
return this.footerBarService.footerElements;
}
public ngOnInit(): void {
this.userService.loadConfig();
this.router.events.subscribe((event: any) => {
if (event instanceof RouteConfigLoadStart) {
this._isLoading = true;
......
......@@ -3,7 +3,7 @@ import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { IQuestionGroup } from 'arsnova-click-v2-types/src/questions/interfaces';
import { questionGroupReflection } from 'arsnova-click-v2-types/src/questions/questionGroup_reflection';
import { Subscription } from 'rxjs';
import { Observable, of } from 'rxjs';
import { DB_TABLE, STORAGE_KEY } from '../../shared/enums';
import { FooterBarService } from '../footer-bar/footer-bar.service';
import { StorageService } from '../storage/storage.service';
......@@ -82,12 +82,18 @@ export class ActiveQuestionGroupService {
}
}
public loadData(): Subscription {
return this.storageService.read(DB_TABLE.CONFIG, STORAGE_KEY.ACTIVE_QUESTION_GROUP).subscribe(parsedObject => {
public loadData(): Observable<IQuestionGroup> {
if (this._activeQuestionGroup) {
return of(this._activeQuestionGroup);
}
const data = this.storageService.read(DB_TABLE.CONFIG, STORAGE_KEY.ACTIVE_QUESTION_GROUP);
data.subscribe(parsedObject => {
if (parsedObject) {
this._activeQuestionGroup = questionGroupReflection[parsedObject.TYPE](parsedObject);
}
});
return data;
}
private dec2hex(dec): string {
......
......@@ -10,9 +10,7 @@ import { DefaultSettings } from '../../../../lib/default.settings';
})
export class QuizApiService {
constructor(
private http: HttpClient,
) { }
constructor(private http: HttpClient) { }
public QUIZ_STATUS_URL(quizName: string): string {
return `${DefaultSettings.httpApiEndpoint}/quiz/status/${quizName}`;
......@@ -82,6 +80,14 @@ export class QuizApiService {
return `${DefaultSettings.httpApiEndpoint}/quiz/upload`;
}
public QUIZ_EXPIRY_GET_URL(): string {
return `${DefaultSettings.httpApiEndpoint}/expiry-quiz/`;
}
public QUIZ_EXPIRY_POST_URL(): string {
return `${DefaultSettings.httpApiEndpoint}/expiry-quiz/quiz`;
}
public getQuizStatus(quizName): Observable<IMessage> {
return this.http.get<IMessage>(this.QUIZ_STATUS_URL(quizName));
}
......@@ -149,4 +155,20 @@ export class QuizApiService {
public postQuizUpload(formData: FormData): Observable<IMessage> {
return this.http.post<IMessage>(this.QUIZ_UPLOAD_POST_URL(), formData);
}
public getExpiryQuiz(): Observable<IMessage> {
return this.http.get<IMessage>(this.QUIZ_EXPIRY_GET_URL());
}
public postExpiryQuiz(data: object): Observable<IMessage> {
return this.http.post<IMessage>(this.QUIZ_EXPIRY_POST_URL(), data);
}
public postInitExpiryQuiz(data: object): Observable<IMessage> {
return this.http.post<IMessage>(this.QUIZ_EXPIRY_INIT_POST_URL(), data);
}
private QUIZ_EXPIRY_INIT_POST_URL(): string {
return `${DefaultSettings.httpApiEndpoint}/expiry-quiz/init`;
}
}
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { USER_AUTHORIZATION } from '../../shared/enums';
import { UserService } from '../user/user.service';
@Injectable({
......@@ -13,7 +14,7 @@ export class StaticLoginService implements CanActivate {
await this.userService.loadConfig();
if (this.userService.isLoggedIn) {
if (this.isAllowedToProceed(route)) {
return true;
}
......@@ -24,4 +25,19 @@ export class StaticLoginService implements CanActivate {
});
return false;
}
private isAllowedToProceed(route): boolean {
if (!this.userService.isLoggedIn) {
return false;
}
switch (route.routeConfig.path) {
case 'i18n-manager':
return this.userService.isAuthorizedFor(USER_AUTHORIZATION.EDIT_I18N);
case 'quiz-manager':
return this.userService.isAuthorizedFor(USER_AUTHORIZATION.CREATE_EXPIRED_QUIZ);
default:
return true;
}
}
}
import { isPlatformServer } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { AbstractQuestionGroup } from 'arsnova-click-v2-types/src/questions/questiongroup_abstract';
import { Observable } from 'rxjs';
import { DB_NAME, DB_TABLE, STORAGE_KEY } from '../../shared/enums';
import { IndexedDbService } from './indexed.db.service';
......@@ -24,6 +25,10 @@ export class StorageService {
}
public create(table: DB_TABLE, key: string | STORAGE_KEY, value: any): Observable<any> {
if (value instanceof AbstractQuestionGroup) {
value = value.serialize();
}
return this.indexedDbService.put(table, {
id: this.formatKey(key),
value,
......
import { Injectable } from '@angular/core';
import { DB_TABLE, STORAGE_KEY } from '../../shared/enums';
import { EventEmitter, Injectable } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt';
import { ILoginSerialized } from 'arsnova-click-v2-types/src/common';
import { DB_TABLE, STORAGE_KEY, USER_AUTHORIZATION } from '../../shared/enums';
import { AuthorizeApiService } from '../api/authorize/authorize-api.service';
import { StorageService } from '../storage/storage.service';
......@@ -12,11 +14,29 @@ export class UserService {
}
set isLoggedIn(value: boolean) {
this._casTicket = null;
this._staticLoginToken = null;
this._username = null;
this.persistTokens();
if (!value) {
this._casTicket = null;
this._staticLoginToken = null;
this._username = null;
this.deleteTokens();
} else {
this.persistTokens();
}
this._isLoggedIn = value;
this._staticLoginTokenContent = this.decodeToken();
this._loginNotifier.emit(value);
}
private _staticLoginTokenContent: ILoginSerialized;
get staticLoginTokenContent(): ILoginSerialized {
return this._staticLoginTokenContent;
}
private _loginNotifier = new EventEmitter<boolean>();
get loginNotifier(): EventEmitter<boolean> {
return this._loginNotifier;
}
private _casTicket: string;
......@@ -37,17 +57,18 @@ export class UserService {
return this._staticLoginToken;
}
constructor(private authorizeApiService: AuthorizeApiService, private storageService: StorageService) {
constructor(private authorizeApiService: AuthorizeApiService, private storageService: StorageService, private jwtHelper: JwtHelperService) {
}
public loadConfig(): Promise<boolean> {
return new Promise<boolean>(async resolve => {
if (!await this.storageService.read(DB_TABLE.CONFIG, STORAGE_KEY.TOKEN).toPromise()) {
const tokens = await this.storageService.read(DB_TABLE.CONFIG, STORAGE_KEY.TOKEN).toPromise();
if (!tokens) {
resolve(true);
return;
}
const tokens = await this.storageService.read(DB_TABLE.CONFIG, STORAGE_KEY.TOKEN).toPromise();
this._casTicket = tokens.casTicket;
this._staticLoginToken = tokens.staticLoginToken;
this._username = tokens.username;
......@@ -58,23 +79,34 @@ export class UserService {
}
this.authorizeApiService.getValidateStaticLoginToken(this._username, this._staticLoginToken).subscribe(response => {
this._isLoggedIn = response.status === 'STATUS:SUCCESSFUL' && response.step === 'AUTHENTICATE_STATIC';
resolve(true);
this.isLoggedIn = response.status === 'STATUS:SUCCESSFUL' && response.step === 'AUTHENTICATE_STATIC';
resolve(this.isLoggedIn);
});
});
}
public logout(): void {
this.isLoggedIn = false;
}
public decodeToken(): ILoginSerialized {
if (!this.staticLoginToken) {
return null;
}
return this.jwtHelper.decodeToken(this.staticLoginToken);
}
public authenticateThroughCas(token: string): Promise<boolean> {
return new Promise(async resolve => {
const data = await this.authorizeApiService.getAuthorizationForToken(token).toPromise();
if (data.status === 'STATUS:SUCCESSFUL') {
this._isLoggedIn = true;
this._casTicket = data.payload.casTicket;
this.persistTokens();
this.isLoggedIn = true;
resolve(true);
} else {
this._isLoggedIn = false;
this.isLoggedIn = false;
resolve(false);
}
});
......@@ -90,13 +122,12 @@ export class UserService {
}).toPromise();
if (data.status === 'STATUS:SUCCESSFUL') {
this._isLoggedIn = true;
this._staticLoginToken = data.payload.token;
this._username = username;
this.persistTokens();
this.isLoggedIn = true;
resolve(true);
} else {
this._isLoggedIn = false;
this.isLoggedIn = false;
resolve(false);
}
});
......@@ -106,6 +137,18 @@ export class UserService {
return this.sha1(`${username}|${password}`);
}
public isAuthorizedFor(authorization: USER_AUTHORIZATION): boolean {
if (!this.staticLoginTokenContent) {
return false;
}
return this.staticLoginTokenContent.userAuthorizations.find(value => value === authorization);
}
private deleteTokens(): void {
this.storageService.delete(DB_TABLE.CONFIG, STORAGE_KEY.TOKEN).subscribe();
}
private persistTokens(): void {
this.storageService.create(DB_TABLE.CONFIG, STORAGE_KEY.TOKEN, {
casTicket: this._casTicket,
......
......@@ -87,3 +87,10 @@ export enum TRACKING_CATEGORY_TYPE {
THEME_CHANGE, //
THEME_PREVIEW, //
}
export enum USER_AUTHORIZATION {
CREATE_EXPIRED_QUIZ = 'CREATE_EXPIRED_QUIZ', //
CREATE_QUIZ_FROM_EXPIRED = 'CREATE_QUIZ_FROM_EXPIRED', //
CREATE_QUIZ = 'CREATE_QUIZ', //
EDIT_I18N = 'EDIT_I18N', //
}
......@@ -57,6 +57,10 @@
"enable_cas_login": "Enable CAS Login",
"global_leaderboard": "Global Leaderboard",
"member_group": "Member Groups",
"edit-i18n": "Edit i18n",
"login": "Login",
"logout": "Logout",
"save_quiz": "Save Quiz",
"description": {
"qr-code": "Attendees can scan the QR code via their mobile devices to join the quiz.",
"sound": "Set the sound for the background music or the countdown here.",
......@@ -199,6 +203,10 @@
"connected": "connected",
"not_connected": "disconnected"
},
"indexedDb_status": {
"writable": "writable",
"non_writable": "not writable"
},
"localStorage_status": {
"writable": "writable",
"non_writable": "not writable"
......@@ -291,6 +299,15 @@
"choose_selectable_nick": "You can select one of the available nicknames from this list."
}
},
"membergroup-manager": {
"title": "Organize members in Teams",
"max-users": "Max users per Team",
"autojoin-to-team": "Add users automatically to Teams",
"create-team-placeholder": "Enter a Team name here...",
"create-team": "Add Team",
"current-teams": "Currently added Teams:",
"no-current-teams": "No Teams added yet"
},
"hashtag_management": {
"enter_quiz_name": "Enter the quiz name here",
"enter_server_password": "Enter the server password here",
......@@ -412,6 +429,7 @@
"no_nicks_selected": "No nicknames selected. All nicknames are allowed.",
"block_illegal_nicks": "Block inappropriate nicknames",
"restrict_to_cas": "Restrict to CAS users (THM University only)",
"filter": "Enter a search text here...",
"category": {
"disney": "Disney",
"science": "Science",
......
......@@ -9,6 +9,7 @@ export const DefaultSettings = {
httpLibEndpoint: environmentData.httpLibEndpoint,
serverEndpoint: environmentData.serverEndpoint,
wsApiEndpoint: environmentData.wsApiEndpoint,
jwtSecret: 'arsnova.click-v2',
defaultQuizSettings: {
answers: {
answerText: '',
......@@ -56,7 +57,7 @@ export const DefaultSettings = {
restrictToCasLogin: false,
selectedNicks: [],
},
theme: 'theme-Material',
theme: 'theme-SchroedelAktuell',
readingConfirmationEnabled: true,
showResponseProgress: true,
confidenceSliderEnabled: true,
......
......@@ -57,6 +57,10 @@ export class FooterbarElement implements IFooterBarElement {
this._isActive = value;
}
get queryParams(): object {
return this._queryParams;
}
private _restoreOnClickCallback: Function;
private readonly _id: string;
private readonly _iconClass: string;
......@@ -64,8 +68,9 @@ export class FooterbarElement implements IFooterBarElement {
private readonly _textName: string;
private readonly _selectable: boolean;
private readonly _showIntro: boolean;
private readonly _queryParams: object;
constructor({ id, iconClass, textClass, textName, selectable, showIntro, isActive, linkTarget }: IFooterBarElement, onClickCallback?: Function) {
constructor({ id, iconClass, textClass, textName, selectable, showIntro, isActive, linkTarget, queryParams }: IFooterBarElement, onClickCallback?: Function) {
this._id = id;
this._iconClass = iconClass;
this._textClass = textClass;
......@@ -81,6 +86,7 @@ export class FooterbarElement implements IFooterBarElement {
}
this._linkTarget = linkTarget;
this._queryParams = queryParams;
this._onClickCallback = onClickCallback;
}
......
......@@ -8,6 +8,7 @@ export declare interface IFooterBarElement {
selectable: boolean;
showIntro: boolean;
linkTarget?: Function | Array<string> | string;
queryParams?: object;
onClickCallback?: Function;
restoreClickCallback?: Function;
isActive?: Observable<boolean> | boolean;
......
import { DB_TABLE, STORAGE_KEY } from '../app/shared/enums';
export function jwtOptionsFactory(storageService) {
return {
tokenGetter: async () => {
const tokens = await storageService.read(DB_TABLE.CONFIG, STORAGE_KEY.TOKEN).toPromise();
return tokens ? tokens.staticLoginToken : null;
},
};
}
\ No newline at end of file