From 4e41e8c603b77dea5bdd4304240da8f40474d34e Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Tue, 28 Aug 2018 11:07:10 +0200 Subject: [PATCH] Auth/Operator/Anonymous/WS-Stuff - operator is more lightweight - auth and auth-guard service updated - anonymous in login form - improved login form - websocket retries to reconnect --- client/src/app/app-routing.module.ts | 3 +- client/src/app/app.component.ts | 42 ++- client/src/app/core/rootInjector.ts | 6 - .../app/core/services/auth-guard.service.ts | 40 +-- client/src/app/core/services/auth.service.ts | 41 ++- .../app/core/services/autoupdate.service.ts | 11 + .../app/core/services/data-store.service.ts | 94 +++--- .../core/services/openslides.service.spec.ts | 15 + .../app/core/services/openslides.service.ts | 135 ++++++++ .../src/app/core/services/operator.service.ts | 304 ++++++------------ .../app/core/services/websocket.service.ts | 135 ++++++-- client/src/app/openslides.component.ts | 8 +- .../shared/directives/os-perms.directive.ts | 70 ++-- client/src/app/shared/models/users/user.ts | 34 +- .../agenda-list/agenda-list.component.html | 2 +- .../src/app/site/login/login.component.html | 7 +- client/src/app/site/login/login.component.ts | 89 +++-- .../settings-list.component.html | 2 +- client/src/app/site/site-routing.module.ts | 4 +- client/src/app/site/site.component.html | 26 +- client/src/app/site/site.component.ts | 64 ++-- .../src/app/site/start/start.component.html | 3 - client/src/app/site/start/start.component.ts | 1 - client/src/main.ts | 4 +- 24 files changed, 689 insertions(+), 451 deletions(-) delete mode 100644 client/src/app/core/rootInjector.ts create mode 100644 client/src/app/core/services/openslides.service.spec.ts create mode 100644 client/src/app/core/services/openslides.service.ts diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 7481f849d..1d9cd898a 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LoginComponent } from './site/login/login.component'; -import { AuthGuard } from './core/services/auth-guard.service'; /** * Global app routing @@ -9,7 +8,7 @@ import { AuthGuard } from './core/services/auth-guard.service'; const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'projector', loadChildren: './projector-container/projector-container.module#ProjectorContainerModule' }, - { path: '', loadChildren: './site/site.module#SiteModule', canActivate: [AuthGuard] }, + { path: '', loadChildren: './site/site.module#SiteModule' }, { path: '**', redirectTo: '' } ]; diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 979e50cb9..ce5e2483f 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,7 +1,12 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Injector, NgModuleRef } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AutoupdateService } from './core/services/autoupdate.service'; import { NotifyService } from './core/services/notify.service'; +import { OperatorService } from './core/services/operator.service'; +import { Subject } from 'rxjs'; +import { AppModule } from './app.module'; +import { OpenSlidesComponent } from './openslides.component'; +import { OpenSlidesService } from './core/services/openslides.service'; /** * Angular's global App Component @@ -13,7 +18,21 @@ import { NotifyService } from './core/services/notify.service'; }) export class AppComponent { /** - * Initialises the operator, the auto update (and therefore a websocket) feature and the translation unit. + * This subject gets called, when the bootstrapping of the hole application is done. + */ + private static bootstrapDoneSubject: Subject> = new Subject>(); + + /** + * This function should only be called, when the bootstrapping is done with a reference to + * the bootstrapped module. + * @param moduleRef Reference to the bootstrapped AppModule + */ + public static bootstrapDone(moduleRef: NgModuleRef) { + AppComponent.bootstrapDoneSubject.next(moduleRef); + } + + /** + * Initialises the translation unit. * @param autoupdateService * @param notifyService * @param translate @@ -21,7 +40,9 @@ export class AppComponent { constructor( private autoupdateService: AutoupdateService, private notifyService: NotifyService, - private translate: TranslateService + private translate: TranslateService, + private operator: OperatorService, + private OpenSlides: OpenSlidesService ) { // manually add the supported languages translate.addLangs(['en', 'de', 'fr']); @@ -31,5 +52,20 @@ export class AppComponent { const browserLang = translate.getBrowserLang(); // try to use the browser language if it is available. If not, uses english. translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); + + AppComponent.bootstrapDoneSubject.asObservable().subscribe(this.setup.bind(this)); + } + + /** + * Gets called, when bootstrapping is done. Gets the root injector, sets up the operator and starts OpenSlides. + * @param moduleRef + */ + private setup(moduleRef: NgModuleRef): void { + OpenSlidesComponent.injector = moduleRef.injector; + + // Setup the operator after the root injector is known. + this.operator.setupSubscription(); + + this.OpenSlides.bootup(); // Yeah! } } diff --git a/client/src/app/core/rootInjector.ts b/client/src/app/core/rootInjector.ts deleted file mode 100644 index 56b1af684..000000000 --- a/client/src/app/core/rootInjector.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injector } from '@angular/core'; - -export class RootInjector { - constructor() {} - public static injector: Injector; -} diff --git a/client/src/app/core/services/auth-guard.service.ts b/client/src/app/core/services/auth-guard.service.ts index 60aa9cbbe..6a2dce8d7 100644 --- a/client/src/app/core/services/auth-guard.service.ts +++ b/client/src/app/core/services/auth-guard.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; -import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; -import { AuthService } from './auth.service'; import { OperatorService } from './operator.service'; /** @@ -10,35 +9,40 @@ import { OperatorService } from './operator.service'; @Injectable({ providedIn: 'root' }) -export class AuthGuard implements CanActivate { +export class AuthGuard implements CanActivate, CanActivateChild { /** - * Initialises the authentication, the operator and the Router - * @param authService * @param operator - * @param router */ - constructor(private authService: AuthService, private operator: OperatorService, private router: Router) {} + constructor(private operator: OperatorService) {} /** - * Checks of the operator has n id. - * If so, forward to the desired target. + * Checks of the operator has the required permission to see the state. * - * If not, forward to login. - * - * TODO: Test if this works for guests and on Projector + * One can set extra data to the state with `data: {basePerm: ''}` or + * `data: {basePerm: ['', '']}` to lock the access to users + * only with the given permission(s). * * @param route required by `canActivate()` * @param state the state (URL) that the user want to access */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any { - const url: string = state.url; + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + const basePerm: string | string[] = route.data.basePerm; - if (this.operator.id) { + if (!basePerm) { return true; + } else if (basePerm instanceof Array) { + return this.operator.hasPerms(...basePerm); } else { - this.authService.redirectUrl = url; - this.router.navigate(['/login']); - return false; + return this.operator.hasPerms(basePerm); } } + + /** + * Calls {@method canActivate}. Should have the same logic. + * @param route + * @param state + */ + canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + return this.canActivate(route, state); + } } diff --git a/client/src/app/core/services/auth.service.ts b/client/src/app/core/services/auth.service.ts index b418ffc52..93290626e 100644 --- a/client/src/app/core/services/auth.service.ts +++ b/client/src/app/core/services/auth.service.ts @@ -6,6 +6,16 @@ import { catchError, tap } from 'rxjs/operators'; import { OperatorService } from 'app/core/services/operator.service'; import { OpenSlidesComponent } from '../../openslides.component'; import { environment } from 'environments/environment'; +import { User } from '../../shared/models/users/user'; +import { OpenSlidesService } from './openslides.service'; + +/** + * The data returned by a post request to the login route. + */ +interface LoginResponse { + user_id: number; + user: User; +} /** * Authenticates an OpenSlides user with username and password @@ -14,11 +24,6 @@ import { environment } from 'environments/environment'; providedIn: 'root' }) export class AuthService extends OpenSlidesComponent { - /** - * if the user tries to access a certain URL without being authenticated, the URL will be stored here - */ - redirectUrl: string; - /** * Initializes the httpClient and the {@link OperatorService}. * @@ -26,7 +31,7 @@ export class AuthService extends OpenSlidesComponent { * @param http HttpClient * @param operator who is using OpenSlides */ - constructor(private http: HttpClient, private operator: OperatorService) { + constructor(private http: HttpClient, private operator: OperatorService, private OpenSlides: OpenSlidesService) { super(); } @@ -40,27 +45,29 @@ export class AuthService extends OpenSlidesComponent { * @param username * @param password */ - login(username: string, password: string): Observable { - const user: any = { + public login(username: string, password: string): Observable { + const user = { username: username, password: password }; - return this.http.post(environment.urlPrefix + '/users/login/', user).pipe( - tap(resp => this.operator.storeUser(resp.user)), + return this.http.post(environment.urlPrefix + '/users/login/', user).pipe( + tap((response: LoginResponse) => { + this.operator.user = new User().deserialize(response.user); + }), catchError(this.handleError()) - ); + ) as Observable; } /** * Logout function for both the client and the server. * * Will clear the current {@link OperatorService} and - * send a `post`-requiest to `/users/logout/'` + * send a `post`-request to `/apps/users/logout/'` */ - //logout the user - //TODO not yet used - logout(): Observable { - this.operator.clear(); - return this.http.post(environment.urlPrefix + '/users/logout/', {}); + public logout(): void { + this.operator.user = null; + this.http.post(environment.urlPrefix + '/users/logout/', {}).subscribe(() => { + this.OpenSlides.reboot(); + }); } } diff --git a/client/src/app/core/services/autoupdate.service.ts b/client/src/app/core/services/autoupdate.service.ts index cd3127f87..66449e1f5 100644 --- a/client/src/app/core/services/autoupdate.service.ts +++ b/client/src/app/core/services/autoupdate.service.ts @@ -69,4 +69,15 @@ export class AutoupdateService extends OpenSlidesComponent { this.DS.add(...autoupdate.changed[collection].map(_obj => new targetClass().deserialize(_obj.data))); }); } + + /** + * Sends a WebSocket request to the Server with the maxChangeId of the DataStore. + * The server should return an autoupdate with all new data. + * + * TODO: Wait for changeIds to be implemented on the server. + */ + public requestChanges() { + console.log('requesting changed objects'); + // this.websocketService.send('changeIdRequest', this.DS.maxChangeId); + } } diff --git a/client/src/app/core/services/data-store.service.ts b/client/src/app/core/services/data-store.service.ts index 88bb1a616..0858ff60d 100644 --- a/client/src/app/core/services/data-store.service.ts +++ b/client/src/app/core/services/data-store.service.ts @@ -10,14 +10,14 @@ import { CollectionStringModelMapperService } from './collectionStringModelMappe * * Part of {@link DataStoreService} */ -interface Collection { +interface ModelCollection { [id: number]: BaseModel; } /** * Represents a serialized collection. */ -interface SerializedCollection { +interface JsonCollection { [id: number]: string; } @@ -26,15 +26,15 @@ interface SerializedCollection { * * {@link DataStoreService} */ -interface Storage { - [collectionString: string]: Collection; +interface ModelStorage { + [collectionString: string]: ModelCollection; } /** * A storage of serialized collection elements. */ -interface SerializedStorage { - [collectionString: string]: SerializedCollection; +interface JsonStorage { + [collectionString: string]: JsonCollection; } /** @@ -49,14 +49,17 @@ interface SerializedStorage { export class DataStoreService { private static cachePrefix = 'DS:'; + /** + * Make sure, that the Datastore only be instantiated once. + */ private static wasInstantiated = false; /** We will store the data twice: One as instances of the actual models in the _store * and one serialized version in the _serializedStore for the cache. Both should be updated in * all cases equal! */ - private modelStore: Storage = {}; - private JsonStore: SerializedStorage = {}; + private modelStore: ModelStorage = {}; + private JsonStore: JsonStorage = {}; /** * Observable subject with changes to enable dynamic changes in models and views @@ -66,7 +69,14 @@ export class DataStoreService { /** * The maximal change id from this DataStore. */ - private maxChangeId = 0; + private _maxChangeId = 0; + + /** + * returns the maxChangeId of the DataStore. + */ + public get maxChangeId(): number { + return this._maxChangeId; + } /** * Empty constructor for dataStore @@ -85,43 +95,43 @@ export class DataStoreService { public initFromCache(): Promise { // This promise will be resolved with the maximal change id of the cache. return new Promise(resolve => { - this.cacheService - .get(DataStoreService.cachePrefix + 'DS') - .subscribe((store: SerializedStorage) => { - if (store != null) { - // There is a store. Deserialize it - this.JsonStore = store; - this.modelStore = this.deserializeJsonStore(this.JsonStore); - // Get the maxChangeId from the cache - this.cacheService - .get(DataStoreService.cachePrefix + 'maxChangeId') - .subscribe((maxChangeId: number) => { - if (maxChangeId == null) { - maxChangeId = 0; - } - this.maxChangeId = maxChangeId; - resolve(maxChangeId); - }); - } else { - // No store here, so get all data from the server. - resolve(0); - } - }); + this.cacheService.get(DataStoreService.cachePrefix + 'DS').subscribe((store: JsonStorage) => { + if (store != null) { + // There is a store. Deserialize it + this.JsonStore = store; + this.modelStore = this.deserializeJsonStore(this.JsonStore); + // Get the maxChangeId from the cache + this.cacheService + .get(DataStoreService.cachePrefix + 'maxChangeId') + .subscribe((maxChangeId: number) => { + if (maxChangeId == null) { + maxChangeId = 0; + } + this._maxChangeId = maxChangeId; + resolve(maxChangeId); + }); + } else { + // No store here, so get all data from the server. + resolve(0); + } + }); }); } /** * Deserialze the given serializedStorage and returns a Storage. */ - private deserializeJsonStore(serializedStore: SerializedStorage): Storage { - const storage: Storage = {}; + private deserializeJsonStore(serializedStore: JsonStorage): ModelStorage { + const storage: ModelStorage = {}; Object.keys(serializedStore).forEach(collectionString => { - storage[collectionString] = {} as Collection; + storage[collectionString] = {} as ModelCollection; const target = CollectionStringModelMapperService.getCollectionStringType(collectionString); - Object.keys(serializedStore[collectionString]).forEach(id => { - const data = JSON.parse(serializedStore[collectionString][id]); - storage[collectionString][id] = new target().deserialize(data); - }); + if (target) { + Object.keys(serializedStore[collectionString]).forEach(id => { + const data = JSON.parse(serializedStore[collectionString][id]); + storage[collectionString][id] = new target().deserialize(data); + }); + } }); return storage; } @@ -133,7 +143,7 @@ export class DataStoreService { public clear(callback?: (value: boolean) => void): void { this.modelStore = {}; this.JsonStore = {}; - this.maxChangeId = 0; + this._maxChangeId = 0; this.cacheService.remove(DataStoreService.cachePrefix + 'DS', () => { this.cacheService.remove(DataStoreService.cachePrefix + 'maxChangeId', callback); }); @@ -159,7 +169,7 @@ export class DataStoreService { collectionString = tempObject.collectionString; } - const collection: Collection = this.modelStore[collectionString]; + const collection: ModelCollection = this.modelStore[collectionString]; const models = []; if (!collection) { @@ -270,8 +280,8 @@ export class DataStoreService { */ private storeToCache(maxChangeId: number) { this.cacheService.set(DataStoreService.cachePrefix + 'DS', this.JsonStore); - if (maxChangeId > this.maxChangeId) { - this.maxChangeId = maxChangeId; + if (maxChangeId > this._maxChangeId) { + this._maxChangeId = maxChangeId; this.cacheService.set(DataStoreService.cachePrefix + 'maxChangeId', maxChangeId); } } diff --git a/client/src/app/core/services/openslides.service.spec.ts b/client/src/app/core/services/openslides.service.spec.ts new file mode 100644 index 000000000..b2706a395 --- /dev/null +++ b/client/src/app/core/services/openslides.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { OpenSlidesService } from './openslides.service'; + +describe('OpenSlidesService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [OpenSlidesService] + }); + }); + + it('should be created', inject([OpenSlidesService], (service: OpenSlidesService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/services/openslides.service.ts b/client/src/app/core/services/openslides.service.ts new file mode 100644 index 000000000..090fc4491 --- /dev/null +++ b/client/src/app/core/services/openslides.service.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +import { OpenSlidesComponent } from 'app/openslides.component'; +import { WebsocketService } from './websocket.service'; +import { OperatorService } from './operator.service'; +import { CacheService } from './cache.service'; +import { AutoupdateService } from './autoupdate.service'; + +/** + * Handles the bootup/showdown of this application. + */ +@Injectable({ + providedIn: 'root' +}) +export class OpenSlidesService extends OpenSlidesComponent { + /** + * if the user tries to access a certain URL without being authenticated, the URL will be stored here + */ + public redirectUrl: string; + + /** + * Constructor to create the NotifyService. Registers itself to the WebsocketService. + * @param cacheService + * @param operator + * @param websocketService + * @param router + * @param autoupdateService + */ + constructor( + private cacheService: CacheService, + private operator: OperatorService, + private websocketService: WebsocketService, + private router: Router, + private autoupdateService: AutoupdateService + ) { + super(); + + // Handler that gets called, if the websocket connection reconnects after a disconnection. + // There might have changed something on the server, so we check the operator, if he changed. + websocketService.getReconnectObservable().subscribe(() => { + this.checkOperator(); + }); + } + + /** + * the bootup-sequence: Do a whoami request and if it was successful, do + * {@method afterLoginBootup}. If not, redirect the user to the login page. + */ + public bootup(): void { + // start autoupdate if the user is logged in: + this.operator.whoAmI().subscribe(resp => { + this.operator.guestsEnabled = resp.guest_enabled; + if (!resp.user && !resp.guest_enabled) { + this.redirectUrl = location.pathname; + // Goto login, if the user isn't login and guests are not allowed + this.router.navigate(['/login']); + } else { + this.afterLoginBootup(resp.user_id); + } + }); + } + + /** + * the login bootup-sequence: Check (and maybe clear) the cache und setup the DataStore + * and websocket. + * @param userId + */ + public afterLoginBootup(userId: number): void { + // Else, check, which user was logged in last time + this.cacheService.get('lastUserLoggedIn').subscribe((id: number) => { + // if the user id changed, reset the cache. + if (userId !== id) { + this.DS.clear((value: boolean) => { + this.setupDataStoreAndWebSocket(); + }); + this.cacheService.set('lastUserLoggedIn', userId); + } else { + this.setupDataStoreAndWebSocket(); + } + }); + } + + /** + * Init DS from cache and after this start the websocket service. + */ + private setupDataStoreAndWebSocket() { + this.DS.initFromCache().then((changeId: number) => { + this.websocketService.connect( + false, + changeId + ); + }); + } + + /** + * SHuts down OpenSlides. The websocket is closed and the operator is not set. + */ + public shutdown(): void { + this.websocketService.close(); + this.operator.user = null; + } + + /** + * Shutdown and bootup. + */ + public reboot(): void { + this.shutdown(); + this.bootup(); + } + + /** + * Verify that the operator is the same as it was before a reconnect. + */ + private checkOperator(): void { + this.operator.whoAmI().subscribe(resp => { + // User logged off. + if (!resp.user && !resp.guest_enabled) { + this.shutdown(); + this.router.navigate(['/login']); + } else { + if ( + (this.operator.user && this.operator.user.id !== resp.user_id) || + (!this.operator.user && resp.user_id) + ) { + // user changed + this.reboot(); + } else { + // User is still the same, but check for missed autoupdates. + this.autoupdateService.requestChanges(); + } + } + }); + } +} diff --git a/client/src/app/core/services/operator.service.ts b/client/src/app/core/services/operator.service.ts index 420f2693c..00bf4af2c 100644 --- a/client/src/app/core/services/operator.service.ts +++ b/client/src/app/core/services/operator.service.ts @@ -7,220 +7,115 @@ import { Group } from 'app/shared/models/users/group'; import { User } from '../../shared/models/users/user'; import { environment } from 'environments/environment'; +/** + * Permissions on the client are just strings. This makes clear, that + * permissions instead of arbitrary strings should be given. + */ +export type Permission = string; + +/** + * Response format of the WHoAMI request. + */ +interface WhoAmIResponse { + user_id: number; + guest_enabled: boolean; + user: User; +} + /** * The operator represents the user who is using OpenSlides. * - * Information is mostly redundant to user but has different purposes. * Changes in operator can be observed, directives do so on order to show * or hide certain information. * - * Could extend User? - * * The operator is an {@link OpenSlidesComponent}. */ @Injectable({ providedIn: 'root' }) export class OperatorService extends OpenSlidesComponent { - about_me: string; - comment: string; - default_password: string; - email: string; - first_name: string; - groups_id: number[]; - id: number; - is_active: boolean; - is_committee: boolean; - is_present: boolean; - last_email_send: string; - last_name: string; - number: string; - structure_level: string; - title: string; - username: string; - logged_in: boolean; - + /** + * The operator. + */ private _user: User; + /** + * Get the user that corresponds to operator. + */ + get user(): User { + return this._user; + } + + /** + * Sets the current operator. + * + * The permissions are updated and the new user published. + */ + set user(user: User) { + this._user = user; + this.updatePermissions(); + } + + /** + * Save, if quests are enabled. + */ + public guestsEnabled: boolean; + + /** + * The permissions of the operator. Updated via {@method updatePermissions}. + */ + private permissions: Permission[] = []; + /** * The subject that can be observed by other instances using observing functions. */ private operatorSubject: BehaviorSubject = new BehaviorSubject(null); /** - * Representation of the {@link Group}s that the operator has (in contrast the the `groups_id`-Array) - * - * The operator observes the dataStore (compare {@link OpenSlidesComponent} in Order to know it's groups) - */ - private groups: Group[] = new Array(); - - /** - * Recreates the operator from localStorage if it's found and starts to observe the dataStore. * @param http HttpClient */ constructor(private http: HttpClient) { super(); - - // recreate old operator from localStorage. - if (localStorage.getItem('operator')) { - const oldOperator = JSON.parse(localStorage.getItem('operator')); - if (Object.keys(oldOperator).length > 0) { - this.storeUser(oldOperator); - } - } - - // observe the DataStore now to avoid race conditions. Ensures to - // find the groups in time - this.observeDataStore(); } /** - * calls `/apps/users/whoami` to find out the real operator + * Setup the subscription of the DataStore.Update the user and it's + * permissions if the user or groups changes. */ - public whoAmI(): Observable { - return this.http.get(environment.urlPrefix + '/users/whoami/').pipe( - tap(whoami => { - if (whoami && whoami.user) { - this.storeUser(whoami.user as User); + public setupSubscription() { + this.DS.getObservable().subscribe(newModel => { + if (this._user) { + if (newModel instanceof Group) { + this.updatePermissions(); + } + + if (newModel instanceof User && this._user.id === newModel.id) { + this._user = newModel; + this.updatePermissions(); + } + } else if (newModel instanceof Group && newModel.id === 1) { + // Group 1 (default) for anonymous changed + this.updatePermissions(); + } + }); + } + + /** + * Calls `/apps/users/whoami` to find out the real operator. + */ + public whoAmI(): Observable { + return this.http.get(environment.urlPrefix + '/users/whoami/').pipe( + tap((response: WhoAmIResponse) => { + if (response && response.user_id) { + this.user = new User().deserialize(response.user); } }), catchError(this.handleError()) - ); + ) as Observable; } /** - * Store the user Information in the operator, the localStorage and update the Observable - * @param user usually a http response that represents a user. - * - * Todo: Could be refractored to use the actual User Object. - * Operator is older than user, so this is still a traditional JS way - */ - public storeUser(user: User): void { - // store in file - this.about_me = user.about_me; - this.comment = user.comment; - this.default_password = user.default_password; - this.email = user.email; - this.first_name = user.first_name; - this.groups_id = user.groups_id; - this.id = user.id; - this.is_active = user.is_active; - this.is_committee = user.is_committee; - this.is_present = user.is_present; - this.last_email_send = user.last_email_send; - this.last_name = user.last_name; - this.number = user.number; - this.structure_level = user.structure_level; - this.title = user.title; - this.username = user.username; - - // also store in localstorrage - this.updateLocalStorage(); - // update mode to inform observers - this.setObservable(this.getUpdateObject()); - } - - /** - * Removes all stored information about the Operator. - * - * The Opposite of StoreUser. Usually a `logout()`-function. - * Also removes the operator from localStorrage and - * updates the observable. - */ - public clear() { - this.about_me = null; - this.comment = null; - this.default_password = null; - this.email = null; - this.first_name = null; - this.groups_id = null; - this.id = null; - this.is_active = null; - this.is_committee = null; - this.is_present = null; - this.last_email_send = null; - this.last_name = null; - this.number = null; - this.structure_level = null; - this.title = null; - this.username = null; - this.setObservable(this.getUpdateObject()); - localStorage.removeItem('operator'); - } - - /** - * Saves the operator in the localStorage for easier and faster re-login - * - * This is a mere comfort feature, even if the operator can be recreated - * it has to pass `this.whoAmI()` during page access. - */ - private updateLocalStorage(): void { - localStorage.setItem('operator', JSON.stringify(this.getUpdateObject())); - } - - /** - * Returns the current operator. - * - * Used to save the operator in localStorage or inform observers. - */ - private getUpdateObject(): any { - return { - about_me: this.about_me, - comment: this.comment, - default_password: this.default_password, - email: this.email, - first_name: this.first_name, - groups_id: this.groups_id, - id: this.id, - is_active: this.is_active, - is_committee: this.is_committee, - is_present: this.is_present, - last_email_send: this.last_email_send, - last_name: this.last_name, - number: this.number, - structure_level: this.structure_level, - title: this.title, - username: this.username, - logged_in: this.logged_in - }; - } - - /** - * Observe dataStore to set groups once they are loaded. - * - * TODO logic to remove groups / user from certain groups. Currently is is only set and was never removed - */ - private observeDataStore(): void { - this.DS.getObservable().subscribe(newModel => { - if (newModel instanceof Group) { - this.addGroup(newModel); - } - - if (newModel instanceof User && this.id === newModel.id) { - this._user = newModel; - } - }); - } - - /** - * Read out the Groups from the DataStore by the operators 'groups_id' - * - * requires that the DataStore has been setup (websocket.service) - * requires that the whoAmI did return a valid operator - * - * This is the normal behavior after a fresh login, everythin else can - * be done by observers. - */ - public readGroupsFromStore(): void { - this.DS.filter(Group, myGroup => { - if (this.groups_id.includes(myGroup.id)) { - this.addGroup(myGroup); - } - }); - } - - /** - * Returns the behaviorSubject as an observable. + * Returns the operatorSubject as an observable. * * Services an components can use it to get informed when something changes in * the operator @@ -230,36 +125,35 @@ export class OperatorService extends OpenSlidesComponent { } /** - * Inform all observers about changes - * @param value + * Checks, if the operator has at least one of the given permissions. + * @param permissions The permissions to check, if at least one matches. */ - private setObservable(value) { - this.operatorSubject.next(value); + public hasPerms(...permissions: Permission[]): boolean { + return permissions.some(permisison => { + return this.permissions.includes(permisison); + }); } /** - * Getter for the (real) {@link Group}s + * Update the operators permissions and publish the operator afterwards. */ - public getGroups() { - return this.groups; - } - - /** - * if the operator has the corresponding ID, set the group - * @param newGroup potential group that the operator has. - */ - private addGroup(newGroup: Group): void { - if (this.groups_id.includes(newGroup.id as number)) { - this.groups.push(newGroup); - // inform the observers about new groups (appOsPerms) - this.setObservable(newGroup); + private updatePermissions(): void { + this.permissions = []; + if (!this.user) { + const defaultGroup = this.DS.get('users/group', 1) as Group; + if (defaultGroup && defaultGroup.permissions instanceof Array) { + this.permissions = defaultGroup.permissions; + } + } else { + const permissionSet = new Set(); + this.user.groups.forEach(group => { + group.permissions.forEach(permission => { + permissionSet.add(permission); + }); + }); + this.permissions = Array.from(permissionSet.values()); } - } - - /** - * get the user that corresponds to operator. - */ - get user(): User { - return this._user; + // publish changes in the operator. + this.operatorSubject.next(this.user); } } diff --git a/client/src/app/core/services/websocket.service.ts b/client/src/app/core/services/websocket.service.ts index 284c07d29..936a571a5 100644 --- a/client/src/app/core/services/websocket.service.ts +++ b/client/src/app/core/services/websocket.service.ts @@ -1,12 +1,19 @@ -import { Injectable } from '@angular/core'; +import { Injectable, NgZone } from '@angular/core'; import { Router } from '@angular/router'; -import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject, of } from 'rxjs'; +import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material'; +import { TranslateService } from '@ngx-translate/core'; +/** + * A key value mapping for params, that should be appendet to the url on a new connection. + */ interface QueryParams { [key: string]: string; } +/** + * The generic message format in which messages are send and recieved by the server. + */ interface WebsocketMessage { type: string; content: any; @@ -14,9 +21,8 @@ interface WebsocketMessage { } /** - * Service that handles WebSocket connections. - * - * Creates or returns already created WebSockets. + * Service that handles WebSocket connections. Other services can register themselfs + * with {@method getOberservable} for a specific type of messages. The content will be published. */ @Injectable({ providedIn: 'root' @@ -26,12 +32,29 @@ export class WebsocketService { * Constructor that handles the router * @param router the URL Router */ - constructor(private router: Router) {} + constructor( + private router: Router, + private matSnackBar: MatSnackBar, + private zone: NgZone, + public translate: TranslateService + ) { + this.reconnectSubject = new Subject(); + } /** - * Observable subject that might be `any` for simplicity, `MessageEvent` or something appropriate + * The reference to the snackbar entry that is shown, if the connection is lost. */ - private websocketSubject: WebSocketSubject; + private connectionErrorNotice: MatSnackBarRef; + + /** + * Subjects that will be called, if a reconnect was successful. + */ + private reconnectSubject: Subject; + + /** + * The websocket. + */ + private websocket: WebSocket; /** * Subjects for types of websocket messages. A subscriber can get an Observable by {@function getOberservable}. @@ -39,33 +62,86 @@ export class WebsocketService { private subjects: { [type: string]: Subject } = {}; /** - * Creates a new WebSocket connection as WebSocketSubject + * Creates a new WebSocket connection and handles incomming events. * - * Can return old Subjects to prevent multiple WebSocket connections. + * Uses NgZone to let all callbacks run in the angular context. */ - public connect(changeId?: number): void { + public connect(retry = false, changeId?: number): void { + if (this.websocket) { + return; + } const queryParams: QueryParams = {}; // comment-in if changes IDs are supported on server side. /*if (changeId !== undefined) { queryParams.changeId = changeId.toString(); }*/ + // Create the websocket const socketProtocol = this.getWebSocketProtocol(); const socketServer = window.location.hostname + ':' + window.location.port; const socketPath = this.getWebSocketPath(queryParams); - if (!this.websocketSubject) { - this.websocketSubject = webSocket(socketProtocol + socketServer + socketPath); - // directly subscribe. The messages are distributes below - this.websocketSubject.subscribe(message => { + this.websocket = new WebSocket(socketProtocol + socketServer + socketPath); + + // connection established. If this connect attept was a retry, + // The error notice will be removed and the reconnectSubject is published. + this.websocket.onopen = (event: Event) => { + this.zone.run(() => { + if (retry) { + if (this.connectionErrorNotice) { + this.connectionErrorNotice.dismiss(); + this.connectionErrorNotice = null; + } + this.reconnectSubject.next(); + } + }); + }; + + this.websocket.onmessage = (event: MessageEvent) => { + this.zone.run(() => { + const message: WebsocketMessage = JSON.parse(event.data); const type: string = message.type; if (type === 'error') { console.error('Websocket error', message.content); } else if (this.subjects[type]) { + // Pass the content to the registered subscribers. this.subjects[type].next(message.content); } else { console.log(`Got unknown websocket message type "${type}" with content`, message.content); } }); + }; + + this.websocket.onclose = (event: CloseEvent) => { + this.zone.run(() => { + this.websocket = null; + if (event.code !== 1000) { + // 1000 is a normal close, like the close on logout + if (!this.connectionErrorNotice) { + // So here we have a connection failure that wasn't intendet. + this.connectionErrorNotice = this.matSnackBar.open( + this.translate.instant('Offline mode: You can use OpenSlides but changes are not saved.'), + '', + { duration: 0 } + ); + } + + // A random retry timeout between 2000 and 5000 ms. + const timeout = Math.floor(Math.random() * 3000 + 2000); + setTimeout(() => { + this.connect((retry = true)); + }, timeout); + } + }); + }; + } + + /** + * Closes the websocket connection. + */ + public close(): void { + if (this.websocket) { + this.websocket.close(); + this.websocket = null; } } @@ -80,28 +156,39 @@ export class WebsocketService { return this.subjects[type].asObservable(); } + /** + * get the reconnect observable. It will be published, if a reconnect was sucessful. + */ + public getReconnectObservable(): Observable { + return this.reconnectSubject.asObservable(); + } + /** * Sends a message to the server with the content and the given type. * * @param type the message type * @param content the actual content */ - public send(type: string, content: T): void { - if (!this.websocketSubject) { + public send(type: string, content: T, id?: string): void { + if (!this.websocket) { return; } const message: WebsocketMessage = { type: type, content: content, - id: '' + id: id }; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - for (let i = 0; i < 8; i++) { - message.id += possible.charAt(Math.floor(Math.random() * possible.length)); + // create message id if not given. Required by the server. + if (!message.id) { + message.id = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + for (let i = 0; i < 8; i++) { + message.id += possible.charAt(Math.floor(Math.random() * possible.length)); + } } - this.websocketSubject.next(message); + this.websocket.send(JSON.stringify(message)); } /** @@ -130,8 +217,6 @@ export class WebsocketService { /** * returns the desired websocket protocol - * - * TODO: HTTPS is not yet tested */ private getWebSocketProtocol(): string { if (location.protocol === 'https') { diff --git a/client/src/app/openslides.component.ts b/client/src/app/openslides.component.ts index b1fdd83ec..44dc619ed 100644 --- a/client/src/app/openslides.component.ts +++ b/client/src/app/openslides.component.ts @@ -3,7 +3,6 @@ import { Observable, of } from 'rxjs'; import { DataStoreService } from './core/services/data-store.service'; import { CacheService } from './core/services/cache.service'; -import { RootInjector } from './core/rootInjector'; /** * injects the {@link DataStoreService} to all its children and provides a generic function to catch errors @@ -15,6 +14,8 @@ export abstract class OpenSlidesComponent { */ private static _DS: DataStoreService; + public static injector: Injector; + /** * Empty constructor * @@ -29,6 +30,9 @@ export abstract class OpenSlidesComponent { * @return access to dataStoreService */ get DS(): DataStoreService { + if (!OpenSlidesComponent.injector) { + throw new Error('OpenSlides is not bootstrapping right. This component should have the Injector.'); + } if (OpenSlidesComponent._DS == null) { const injector = Injector.create({ providers: [ @@ -38,7 +42,7 @@ export abstract class OpenSlidesComponent { deps: [CacheService] } ], - parent: RootInjector.injector + parent: OpenSlidesComponent.injector }); OpenSlidesComponent._DS = injector.get(DataStoreService); } diff --git a/client/src/app/shared/directives/os-perms.directive.ts b/client/src/app/shared/directives/os-perms.directive.ts index 02e896325..72f81d765 100644 --- a/client/src/app/shared/directives/os-perms.directive.ts +++ b/client/src/app/shared/directives/os-perms.directive.ts @@ -1,33 +1,33 @@ -import { Directive, Input, ElementRef, TemplateRef, ViewContainerRef, OnInit } from '@angular/core'; +import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; -import { OperatorService } from 'app/core/services/operator.service'; +import { OperatorService, Permission } from 'app/core/services/operator.service'; import { OpenSlidesComponent } from 'app/openslides.component'; -import { Group } from 'app/shared/models/users/group'; -import { BehaviorSubject } from 'rxjs'; /** * Directive to check if the {@link OperatorService} has the correct permissions to access certain functions * * Successor of os-perms in OpenSlides 2.2 - * @example
... < /div> + * @example
... < /div> + * @example
... < /div> */ @Directive({ selector: '[appOsPerms]' }) export class OsPermsDirective extends OpenSlidesComponent { - /** - * Holds the {@link OperatorService} permissions - */ - private userPermissions: string[]; - /** * Holds the required permissions the access a feature */ - private permissions; + private permissions: Permission[] = []; /** - * Constructs the directive once. Observes the operator for it's groups so the directive can perform changes - * dynamically + * Holds the value of the last permission check. Therefore one can check, if the + * permission has changes, to save unnecessary view updates, if not. + */ + private lastPermissionCheckResult = false; + + /** + * Constructs the directive once. Observes the operator for it's groups so the + * directive can perform changes dynamically * * @param template inner part of the HTML container * @param viewContainer outer part of the HTML container (for example a `
`) @@ -39,50 +39,44 @@ export class OsPermsDirective extends OpenSlidesComponent { private operator: OperatorService ) { super(); - this.userPermissions = []; // observe groups of operator, so the directive can actively react to changes this.operator.getObservable().subscribe(content => { - if (content instanceof Group && this.permissions !== '') { - this.userPermissions = [...this.userPermissions, ...content.permissions]; - this.updateView(); - } + this.updateView(); }); } /** * Comes directly from the view. - * The value defines the requires permissions. + * The value defines the requires permissions as an array or a single permission. */ @Input() set appOsPerms(value) { + if (!value) { + value = []; + } else if (typeof value === 'string') { + value = [value]; + } this.permissions = value; - this.readUserPermissions(); this.updateView(); } - /** - * Updates the local `userPermissions[]` by the permissions found in the operators groups - * Will just set, but not remove them. - */ - private readUserPermissions(): void { - const opGroups = this.operator.getGroups(); - opGroups.forEach(group => { - this.userPermissions = [...this.userPermissions, ...group.permissions]; - }); - } - /** * Shows or hides certain content in the view. */ private updateView(): void { - if (this.checkPermissions()) { - // will just render the page normally + const hasPerms = this.checkPermissions(); + const permsChanged = hasPerms !== this.lastPermissionCheckResult; + + if (hasPerms && permsChanged) { + // clean up and add the template + this.viewContainer.clear(); this.viewContainer.createEmbeddedView(this.template); - } else { + } else if (!hasPerms) { // will remove the content of the container this.viewContainer.clear(); } + this.lastPermissionCheckResult = hasPerms; } /** @@ -90,12 +84,6 @@ export class OsPermsDirective extends OpenSlidesComponent { * Returns true if the users permissions fit. */ private checkPermissions(): boolean { - let isPermitted = false; - if (this.userPermissions && this.permissions) { - this.permissions.forEach(perm => { - isPermitted = this.userPermissions.find(userPerm => userPerm === perm) ? true : false; - }); - } - return isPermitted; + return this.permissions.length === 0 || this.operator.hasPerms(...this.permissions); } } diff --git a/client/src/app/shared/models/users/user.ts b/client/src/app/shared/models/users/user.ts index a3d8edb18..e20bd6d7e 100644 --- a/client/src/app/shared/models/users/user.ts +++ b/client/src/app/shared/models/users/user.ts @@ -1,4 +1,5 @@ import { BaseModel } from '../base.model'; +import { Group } from './group'; /** * Representation of a user in contrast to the operator. @@ -61,11 +62,38 @@ export class User extends BaseModel { this.default_password = default_password; } - getGroups(): BaseModel | BaseModel[] { - return this.DS.get('users/group', ...this.groups_id); + get groups(): Group[] { + const groups = this.DS.get('users/group', ...this.groups_id); + if (!groups) { + return []; + } else if (groups instanceof BaseModel) { + return [groups] as Group[]; + } else { + return groups as Group[]; + } } - //TODO get full_name + get full_name(): string { + let name = this.short_name; + const addition: string[] = []; + + // addition: add number and structure level + const structure_level = this.structure_level.trim(); + if (structure_level) { + addition.push(structure_level); + } + + const number = this.number.trim(); + if (number) { + // TODO Translate + addition.push('No.' + ' ' + number); + } + + if (addition.length > 0) { + name += ' (' + addition.join(' ยท ') + ')'; + } + return name.trim(); + } // TODO read config values for "users_sort_by" get short_name(): string { diff --git a/client/src/app/site/agenda/agenda-list/agenda-list.component.html b/client/src/app/site/agenda/agenda-list/agenda-list.component.html index 8003ad42e..eb24b6838 100644 --- a/client/src/app/site/agenda/agenda-list/agenda-list.component.html +++ b/client/src/app/site/agenda/agenda-list/agenda-list.component.html @@ -8,7 +8,7 @@ everyone should see this

-
+
Only permitted users should see this
diff --git a/client/src/app/site/login/login.component.html b/client/src/app/site/login/login.component.html index 7f515cfd2..e73e9a07a 100644 --- a/client/src/app/site/login/login.component.html +++ b/client/src/app/site/login/login.component.html @@ -18,11 +18,14 @@
- +
- + + +
\ No newline at end of file diff --git a/client/src/app/site/login/login.component.ts b/client/src/app/site/login/login.component.ts index 5c9e7d843..29d6c684c 100644 --- a/client/src/app/site/login/login.component.ts +++ b/client/src/app/site/login/login.component.ts @@ -1,13 +1,16 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit, Input, OnDestroy } from '@angular/core'; import { Router } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { BaseComponent } from 'app/base.component'; import { AuthService } from 'app/core/services/auth.service'; import { OperatorService } from 'app/core/services/operator.service'; -import { ErrorStateMatcher } from '@angular/material'; +import { ErrorStateMatcher, MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material'; import { FormControl, FormGroupDirective, NgForm, FormGroup, Validators, FormBuilder } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; +import { HttpErrorResponse, HttpClient } from '@angular/common/http'; +import { environment } from 'environments/environment'; +import { OpenSlidesService } from '../../core/services/openslides.service'; /** * Custom error states. Might become part of the shared module later. @@ -38,31 +41,36 @@ export class ParentErrorStateMatcher implements ErrorStateMatcher { templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) -export class LoginComponent extends BaseComponent implements OnInit { +export class LoginComponent extends BaseComponent implements OnInit, OnDestroy { /** * Show or hide password and change the indicator accordingly */ - hide: boolean; + public hide: boolean; + + /** + * Reference to the SnackBarEntry for the installation notice send by the server. + */ + private installationNotice: MatSnackBarRef; /** * Login Error Message if any */ - loginErrorMsg = ''; + public loginErrorMsg = ''; /** * Form group for the login form */ - loginForm: FormGroup; + public loginForm: FormGroup; /** * Custom Form validation */ - parentErrorStateMatcher = new ParentErrorStateMatcher(); + public parentErrorStateMatcher = new ParentErrorStateMatcher(); /** * Show the Spinner if validation is in process */ - inProcess = false; + public inProcess = false; /** * Constructor for the login component @@ -79,7 +87,10 @@ export class LoginComponent extends BaseComponent implements OnInit { private authService: AuthService, private operator: OperatorService, private router: Router, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + private http: HttpClient, + private matSnackBar: MatSnackBar, + private OpenSlides: OpenSlidesService ) { super(titleService, translate); this.createForm(); @@ -91,23 +102,26 @@ export class LoginComponent extends BaseComponent implements OnInit { * Set the title to "Log In" * Observes the operator, if a user was already logged in, recreate to user and skip the login */ - ngOnInit() { - //this is necessary since the HTML document never uses the word "Log In" - const loginWord = this.translate.instant('Log In'); - super.setTitle('Log In'); + public ngOnInit(): void { + super.setTitle('Login'); - // if there is stored login information, try to login directly. - this.operator.getObservable().subscribe(user => { - if (user && user.id) { - this.router.navigate(['/']); - } + this.http.get(environment.urlPrefix + '/users/login/', {}).subscribe(response => { + this.installationNotice = this.matSnackBar.open(response.info_text, this.translate.instant('OK'), { + duration: 5000 + }); }); } + public ngOnDestroy(): void { + if (this.installationNotice) { + this.installationNotice.dismiss(); + } + } + /** * Create the login Form */ - createForm() { + public createForm(): void { this.loginForm = this.formBuilder.group({ username: ['', [Validators.required, Validators.maxLength(128)]], password: ['', [Validators.required, Validators.maxLength(128)]] @@ -119,23 +133,46 @@ export class LoginComponent extends BaseComponent implements OnInit { * * Send username and password to the {@link AuthService} */ - formLogin(): void { + public formLogin(): void { this.loginErrorMsg = ''; this.inProcess = true; this.authService.login(this.loginForm.value.username, this.loginForm.value.password).subscribe(res => { - if (res.status === 400) { - this.inProcess = false; + this.inProcess = false; + + if (res instanceof HttpErrorResponse) { this.loginForm.setErrors({ notFound: true }); this.loginErrorMsg = res.error.detail; } else { - this.inProcess = false; - if (res.user_id) { - const redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/'; - this.router.navigate([redirect]); + this.OpenSlides.afterLoginBootup(res.user_id); + let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/'; + if (redirect.includes('login')) { + redirect = '/'; } + this.router.navigate([redirect]); } }); } + + /** + * TODO, should open an edit view for the users password. + */ + public resetPassword(): void { + console.log('TODO'); + } + + /** + * returns if the anonymous is enabled. + */ + public areGuestsEnabled(): boolean { + return this.operator.guestsEnabled; + } + + /** + * Guests (if enabled) can navigate directly to the main page. + */ + public guestLogin(): void { + this.router.navigate(['/']); + } } diff --git a/client/src/app/site/settings/settings-list/settings-list.component.html b/client/src/app/site/settings/settings-list/settings-list.component.html index 80549a965..b477ea4a2 100644 --- a/client/src/app/site/settings/settings-list/settings-list.component.html +++ b/client/src/app/site/settings/settings-list/settings-list.component.html @@ -1,7 +1,7 @@ -
+
diff --git a/client/src/app/site/site-routing.module.ts b/client/src/app/site/site-routing.module.ts index e16d90ae1..916bbc955 100644 --- a/client/src/app/site/site-routing.module.ts +++ b/client/src/app/site/site-routing.module.ts @@ -6,6 +6,7 @@ import { SiteComponent } from './site.component'; import { StartComponent } from './start/start.component'; import { PrivacyPolicyComponent } from './privacy-policy/privacy-policy.component'; import { LegalNoticeComponent } from './legal-notice/legal-notice.component'; +import { AuthGuard } from '../core/services/auth-guard.service'; // import { LoginComponent } from './login/login.component'; /** @@ -37,7 +38,8 @@ const routes: Routes = [ loadChildren: './settings/settings.module#SettingsModule' }, { path: 'users', loadChildren: './users/users.module#UsersModule' } - ] + ], + canActivateChild: [AuthGuard] } ]; diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html index fcc467f34..370885c09 100644 --- a/client/src/app/site/site.component.html +++ b/client/src/app/site/site.component.html @@ -17,19 +17,23 @@ {{getLangName(this.translate.currentLang)}} - + Edit Profile - + Change Password - + Logout + + + Login + @@ -41,39 +45,39 @@ - Home - + Agenda - + Motions - Assignments - + Participants - + Files - Settings - Projector diff --git a/client/src/app/site/site.component.ts b/client/src/app/site/site.component.ts index 7c93bc296..70ad53afb 100644 --- a/client/src/app/site/site.component.ts +++ b/client/src/app/site/site.component.ts @@ -3,14 +3,12 @@ import { Router } from '@angular/router'; import { AuthService } from 'app/core/services/auth.service'; import { OperatorService } from 'app/core/services/operator.service'; -import { WebsocketService } from 'app/core/services/websocket.service'; import { TranslateService } from '@ngx-translate/core'; //showcase import { BaseComponent } from 'app/base.component'; import { pageTransition, navItemAnim } from 'app/shared/animations'; import { MatDialog, MatSidenav } from '@angular/material'; import { ViewportService } from '../core/services/viewport.service'; -import { CacheService } from '../core/services/cache.service'; @Component({ selector: 'app-site', @@ -27,30 +25,39 @@ export class SiteComponent extends BaseComponent implements OnInit { /** * Get the username from the operator (should be known already) */ - username = this.operator.username; + public username: string; + + /** + * is the user logged in, or the anonymous is active. + */ + public isLoggedIn: boolean; /** * Constructor * * @param authService - * @param websocketService * @param operator - * @param router - * @param breakpointObserver + * @param vp * @param translate * @param dialog */ constructor( private authService: AuthService, - private websocketService: WebsocketService, private operator: OperatorService, - private router: Router, public vp: ViewportService, public translate: TranslateService, - public dialog: MatDialog, - private cacheService: CacheService + public dialog: MatDialog ) { super(); + + operator.getObservable().subscribe(user => { + if (user) { + this.username = user.full_name; + } else { + this.username = translate.instant('Guest'); + } + this.isLoggedIn = !!user; + }); } /** @@ -63,32 +70,6 @@ export class SiteComponent extends BaseComponent implements OnInit { // this.translate.get('Motions').subscribe((res: string) => { // console.log('translation of motions in the target language: ' + res); // }); - - // start autoupdate if the user is logged in: - this.operator.whoAmI().subscribe(resp => { - if (resp.user) { - this.cacheService.get('lastUserLoggedIn').subscribe((id: number) => { - if (resp.user_id !== id) { - this.DS.clear((value: boolean) => { - this.setupDataStoreAndWebSocket(); - }); - this.cacheService.set('lastUserLoggedIn', resp.user_id); - } else { - this.setupDataStoreAndWebSocket(); - } - }); - } else { - //if whoami is not sucsessfull, forward to login again - this.operator.clear(); - this.router.navigate(['/login']); - } - }); - } - - private setupDataStoreAndWebSocket() { - this.DS.initFromCache().then((changeId: number) => { - this.websocketService.connect(changeId); - }); } /** @@ -121,11 +102,16 @@ export class SiteComponent extends BaseComponent implements OnInit { } } + // TODO: Implement this + editProfile() {} + + // TODO: Implement this + changePassword() {} + /** * Function to log out the current user */ - logOutButton() { - this.authService.logout().subscribe(); - this.router.navigate(['/login']); + logout() { + this.authService.logout(); } } diff --git a/client/src/app/site/start/start.component.html b/client/src/app/site/start/start.component.html index 5dc637b5e..83fc3afa0 100644 --- a/client/src/app/site/start/start.component.html +++ b/client/src/app/site/start/start.component.html @@ -5,9 +5,6 @@

{{welcomeTitle | translate}}

{{welcomeText | translate}} -
- -

{{'Hello user' | translate:username}}


diff --git a/client/src/app/site/start/start.component.ts b/client/src/app/site/start/start.component.ts index cc4db77d3..12f6afe98 100644 --- a/client/src/app/site/start/start.component.ts +++ b/client/src/app/site/start/start.component.ts @@ -20,7 +20,6 @@ import { MotionSubmitter } from '../../shared/models/motions/motion-submitter'; export class StartComponent extends BaseComponent implements OnInit { welcomeTitle: string; welcomeText: string; - username = { user: this.operator.username }; /** * Constructor of the StartComponent diff --git a/client/src/main.ts b/client/src/main.ts index d53cc2d87..379e23834 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -3,7 +3,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; -import { RootInjector } from 'app/core/rootInjector'; +import { AppComponent } from 'app/app.component'; if (environment.production) { enableProdMode(); @@ -12,6 +12,6 @@ if (environment.production) { platformBrowserDynamic() .bootstrapModule(AppModule) .then((moduleRef: NgModuleRef) => { - RootInjector.injector = moduleRef.injector; + AppComponent.bootstrapDone(moduleRef); }) .catch(err => console.log(err));