offline operator

This commit is contained in:
FinnStutzenstein 2019-03-04 11:45:15 +01:00
parent 118b853a91
commit 0c6a7b9c4b
18 changed files with 280 additions and 173 deletions

View File

@ -7,7 +7,6 @@ import { LoginLegalNoticeComponent } from './site/login/components/login-legal-n
import { LoginPrivacyPolicyComponent } from './site/login/components/login-privacy-policy/login-privacy-policy.component'; import { LoginPrivacyPolicyComponent } from './site/login/components/login-privacy-policy/login-privacy-policy.component';
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component'; import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component';
import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component'; import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
import { AppPreloader } from './shared/utils/app-preloader';
/** /**
* Global app routing * Global app routing
@ -30,8 +29,7 @@ const routes: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: AppPreloader })], imports: [RouterModule.forRoot(routes)],
exports: [RouterModule], exports: [RouterModule]
providers: [AppPreloader]
}) })
export class AppRoutingModule {} export class AppRoutingModule {}

View File

@ -42,8 +42,7 @@ export class AuthService {
/** /**
* Try to log in a user. * Try to log in a user.
* *
* Returns an observable 'user' with the correct login information or an error. * Returns an observable with the correct login information or an error.
* The user will then be stored in the {@link OperatorService},
* errors will be forwarded to the parents error function. * errors will be forwarded to the parents error function.
* *
* @param username * @param username
@ -56,18 +55,17 @@ export class AuthService {
password: password password: password
}; };
const response = await this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user); const response = await this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user);
this.operator.user = new User(response.user);
return response; return response;
} }
/** /**
* Logout function for both the client and the server. * Logout function for both the client and the server.
* *
* Will clear the current {@link OperatorService} and * Will clear the current operator and datastore and
* send a `post`-request to `/apps/users/logout/'` * send a `post`-request to `/apps/users/logout/'`. Restarts OpenSlides.
*/ */
public async logout(): Promise<void> { public async logout(): Promise<void> {
this.operator.user = null; await this.operator.setUser(null);
try { try {
await this.http.post(environment.urlPrefix + '/users/logout/', {}); await this.http.post(environment.urlPrefix + '/users/logout/', {});
} catch (e) { } catch (e) {

View File

@ -1,8 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DataStoreService } from './data-store.service';
import { WhoAmIResponse } from './operator.service';
/** /**
* This service handles everything connected with being offline. * This service handles everything connected with being offline.
* *
@ -20,20 +17,15 @@ export class OfflineService {
} }
/** /**
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
* @param DS
*/ */
public constructor(private DS: DataStoreService) {} public constructor() {}
/** /**
* Sets the offline flag. Restores the DataStoreService to the last known configuration. * Sets the offline flag. Restores the DataStoreService to the last known configuration.
*/ */
public async goOfflineBecauseFailedWhoAmI(): Promise<void> { public goOfflineBecauseFailedWhoAmI(): void {
this._offline = true; this._offline = true;
console.log('offline because whoami failed.'); console.log('offline because whoami failed.');
// TODO: Init the DS from cache.
await this.DS.clear();
} }
/** /**
@ -50,16 +42,4 @@ export class OfflineService {
public goOnline(): void { public goOnline(): void {
this._offline = false; this._offline = false;
} }
/**
* Returns the last cached WhoAmI response.
*/
public getLastWhoAmI(): WhoAmIResponse {
// TODO: use a cached WhoAmI response.
return {
user_id: null,
guest_enabled: false,
user: null
};
}
} }

View File

@ -2,10 +2,11 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { WebsocketService } from './websocket.service'; import { WebsocketService } from './websocket.service';
import { OperatorService } from './operator.service'; import { OperatorService, WhoAmIResponse } from './operator.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { AutoupdateService } from './autoupdate.service'; import { AutoupdateService } from './autoupdate.service';
import { DataStoreService } from './data-store.service'; import { DataStoreService } from './data-store.service';
import { take } from 'rxjs/operators';
/** /**
* Handles the bootup/showdown of this application. * Handles the bootup/showdown of this application.
@ -51,8 +52,11 @@ export class OpenSlidesService {
*/ */
public async bootup(): Promise<void> { public async bootup(): Promise<void> {
// start autoupdate if the user is logged in: // start autoupdate if the user is logged in:
const response = await this.operator.whoAmI(); const response = await this.operator.whoAmIFromStorage();
this.operator.guestsEnabled = response.guest_enabled; await this.bootupWithWhoAmI(response);
}
private async bootupWithWhoAmI(response: WhoAmIResponse): Promise<void> {
if (!response.user && !response.guest_enabled) { if (!response.user && !response.guest_enabled) {
this.redirectUrl = location.pathname; this.redirectUrl = location.pathname;
@ -66,8 +70,14 @@ export class OpenSlidesService {
// Goto login, if the user isn't login and guests are not allowed // Goto login, if the user isn't login and guests are not allowed
this.router.navigate(['/login']); this.router.navigate(['/login']);
} }
this.checkOperator(false);
} else { } else {
await this.afterLoginBootup(response.user_id); await this.afterLoginBootup(response.user_id);
// Check for the operator via a async whoami (so no await here)
// to validate, that the cache was correct.
this.checkOperator(false);
} }
} }
@ -78,7 +88,7 @@ export class OpenSlidesService {
* @param userId * @param userId
*/ */
public async afterLoginBootup(userId: number): Promise<void> { public async afterLoginBootup(userId: number): Promise<void> {
// Else, check, which user was logged in last time // Check, which user was logged in last time
const lastUserId = await this.storageService.get<number>('lastUserLoggedIn'); const lastUserId = await this.storageService.get<number>('lastUserLoggedIn');
// if the user changed, reset the cache and save the new user. // if the user changed, reset the cache and save the new user.
if (userId !== lastUserId) { if (userId !== lastUserId) {
@ -96,33 +106,42 @@ export class OpenSlidesService {
if (changeId > 0) { if (changeId > 0) {
changeId += 1; changeId += 1;
} }
// disconnect the WS connection, if there was one. This is needed
// to update the connection parameters, namely the cookies. If the user
// is changed, the WS needs to reconnect, so the new connection holds the new
// user information.
if (this.websocketService.isConnected) {
this.websocketService.close();
// Wait for the disconnect.
await this.websocketService.closeEvent.pipe(take(1)).toPromise();
}
this.websocketService.connect({ changeId: changeId }); // Request changes after changeId. this.websocketService.connect({ changeId: changeId }); // Request changes after changeId.
} }
/** /**
* Shuts OpenSlides down. The websocket is closed and the operator is not set. * Shuts OpenSlides down. The websocket is closed and the operator is not set.
*/ */
public shutdown(): void { public async shutdown(): Promise<void> {
this.websocketService.close(); this.websocketService.close();
this.operator.user = null; await this.operator.setUser(null);
} }
/** /**
* Shutdown and bootup. * Shutdown and bootup.
*/ */
public async reboot(): Promise<void> { public async reboot(): Promise<void> {
this.shutdown(); await this.shutdown();
await this.bootup(); await this.bootup();
} }
/** /**
* Verify that the operator is the same as it was before. Should be alled on a reconnect. * Verify that the operator is the same as it was before. Should be alled on a reconnect.
*/ */
private async checkOperator(): Promise<void> { private async checkOperator(requestChanges: boolean = true): Promise<void> {
const response = await this.operator.whoAmI(); const response = await this.operator.whoAmI();
// User logged off. // User logged off.
if (!response.user && !response.guest_enabled) { if (!response.user && !response.guest_enabled) {
this.shutdown(); await this.shutdown();
this.router.navigate(['/login']); this.router.navigate(['/login']);
} else { } else {
if ( if (
@ -130,8 +149,9 @@ export class OpenSlidesService {
(!this.operator.user && response.user_id) (!this.operator.user && response.user_id)
) { ) {
// user changed // user changed
await this.DS.clear();
await this.reboot(); await this.reboot();
} else { } else if (requestChanges) {
// User is still the same, but check for missed autoupdates. // User is still the same, but check for missed autoupdates.
this.autoupdateService.requestChanges(); this.autoupdateService.requestChanges();
} }

View File

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
@ -11,6 +10,9 @@ import { OfflineService } from './offline.service';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { OnAfterAppsLoaded } from '../onAfterAppsLoaded'; import { OnAfterAppsLoaded } from '../onAfterAppsLoaded';
import { UserRepositoryService } from '../repositories/users/user-repository.service'; import { UserRepositoryService } from '../repositories/users/user-repository.service';
import { CollectionStringMapperService } from './collectionStringMapper.service';
import { StorageService } from './storage.service';
import { HttpService } from './http.service';
/** /**
* Permissions on the client are just strings. This makes clear, that * Permissions on the client are just strings. This makes clear, that
@ -19,7 +21,7 @@ import { UserRepositoryService } from '../repositories/users/user-repository.ser
export type Permission = string; export type Permission = string;
/** /**
* Response format of the WHoAMI request. * Response format of the WhoAmI request.
*/ */
export interface WhoAmIResponse { export interface WhoAmIResponse {
user_id: number; user_id: number;
@ -27,6 +29,16 @@ export interface WhoAmIResponse {
user: User; user: User;
} }
function isWhoAmIResponse(obj: any): obj is WhoAmIResponse {
if (!obj) {
return false;
}
const whoAmI = obj as WhoAmIResponse;
return whoAmI.guest_enabled !== undefined && whoAmI.user !== undefined && whoAmI.user_id !== undefined;
}
const WHOAMI_STORAGE_KEY = 'whoami';
/** /**
* The operator represents the user who is using OpenSlides. * The operator represents the user who is using OpenSlides.
* *
@ -42,6 +54,10 @@ export class OperatorService implements OnAfterAppsLoaded {
*/ */
private _user: User; private _user: User;
public get user(): User {
return this._user;
}
/** /**
* The operator as a view user. We need a separation here, because * The operator as a view user. We need a separation here, because
* we need to acces the operators permissions, before we get data * we need to acces the operators permissions, before we get data
@ -49,13 +65,6 @@ export class OperatorService implements OnAfterAppsLoaded {
*/ */
private _viewUser: ViewUser; private _viewUser: ViewUser;
/**
* Get the user that corresponds to operator.
*/
public get user(): User {
return this._user;
}
/** /**
* Get the user that corresponds to operator. * Get the user that corresponds to operator.
*/ */
@ -63,23 +72,16 @@ export class OperatorService implements OnAfterAppsLoaded {
return this._viewUser; return this._viewUser;
} }
/**
* Sets the current operator.
*
* The permissions are updated and the new user published.
*/
public set user(user: User) {
this.updateUser(user);
}
public get isAnonymous(): boolean { public get isAnonymous(): boolean {
return !this.user || this.user.id === 0; return !this.user || this.user.id === 0;
} }
/** /**
* Save, if quests are enabled. * Save, if guests are enabled.
*/ */
public guestsEnabled: boolean; public get guestsEnabled(): boolean {
return this.currentWhoAmIResponse ? this.currentWhoAmIResponse.guest_enabled : false;
}
/** /**
* The permissions of the operator. Updated via {@method updatePermissions}. * The permissions of the operator. Updated via {@method updatePermissions}.
@ -99,21 +101,27 @@ export class OperatorService implements OnAfterAppsLoaded {
/** /**
* Do not access the repo before it wasn't loaded. Will be true after `onAfterAppsLoaded`. * Do not access the repo before it wasn't loaded. Will be true after `onAfterAppsLoaded`.
*/ */
private userRepoLoaded = false; private userRepository: UserRepositoryService | null;
/**
* The current WhoAmI response to extract the user (the operator) from.
*/
private currentWhoAmIResponse: WhoAmIResponse | null;
/** /**
* Sets up an observer for watching changes in the DS. If the operator user or groups are changed, * Sets up an observer for watching changes in the DS. If the operator user or groups are changed,
* the operator's permissions are updated. * the operator's permissions are updated.
* *
* @param http HttpClient * @param http
* @param DS * @param DS
* @param offlineService * @param offlineService
*/ */
public constructor( public constructor(
private http: HttpClient, private http: HttpService,
private DS: DataStoreService, private DS: DataStoreService,
private offlineService: OfflineService, private offlineService: OfflineService,
private userRepository: UserRepositoryService private collectionStringMapper: CollectionStringMapperService,
private storageService: StorageService
) { ) {
this.DS.changeObservable.subscribe(newModel => { this.DS.changeObservable.subscribe(newModel => {
if (this._user) { if (this._user) {
@ -131,24 +139,60 @@ export class OperatorService implements OnAfterAppsLoaded {
}); });
} }
/**
* Gets the current WHoAmI response from the storage.
*/
public async whoAmIFromStorage(): Promise<WhoAmIResponse> {
const defaultResponse = {
user_id: null,
guest_enabled: false,
user: null
};
let response: WhoAmIResponse;
try {
response = await this.storageService.get<WhoAmIResponse>(WHOAMI_STORAGE_KEY);
if (response) {
this.processWhoAmIResponse(response);
} else {
response = defaultResponse;
}
} catch (e) {
response = defaultResponse;
}
this.currentWhoAmIResponse = response;
return this.currentWhoAmIResponse;
}
/** /**
* Load the repo to get a view user. * Load the repo to get a view user.
*/ */
public onAfterAppsLoaded(): void { public onAfterAppsLoaded(): void {
this.userRepoLoaded = true; this.userRepository = this.collectionStringMapper.getRepository(ViewUser) as UserRepositoryService;
if (this.user) { if (this.user) {
this._viewUser = this.userRepository.getViewModel(this.user.id); this._viewUser = this.userRepository.getViewModel(this.user.id);
} }
} }
/**
* Sets the operator user. Will be saved to storage
* @param user The new operator.
*/
public async setUser(user: User): Promise<void> {
await this.updateUser(user, true);
}
/** /**
* Updates the user and update the permissions. * Updates the user and update the permissions.
* *
* @param user The user to set. * @param user The user to set.
* @param saveToStoare Whether to save the user to the storage WhoAmI.
*/ */
private updateUser(user: User | null): void { private async updateUser(user: User | null, saveToStorage: boolean = false): Promise<void> {
this._user = user; this._user = user;
if (user && this.userRepoLoaded) { if (saveToStorage) {
await this.saveUserToStorate();
}
if (user && this.userRepository) {
this._viewUser = this.userRepository.getViewModel(user.id); this._viewUser = this.userRepository.getViewModel(user.id);
} else { } else {
this._viewUser = null; this._viewUser = null;
@ -158,33 +202,68 @@ export class OperatorService implements OnAfterAppsLoaded {
/** /**
* Calls `/apps/users/whoami` to find out the real operator. * Calls `/apps/users/whoami` to find out the real operator.
*
* @returns The response of the WhoAmI request. * @returns The response of the WhoAmI request.
*/ */
public async whoAmI(): Promise<WhoAmIResponse> { public async whoAmI(): Promise<WhoAmIResponse> {
try { try {
const response = await this.http.get<WhoAmIResponse>(environment.urlPrefix + '/users/whoami/').toPromise(); const response = await this.http.get(environment.urlPrefix + '/users/whoami/');
if (response && response.user) { if (isWhoAmIResponse(response)) {
this.user = new User(response.user); this.processWhoAmIResponse(response);
await this.storageService.set(WHOAMI_STORAGE_KEY, response);
this.currentWhoAmIResponse = response;
} else {
this.offlineService.goOfflineBecauseFailedWhoAmI();
} }
return response;
} catch (e) { } catch (e) {
// TODO: Implement the offline service. Currently a guest-whoami response is returned and
// the DS cleared.
this.offlineService.goOfflineBecauseFailedWhoAmI(); this.offlineService.goOfflineBecauseFailedWhoAmI();
return this.offlineService.getLastWhoAmI();
} }
return this.currentWhoAmIResponse;
} }
/** /**
* Returns the operatorSubject as an observable. * Saves the user to storage by wrapping it into a (maybe existing)
* WhoAMI response.
*/
private async saveUserToStorate(): Promise<void> {
if (!this.currentWhoAmIResponse) {
this.currentWhoAmIResponse = {
user_id: null,
guest_enabled: false,
user: null
};
}
if (this.user) {
this.currentWhoAmIResponse.user_id = this.user.id;
this.currentWhoAmIResponse.user = this.user;
} else {
this.currentWhoAmIResponse.user_id = null;
this.currentWhoAmIResponse.user = null;
}
await this.storageService.set(WHOAMI_STORAGE_KEY, this.currentWhoAmIResponse);
}
/**
* Processes a WhoAmI response and set the user appropriately.
* *
* Services an components can use it to get informed when something changes in * @param response The WhoAMI response
* the operator */
private processWhoAmIResponse(response: WhoAmIResponse): void {
this.updateUser(response.user ? new User(response.user) : null);
}
/**
* @returns an observable for the operator as a user.
*/ */
public getUserObservable(): Observable<User> { public getUserObservable(): Observable<User> {
return this.operatorSubject.asObservable(); return this.operatorSubject.asObservable();
} }
/**
* @returns an observable for the operator as a viewUser. Note, that
* the viewUser might not be there, so for reliable (and not display) information,
* use the `getUserObservable`.
*/
public getViewUserObservable(): Observable<ViewUser> { public getViewUserObservable(): Observable<ViewUser> {
return this.viewOperatorSubject.asObservable(); return this.viewOperatorSubject.asObservable();
} }

View File

@ -16,6 +16,8 @@ import { environment } from 'environments/environment.prod';
providedIn: 'root' providedIn: 'root'
}) })
export class ServertimeService { export class ServertimeService {
// TODO: couple this with the offlineService: Just retry often, if we are online.
// When we are offline, this is not necessary.
private static FAILURE_TIMEOUT = 30; private static FAILURE_TIMEOUT = 30;
private static NORMAL_TIMEOUT = 60 * 5; private static NORMAL_TIMEOUT = 60 * 5;

View File

@ -46,10 +46,6 @@ export class WebsocketService {
*/ */
private _connectEvent: EventEmitter<void> = new EventEmitter<void>(); private _connectEvent: EventEmitter<void> = new EventEmitter<void>();
private connectionOpen = false;
private sendQueueWhileNotConnected: string[] = [];
/** /**
* Getter for the connect event. * Getter for the connect event.
*/ */
@ -57,6 +53,32 @@ export class WebsocketService {
return this._connectEvent; return this._connectEvent;
} }
/**
* Listeners will be nofitied, if the wesocket connection is closed.
*/
private _closeEvent: EventEmitter<void> = new EventEmitter<void>();
/**
* Getter for the close event.
*/
public get closeEvent(): EventEmitter<void> {
return this._closeEvent;
}
/**
* Saves, if the connection is open
*/
private _connectionOpen = false;
/**
* Whether the WebSocket connection is established
*/
public get isConnected(): boolean {
return this._connectionOpen;
}
private sendQueueWhileNotConnected: string[] = [];
/** /**
* The websocket. * The websocket.
*/ */
@ -134,7 +156,7 @@ export class WebsocketService {
this._reconnectEvent.emit(); this._reconnectEvent.emit();
} }
this._connectEvent.emit(); this._connectEvent.emit();
this.connectionOpen = true; this._connectionOpen = true;
this.sendQueueWhileNotConnected.forEach(entry => { this.sendQueueWhileNotConnected.forEach(entry => {
this.websocket.send(entry); this.websocket.send(entry);
}); });
@ -160,8 +182,9 @@ export class WebsocketService {
this.websocket.onclose = (event: CloseEvent) => { this.websocket.onclose = (event: CloseEvent) => {
this.zone.run(() => { this.zone.run(() => {
this.websocket = null; this.websocket = null;
this.connectionOpen = false; this._connectionOpen = false;
if (event.code !== 1000) { if (event.code !== 1000) {
console.error(event);
// Do not show the message snackbar on the projector // Do not show the message snackbar on the projector
// tests for /projector and /projector/<id> // tests for /projector and /projector/<id>
const onProjector = this.router.url.match(/^\/projector(\/[0-9]+\/?)?$/); const onProjector = this.router.url.match(/^\/projector(\/[0-9]+\/?)?$/);
@ -182,6 +205,7 @@ export class WebsocketService {
this.connect({ enableAutoupdates: true }); this.connect({ enableAutoupdates: true });
}, timeout); }, timeout);
} }
this._closeEvent.emit();
}); });
}; };
} }
@ -235,7 +259,7 @@ export class WebsocketService {
// Either send directly or add to queue, if not connected. // Either send directly or add to queue, if not connected.
const jsonMessage = JSON.stringify(message); const jsonMessage = JSON.stringify(message);
if (this.connectionOpen) { if (this.isConnected) {
this.websocket.send(jsonMessage); this.websocket.send(jsonMessage);
} else { } else {
this.sendQueueWhileNotConnected.push(jsonMessage); this.sendQueueWhileNotConnected.push(jsonMessage);

View File

@ -1,27 +0,0 @@
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
/**
* Custom preload strategy class
*
* @example
* ```
* {
* path: 'agenda',
* loadChildren: './agenda/agenda.module#AgendaModule',
* data: { preload: true }
* }
* ```
*/
export class AppPreloader implements PreloadingStrategy {
/**
* Custom preload function.
* Can be add to routes as data argument.
*
* @param route The route to load
* @param load The load function
*/
public preload(route: Route, load: Function): Observable<any> {
return route.data && route.data.preload ? load() : of(null);
}
}

View File

@ -1,8 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ConfigRepositoryService, ConfigGroup } from '../../../../core/repositories/config/config-repository.service';
import { BaseComponent } from '../../../../base.component'; import { ConfigRepositoryService, ConfigGroup } from 'app/core/repositories/config/config-repository.service';
import { BaseComponent } from 'app/base.component';
/** /**
* List view for the global settings * List view for the global settings

View File

@ -1,7 +1,7 @@
import { Component, forwardRef } from '@angular/core'; import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CustomTranslation, CustomTranslations } from 'app/core/translate/translation-parser'; import { CustomTranslation, CustomTranslations } from 'app/core/translate/translation-parser';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
/** /**
* Custom translations as custom form component * Custom translations as custom form component

View File

@ -1,10 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { History } from 'app/shared/models/core/history'; import { History } from 'app/shared/models/core/history';
import { HistoryRepositoryService } from 'app/core/repositories/history/history-repository.service'; import { HistoryRepositoryService } from 'app/core/repositories/history/history-repository.service';

View File

@ -1,16 +1,19 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { AuthService } from 'app/core/core-services/auth.service'; import { AuthService } from 'app/core/core-services/auth.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { OpenSlidesService } from 'app/core/core-services/openslides.service'; import { OpenSlidesService } from 'app/core/core-services/openslides.service';
import { LoginDataService } from 'app/core/ui-services/login-data.service'; import { LoginDataService } from 'app/core/ui-services/login-data.service';
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher'; import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { User } from 'app/shared/models/users/user';
/** /**
* Login mask component. * Login mask component.
@ -22,7 +25,7 @@ import { HttpService } from 'app/core/core-services/http.service';
templateUrl: './login-mask.component.html', templateUrl: './login-mask.component.html',
styleUrls: ['./login-mask.component.scss'] styleUrls: ['./login-mask.component.scss']
}) })
export class LoginMaskComponent extends BaseComponent implements OnInit { export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestroy {
/** /**
* Show or hide password and change the indicator accordingly * Show or hide password and change the indicator accordingly
*/ */
@ -53,6 +56,8 @@ export class LoginMaskComponent extends BaseComponent implements OnInit {
*/ */
public inProcess = false; public inProcess = false;
public operatorSubscription: Subscription | null;
/** /**
* Constructor for the login component * Constructor for the login component
* *
@ -99,6 +104,32 @@ export class LoginMaskComponent extends BaseComponent implements OnInit {
}, },
() => {} () => {}
); );
// Maybe the operator changes and the user is logged in. If so, redirect him and boot OpenSlides.
this.operatorSubscription = this.operator.getUserObservable().subscribe(user => {
if (user) {
this.clearOperatorSubscription();
this.redirectUser();
this.OpenSlides.afterLoginBootup(user.id);
}
});
}
/**
* Clear the subscription on destroy.
*/
public ngOnDestroy(): void {
this.clearOperatorSubscription();
}
/**
* Clears the subscription to the operator.
*/
private clearOperatorSubscription(): void {
if (this.operatorSubscription) {
this.operatorSubscription.unsubscribe();
this.operatorSubscription = null;
}
} }
/** /**
@ -120,14 +151,12 @@ export class LoginMaskComponent extends BaseComponent implements OnInit {
this.loginErrorMsg = ''; this.loginErrorMsg = '';
this.inProcess = true; this.inProcess = true;
try { try {
const res = await this.authService.login(this.loginForm.value.username, this.loginForm.value.password); const response = await this.authService.login(this.loginForm.value.username, this.loginForm.value.password);
this.clearOperatorSubscription(); // We take control, not the subscription.
this.inProcess = false; this.inProcess = false;
this.OpenSlides.afterLoginBootup(res.user_id); await this.operator.setUser(new User(response.user));
let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/'; this.OpenSlides.afterLoginBootup(response.user_id);
if (redirect.includes('login')) { this.redirectUser();
redirect = '/';
}
this.router.navigate([redirect]);
} catch (e) { } catch (e) {
this.loginForm.setErrors({ this.loginForm.setErrors({
notFound: true notFound: true
@ -137,6 +166,17 @@ export class LoginMaskComponent extends BaseComponent implements OnInit {
} }
} }
/**
* Redirects the user to the page where he came from.
*/
private redirectUser(): void {
let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/';
if (redirect.includes('login')) {
redirect = '/';
}
this.router.navigate([redirect]);
}
/** /**
* Go to the reset password view * Go to the reset password view
*/ */

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component'; import { BaseComponent } from '../../../../base.component';

View File

@ -1,13 +1,14 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component'; import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { ActivatedRoute, Router } from '@angular/router'; import { HttpService } from 'app/core/core-services/http.service';
import { MatSnackBar } from '@angular/material';
/** /**
* Reset password component. * Reset password component.
@ -40,7 +41,7 @@ export class ResetPasswordConfirmComponent extends BaseComponent implements OnIn
public constructor( public constructor(
protected titleService: Title, protected titleService: Title,
protected translate: TranslateService, protected translate: TranslateService,
private http: HttpClient, private http: HttpService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
@ -88,13 +89,11 @@ export class ResetPasswordConfirmComponent extends BaseComponent implements OnIn
} }
try { try {
await this.http await this.http.post<void>(environment.urlPrefix + '/users/reset-password-confirm/', {
.post<void>(environment.urlPrefix + '/users/reset-password-confirm/', { user_id: this.user_id,
user_id: this.user_id, token: this.token,
token: this.token, password: this.newPasswordForm.get('password').value
password: this.newPasswordForm.get('password').value });
})
.toPromise();
// TODO: Does we get a response for displaying? // TODO: Does we get a response for displaying?
this.matSnackBar.open( this.matSnackBar.open(
this.translate.instant('Your password was resetted successfully!'), this.translate.instant('Your password was resetted successfully!'),

View File

@ -1,14 +1,15 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { environment } from 'environments/environment';
import { BaseComponent } from '../../../../base.component';
import { HttpService } from 'app/core/core-services/http.service';
/** /**
* Reset password component. * Reset password component.
* *
@ -30,7 +31,7 @@ export class ResetPasswordComponent extends BaseComponent implements OnInit {
public constructor( public constructor(
protected titleService: Title, protected titleService: Title,
protected translate: TranslateService, protected translate: TranslateService,
private http: HttpClient, private http: HttpService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
private matSnackBar: MatSnackBar, private matSnackBar: MatSnackBar,
private router: Router private router: Router
@ -57,11 +58,9 @@ export class ResetPasswordComponent extends BaseComponent implements OnInit {
} }
try { try {
await this.http await this.http.post<void>(environment.urlPrefix + '/users/reset-password/', {
.post<void>(environment.urlPrefix + '/users/reset-password/', { email: this.resetPasswordForm.get('email').value
email: this.resetPasswordForm.get('email').value });
})
.toPromise();
// TODO: Does we get a response for displaying? // TODO: Does we get a response for displaying?
this.matSnackBar.open( this.matSnackBar.open(
this.translate.instant('An email with a password reset link was send!'), this.translate.instant('An email with a password reset link was send!'),

View File

@ -16,53 +16,43 @@ const routes: Routes = [
children: [ children: [
{ {
path: '', path: '',
loadChildren: './common/os-common.module#OsCommonModule', loadChildren: './common/os-common.module#OsCommonModule'
data: { preload: true }
}, },
{ {
path: 'agenda', path: 'agenda',
loadChildren: './agenda/agenda.module#AgendaModule', loadChildren: './agenda/agenda.module#AgendaModule'
data: { preload: true }
}, },
{ {
path: 'assignments', path: 'assignments',
loadChildren: './assignments/assignments.module#AssignmentsModule', loadChildren: './assignments/assignments.module#AssignmentsModule'
data: { preload: true }
}, },
{ {
path: 'mediafiles', path: 'mediafiles',
loadChildren: './mediafiles/mediafiles.module#MediafilesModule', loadChildren: './mediafiles/mediafiles.module#MediafilesModule'
data: { preload: true }
}, },
{ {
path: 'motions', path: 'motions',
loadChildren: './motions/motions.module#MotionsModule', loadChildren: './motions/motions.module#MotionsModule'
data: { preload: true }
}, },
{ {
path: 'settings', path: 'settings',
loadChildren: './config/config.module#ConfigModule', loadChildren: './config/config.module#ConfigModule'
data: { preload: true }
}, },
{ {
path: 'users', path: 'users',
loadChildren: './users/users.module#UsersModule', loadChildren: './users/users.module#UsersModule'
data: { preload: true }
}, },
{ {
path: 'tags', path: 'tags',
loadChildren: './tags/tag.module#TagModule', loadChildren: './tags/tag.module#TagModule'
data: { preload: true }
}, },
{ {
path: 'history', path: 'history',
loadChildren: './history/history.module#HistoryModule', loadChildren: './history/history.module#HistoryModule'
data: { preload: true }
}, },
{ {
path: 'projectors', path: 'projectors',
loadChildren: './projector/projector.module#ProjectorModule', loadChildren: './projector/projector.module#ProjectorModule'
data: { preload: true }
} }
], ],
canActivateChild: [AuthGuard] canActivateChild: [AuthGuard]

View File

@ -36,7 +36,7 @@
<span> {{ getLangName(this.translate.currentLang) }} </span> <span> {{ getLangName(this.translate.currentLang) }} </span>
</a> </a>
<div *ngIf="isLoggedIn"> <div *ngIf="isLoggedIn">
<a [routerLink]="['/users/', operator.user.id]" (click)="mobileAutoCloseNav()" mat-list-item> <a [routerLink]="operator.user ? ['/users/', operator.user.id] : []" (click)="mobileAutoCloseNav()" mat-list-item>
<mat-icon>person</mat-icon> <mat-icon>person</mat-icon>
<span translate>Show profile</span> <span translate>Show profile</span>
</a> </a>

View File

@ -99,7 +99,9 @@ export class SiteComponent extends BaseComponent implements OnInit {
// Scroll to top if accessing a page, not via browser history stack // Scroll to top if accessing a page, not via browser history stack
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
const contentContainer = document.querySelector('.mat-sidenav-content'); const contentContainer = document.querySelector('.mat-sidenav-content');
contentContainer.scrollTo(0, 0); if (contentContainer) {
contentContainer.scrollTo(0, 0);
}
} }
}); });
} }