Merge pull request #3834 from FinnStutzenstein/operator

Auth/Operator/Anonymous/WS-Stuff
This commit is contained in:
Finn Stutzenstein 2018-08-29 13:13:45 +02:00 committed by GitHub
commit b07641b85c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 689 additions and 451 deletions

View File

@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './site/login/login.component'; import { LoginComponent } from './site/login/login.component';
import { AuthGuard } from './core/services/auth-guard.service';
/** /**
* Global app routing * Global app routing
@ -9,7 +8,7 @@ import { AuthGuard } from './core/services/auth-guard.service';
const routes: Routes = [ const routes: Routes = [
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
{ path: 'projector', loadChildren: './projector-container/projector-container.module#ProjectorContainerModule' }, { 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: '' } { path: '**', redirectTo: '' }
]; ];

View File

@ -1,7 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, Injector, NgModuleRef } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AutoupdateService } from './core/services/autoupdate.service'; import { AutoupdateService } from './core/services/autoupdate.service';
import { NotifyService } from './core/services/notify.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 * Angular's global App Component
@ -13,7 +18,21 @@ import { NotifyService } from './core/services/notify.service';
}) })
export class AppComponent { 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<NgModuleRef<AppModule>> = new Subject<NgModuleRef<AppModule>>();
/**
* 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<AppModule>) {
AppComponent.bootstrapDoneSubject.next(moduleRef);
}
/**
* Initialises the translation unit.
* @param autoupdateService * @param autoupdateService
* @param notifyService * @param notifyService
* @param translate * @param translate
@ -21,7 +40,9 @@ export class AppComponent {
constructor( constructor(
private autoupdateService: AutoupdateService, private autoupdateService: AutoupdateService,
private notifyService: NotifyService, private notifyService: NotifyService,
private translate: TranslateService private translate: TranslateService,
private operator: OperatorService,
private OpenSlides: OpenSlidesService
) { ) {
// manually add the supported languages // manually add the supported languages
translate.addLangs(['en', 'de', 'fr']); translate.addLangs(['en', 'de', 'fr']);
@ -31,5 +52,20 @@ export class AppComponent {
const browserLang = translate.getBrowserLang(); const browserLang = translate.getBrowserLang();
// try to use the browser language if it is available. If not, uses english. // try to use the browser language if it is available. If not, uses english.
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); 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<AppModule>): void {
OpenSlidesComponent.injector = moduleRef.injector;
// Setup the operator after the root injector is known.
this.operator.setupSubscription();
this.OpenSlides.bootup(); // Yeah!
} }
} }

View File

@ -1,6 +0,0 @@
import { Injector } from '@angular/core';
export class RootInjector {
constructor() {}
public static injector: Injector;
}

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; 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'; import { OperatorService } from './operator.service';
/** /**
@ -10,35 +9,40 @@ import { OperatorService } from './operator.service';
@Injectable({ @Injectable({
providedIn: 'root' 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 operator
* @param router
*/ */
constructor(private authService: AuthService, private operator: OperatorService, private router: Router) {} constructor(private operator: OperatorService) {}
/** /**
* Checks of the operator has n id. * Checks of the operator has the required permission to see the state.
* If so, forward to the desired target.
* *
* If not, forward to login. * One can set extra data to the state with `data: {basePerm: '<perm>'}` or
* * `data: {basePerm: ['<perm1>', '<perm2>']}` to lock the access to users
* TODO: Test if this works for guests and on Projector * only with the given permission(s).
* *
* @param route required by `canActivate()` * @param route required by `canActivate()`
* @param state the state (URL) that the user want to access * @param state the state (URL) that the user want to access
*/ */
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const url: string = state.url; const basePerm: string | string[] = route.data.basePerm;
if (this.operator.id) { if (!basePerm) {
return true; return true;
} else if (basePerm instanceof Array) {
return this.operator.hasPerms(...basePerm);
} else { } else {
this.authService.redirectUrl = url; return this.operator.hasPerms(basePerm);
this.router.navigate(['/login']);
return false;
} }
} }
/**
* Calls {@method canActivate}. Should have the same logic.
* @param route
* @param state
*/
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}
} }

View File

@ -6,6 +6,16 @@ import { catchError, tap } from 'rxjs/operators';
import { OperatorService } from 'app/core/services/operator.service'; import { OperatorService } from 'app/core/services/operator.service';
import { OpenSlidesComponent } from '../../openslides.component'; import { OpenSlidesComponent } from '../../openslides.component';
import { environment } from 'environments/environment'; 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 * Authenticates an OpenSlides user with username and password
@ -14,11 +24,6 @@ import { environment } from 'environments/environment';
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService extends OpenSlidesComponent { 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}. * Initializes the httpClient and the {@link OperatorService}.
* *
@ -26,7 +31,7 @@ export class AuthService extends OpenSlidesComponent {
* @param http HttpClient * @param http HttpClient
* @param operator who is using OpenSlides * @param operator who is using OpenSlides
*/ */
constructor(private http: HttpClient, private operator: OperatorService) { constructor(private http: HttpClient, private operator: OperatorService, private OpenSlides: OpenSlidesService) {
super(); super();
} }
@ -40,27 +45,29 @@ export class AuthService extends OpenSlidesComponent {
* @param username * @param username
* @param password * @param password
*/ */
login(username: string, password: string): Observable<any> { public login(username: string, password: string): Observable<LoginResponse> {
const user: any = { const user = {
username: username, username: username,
password: password password: password
}; };
return this.http.post<any>(environment.urlPrefix + '/users/login/', user).pipe( return this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user).pipe(
tap(resp => this.operator.storeUser(resp.user)), tap((response: LoginResponse) => {
this.operator.user = new User().deserialize(response.user);
}),
catchError(this.handleError()) catchError(this.handleError())
); ) as Observable<LoginResponse>;
} }
/** /**
* 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 {@link OperatorService} and
* send a `post`-requiest to `/users/logout/'` * send a `post`-request to `/apps/users/logout/'`
*/ */
//logout the user public logout(): void {
//TODO not yet used this.operator.user = null;
logout(): Observable<any> { this.http.post<any>(environment.urlPrefix + '/users/logout/', {}).subscribe(() => {
this.operator.clear(); this.OpenSlides.reboot();
return this.http.post<any>(environment.urlPrefix + '/users/logout/', {}); });
} }
} }

View File

@ -69,4 +69,15 @@ export class AutoupdateService extends OpenSlidesComponent {
this.DS.add(...autoupdate.changed[collection].map(_obj => new targetClass().deserialize(_obj.data))); 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);
}
} }

View File

@ -10,14 +10,14 @@ import { CollectionStringModelMapperService } from './collectionStringModelMappe
* *
* Part of {@link DataStoreService} * Part of {@link DataStoreService}
*/ */
interface Collection { interface ModelCollection {
[id: number]: BaseModel; [id: number]: BaseModel;
} }
/** /**
* Represents a serialized collection. * Represents a serialized collection.
*/ */
interface SerializedCollection { interface JsonCollection {
[id: number]: string; [id: number]: string;
} }
@ -26,15 +26,15 @@ interface SerializedCollection {
* *
* {@link DataStoreService} * {@link DataStoreService}
*/ */
interface Storage { interface ModelStorage {
[collectionString: string]: Collection; [collectionString: string]: ModelCollection;
} }
/** /**
* A storage of serialized collection elements. * A storage of serialized collection elements.
*/ */
interface SerializedStorage { interface JsonStorage {
[collectionString: string]: SerializedCollection; [collectionString: string]: JsonCollection;
} }
/** /**
@ -49,14 +49,17 @@ interface SerializedStorage {
export class DataStoreService { export class DataStoreService {
private static cachePrefix = 'DS:'; private static cachePrefix = 'DS:';
/**
* Make sure, that the Datastore only be instantiated once.
*/
private static wasInstantiated = false; private static wasInstantiated = false;
/** We will store the data twice: One as instances of the actual models in the _store /** 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 * and one serialized version in the _serializedStore for the cache. Both should be updated in
* all cases equal! * all cases equal!
*/ */
private modelStore: Storage = {}; private modelStore: ModelStorage = {};
private JsonStore: SerializedStorage = {}; private JsonStore: JsonStorage = {};
/** /**
* Observable subject with changes to enable dynamic changes in models and views * 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. * 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 * Empty constructor for dataStore
@ -85,43 +95,43 @@ export class DataStoreService {
public initFromCache(): Promise<number> { public initFromCache(): Promise<number> {
// This promise will be resolved with the maximal change id of the cache. // This promise will be resolved with the maximal change id of the cache.
return new Promise<number>(resolve => { return new Promise<number>(resolve => {
this.cacheService this.cacheService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS').subscribe((store: JsonStorage) => {
.get<SerializedStorage>(DataStoreService.cachePrefix + 'DS') if (store != null) {
.subscribe((store: SerializedStorage) => { // There is a store. Deserialize it
if (store != null) { this.JsonStore = store;
// There is a store. Deserialize it this.modelStore = this.deserializeJsonStore(this.JsonStore);
this.JsonStore = store; // Get the maxChangeId from the cache
this.modelStore = this.deserializeJsonStore(this.JsonStore); this.cacheService
// Get the maxChangeId from the cache .get<number>(DataStoreService.cachePrefix + 'maxChangeId')
this.cacheService .subscribe((maxChangeId: number) => {
.get<number>(DataStoreService.cachePrefix + 'maxChangeId') if (maxChangeId == null) {
.subscribe((maxChangeId: number) => { maxChangeId = 0;
if (maxChangeId == null) { }
maxChangeId = 0; this._maxChangeId = maxChangeId;
} resolve(maxChangeId);
this.maxChangeId = maxChangeId; });
resolve(maxChangeId); } else {
}); // No store here, so get all data from the server.
} else { resolve(0);
// No store here, so get all data from the server. }
resolve(0); });
}
});
}); });
} }
/** /**
* Deserialze the given serializedStorage and returns a Storage. * Deserialze the given serializedStorage and returns a Storage.
*/ */
private deserializeJsonStore(serializedStore: SerializedStorage): Storage { private deserializeJsonStore(serializedStore: JsonStorage): ModelStorage {
const storage: Storage = {}; const storage: ModelStorage = {};
Object.keys(serializedStore).forEach(collectionString => { Object.keys(serializedStore).forEach(collectionString => {
storage[collectionString] = {} as Collection; storage[collectionString] = {} as ModelCollection;
const target = CollectionStringModelMapperService.getCollectionStringType(collectionString); const target = CollectionStringModelMapperService.getCollectionStringType(collectionString);
Object.keys(serializedStore[collectionString]).forEach(id => { if (target) {
const data = JSON.parse(serializedStore[collectionString][id]); Object.keys(serializedStore[collectionString]).forEach(id => {
storage[collectionString][id] = new target().deserialize(data); const data = JSON.parse(serializedStore[collectionString][id]);
}); storage[collectionString][id] = new target().deserialize(data);
});
}
}); });
return storage; return storage;
} }
@ -133,7 +143,7 @@ export class DataStoreService {
public clear(callback?: (value: boolean) => void): void { public clear(callback?: (value: boolean) => void): void {
this.modelStore = {}; this.modelStore = {};
this.JsonStore = {}; this.JsonStore = {};
this.maxChangeId = 0; this._maxChangeId = 0;
this.cacheService.remove(DataStoreService.cachePrefix + 'DS', () => { this.cacheService.remove(DataStoreService.cachePrefix + 'DS', () => {
this.cacheService.remove(DataStoreService.cachePrefix + 'maxChangeId', callback); this.cacheService.remove(DataStoreService.cachePrefix + 'maxChangeId', callback);
}); });
@ -159,7 +169,7 @@ export class DataStoreService {
collectionString = tempObject.collectionString; collectionString = tempObject.collectionString;
} }
const collection: Collection = this.modelStore[collectionString]; const collection: ModelCollection = this.modelStore[collectionString];
const models = []; const models = [];
if (!collection) { if (!collection) {
@ -270,8 +280,8 @@ export class DataStoreService {
*/ */
private storeToCache(maxChangeId: number) { private storeToCache(maxChangeId: number) {
this.cacheService.set(DataStoreService.cachePrefix + 'DS', this.JsonStore); this.cacheService.set(DataStoreService.cachePrefix + 'DS', this.JsonStore);
if (maxChangeId > this.maxChangeId) { if (maxChangeId > this._maxChangeId) {
this.maxChangeId = maxChangeId; this._maxChangeId = maxChangeId;
this.cacheService.set(DataStoreService.cachePrefix + 'maxChangeId', maxChangeId); this.cacheService.set(DataStoreService.cachePrefix + 'maxChangeId', maxChangeId);
} }
} }

View File

@ -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();
}));
});

View File

@ -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<number>('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();
}
}
});
}
}

View File

@ -7,220 +7,115 @@ import { Group } from 'app/shared/models/users/group';
import { User } from '../../shared/models/users/user'; import { User } from '../../shared/models/users/user';
import { environment } from 'environments/environment'; 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. * 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 * Changes in operator can be observed, directives do so on order to show
* or hide certain information. * or hide certain information.
* *
* Could extend User?
*
* The operator is an {@link OpenSlidesComponent}. * The operator is an {@link OpenSlidesComponent}.
*/ */
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class OperatorService extends OpenSlidesComponent { export class OperatorService extends OpenSlidesComponent {
about_me: string; /**
comment: string; * The operator.
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;
private _user: User; 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. * The subject that can be observed by other instances using observing functions.
*/ */
private operatorSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null); private operatorSubject: BehaviorSubject<any> = new BehaviorSubject<any>(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 * @param http HttpClient
*/ */
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
super(); 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<any> { public setupSubscription() {
return this.http.get<any>(environment.urlPrefix + '/users/whoami/').pipe( this.DS.getObservable().subscribe(newModel => {
tap(whoami => { if (this._user) {
if (whoami && whoami.user) { if (newModel instanceof Group) {
this.storeUser(whoami.user as User); 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<WhoAmIResponse> {
return this.http.get<WhoAmIResponse>(environment.urlPrefix + '/users/whoami/').pipe(
tap((response: WhoAmIResponse) => {
if (response && response.user_id) {
this.user = new User().deserialize(response.user);
} }
}), }),
catchError(this.handleError()) catchError(this.handleError())
); ) as Observable<WhoAmIResponse>;
} }
/** /**
* Store the user Information in the operator, the localStorage and update the Observable * Returns the operatorSubject as an 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.
* *
* Services an components can use it to get informed when something changes in * Services an components can use it to get informed when something changes in
* the operator * the operator
@ -230,36 +125,35 @@ export class OperatorService extends OpenSlidesComponent {
} }
/** /**
* Inform all observers about changes * Checks, if the operator has at least one of the given permissions.
* @param value * @param permissions The permissions to check, if at least one matches.
*/ */
private setObservable(value) { public hasPerms(...permissions: Permission[]): boolean {
this.operatorSubject.next(value); 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() { private updatePermissions(): void {
return this.groups; this.permissions = [];
} if (!this.user) {
const defaultGroup = this.DS.get('users/group', 1) as Group;
/** if (defaultGroup && defaultGroup.permissions instanceof Array) {
* if the operator has the corresponding ID, set the group this.permissions = defaultGroup.permissions;
* @param newGroup potential group that the operator has. }
*/ } else {
private addGroup(newGroup: Group): void { const permissionSet = new Set();
if (this.groups_id.includes(newGroup.id as number)) { this.user.groups.forEach(group => {
this.groups.push(newGroup); group.permissions.forEach(permission => {
// inform the observers about new groups (appOsPerms) permissionSet.add(permission);
this.setObservable(newGroup); });
});
this.permissions = Array.from(permissionSet.values());
} }
} // publish changes in the operator.
this.operatorSubject.next(this.user);
/**
* get the user that corresponds to operator.
*/
get user(): User {
return this._user;
} }
} }

View File

@ -1,12 +1,19 @@
import { Injectable } from '@angular/core'; import { Injectable, NgZone } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { Observable, Subject, of } from 'rxjs';
import { Observable, Subject } 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 { interface QueryParams {
[key: string]: string; [key: string]: string;
} }
/**
* The generic message format in which messages are send and recieved by the server.
*/
interface WebsocketMessage { interface WebsocketMessage {
type: string; type: string;
content: any; content: any;
@ -14,9 +21,8 @@ interface WebsocketMessage {
} }
/** /**
* Service that handles WebSocket connections. * Service that handles WebSocket connections. Other services can register themselfs
* * with {@method getOberservable} for a specific type of messages. The content will be published.
* Creates or returns already created WebSockets.
*/ */
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -26,12 +32,29 @@ export class WebsocketService {
* Constructor that handles the router * Constructor that handles the router
* @param router the URL 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<void>();
}
/** /**
* 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<WebsocketMessage>; private connectionErrorNotice: MatSnackBarRef<SimpleSnackBar>;
/**
* Subjects that will be called, if a reconnect was successful.
*/
private reconnectSubject: Subject<void>;
/**
* The websocket.
*/
private websocket: WebSocket;
/** /**
* Subjects for types of websocket messages. A subscriber can get an Observable by {@function getOberservable}. * 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<any> } = {}; private subjects: { [type: string]: Subject<any> } = {};
/** /**
* 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 = {}; const queryParams: QueryParams = {};
// comment-in if changes IDs are supported on server side. // comment-in if changes IDs are supported on server side.
/*if (changeId !== undefined) { /*if (changeId !== undefined) {
queryParams.changeId = changeId.toString(); queryParams.changeId = changeId.toString();
}*/ }*/
// Create the websocket
const socketProtocol = this.getWebSocketProtocol(); const socketProtocol = this.getWebSocketProtocol();
const socketServer = window.location.hostname + ':' + window.location.port; const socketServer = window.location.hostname + ':' + window.location.port;
const socketPath = this.getWebSocketPath(queryParams); const socketPath = this.getWebSocketPath(queryParams);
if (!this.websocketSubject) { this.websocket = new WebSocket(socketProtocol + socketServer + socketPath);
this.websocketSubject = webSocket(socketProtocol + socketServer + socketPath);
// directly subscribe. The messages are distributes below // connection established. If this connect attept was a retry,
this.websocketSubject.subscribe(message => { // 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; const type: string = message.type;
if (type === 'error') { if (type === 'error') {
console.error('Websocket error', message.content); console.error('Websocket error', message.content);
} else if (this.subjects[type]) { } else if (this.subjects[type]) {
// Pass the content to the registered subscribers.
this.subjects[type].next(message.content); this.subjects[type].next(message.content);
} else { } else {
console.log(`Got unknown websocket message type "${type}" with content`, message.content); 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(); return this.subjects[type].asObservable();
} }
/**
* get the reconnect observable. It will be published, if a reconnect was sucessful.
*/
public getReconnectObservable(): Observable<void> {
return this.reconnectSubject.asObservable();
}
/** /**
* Sends a message to the server with the content and the given type. * Sends a message to the server with the content and the given type.
* *
* @param type the message type * @param type the message type
* @param content the actual content * @param content the actual content
*/ */
public send<T>(type: string, content: T): void { public send<T>(type: string, content: T, id?: string): void {
if (!this.websocketSubject) { if (!this.websocket) {
return; return;
} }
const message: WebsocketMessage = { const message: WebsocketMessage = {
type: type, type: type,
content: content, content: content,
id: '' id: id
}; };
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; // create message id if not given. Required by the server.
for (let i = 0; i < 8; i++) { if (!message.id) {
message.id += possible.charAt(Math.floor(Math.random() * possible.length)); 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 * returns the desired websocket protocol
*
* TODO: HTTPS is not yet tested
*/ */
private getWebSocketProtocol(): string { private getWebSocketProtocol(): string {
if (location.protocol === 'https') { if (location.protocol === 'https') {

View File

@ -3,7 +3,6 @@ import { Observable, of } from 'rxjs';
import { DataStoreService } from './core/services/data-store.service'; import { DataStoreService } from './core/services/data-store.service';
import { CacheService } from './core/services/cache.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 * 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; private static _DS: DataStoreService;
public static injector: Injector;
/** /**
* Empty constructor * Empty constructor
* *
@ -29,6 +30,9 @@ export abstract class OpenSlidesComponent {
* @return access to dataStoreService * @return access to dataStoreService
*/ */
get DS(): 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) { if (OpenSlidesComponent._DS == null) {
const injector = Injector.create({ const injector = Injector.create({
providers: [ providers: [
@ -38,7 +42,7 @@ export abstract class OpenSlidesComponent {
deps: [CacheService] deps: [CacheService]
} }
], ],
parent: RootInjector.injector parent: OpenSlidesComponent.injector
}); });
OpenSlidesComponent._DS = injector.get(DataStoreService); OpenSlidesComponent._DS = injector.get(DataStoreService);
} }

View File

@ -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 { 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 * Directive to check if the {@link OperatorService} has the correct permissions to access certain functions
* *
* Successor of os-perms in OpenSlides 2.2 * Successor of os-perms in OpenSlides 2.2
* @example <div *appOsPerms=".." ..> ... < /div> * @example <div *appOsPerms="'perm'" ..> ... < /div>
* @example <div *appOsPerms="['perm1', 'perm2']" ..> ... < /div>
*/ */
@Directive({ @Directive({
selector: '[appOsPerms]' selector: '[appOsPerms]'
}) })
export class OsPermsDirective extends OpenSlidesComponent { export class OsPermsDirective extends OpenSlidesComponent {
/**
* Holds the {@link OperatorService} permissions
*/
private userPermissions: string[];
/** /**
* Holds the required permissions the access a feature * 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 * Holds the value of the last permission check. Therefore one can check, if the
* dynamically * 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 template inner part of the HTML container
* @param viewContainer outer part of the HTML container (for example a `<div>`) * @param viewContainer outer part of the HTML container (for example a `<div>`)
@ -39,50 +39,44 @@ export class OsPermsDirective extends OpenSlidesComponent {
private operator: OperatorService private operator: OperatorService
) { ) {
super(); super();
this.userPermissions = [];
// observe groups of operator, so the directive can actively react to changes // observe groups of operator, so the directive can actively react to changes
this.operator.getObservable().subscribe(content => { this.operator.getObservable().subscribe(content => {
if (content instanceof Group && this.permissions !== '') { this.updateView();
this.userPermissions = [...this.userPermissions, ...content.permissions];
this.updateView();
}
}); });
} }
/** /**
* Comes directly from the view. * 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() @Input()
set appOsPerms(value) { set appOsPerms(value) {
if (!value) {
value = [];
} else if (typeof value === 'string') {
value = [value];
}
this.permissions = value; this.permissions = value;
this.readUserPermissions();
this.updateView(); 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. * Shows or hides certain content in the view.
*/ */
private updateView(): void { private updateView(): void {
if (this.checkPermissions()) { const hasPerms = this.checkPermissions();
// will just render the page normally const permsChanged = hasPerms !== this.lastPermissionCheckResult;
if (hasPerms && permsChanged) {
// clean up and add the template
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.template); this.viewContainer.createEmbeddedView(this.template);
} else { } else if (!hasPerms) {
// will remove the content of the container // will remove the content of the container
this.viewContainer.clear(); this.viewContainer.clear();
} }
this.lastPermissionCheckResult = hasPerms;
} }
/** /**
@ -90,12 +84,6 @@ export class OsPermsDirective extends OpenSlidesComponent {
* Returns true if the users permissions fit. * Returns true if the users permissions fit.
*/ */
private checkPermissions(): boolean { private checkPermissions(): boolean {
let isPermitted = false; return this.permissions.length === 0 || this.operator.hasPerms(...this.permissions);
if (this.userPermissions && this.permissions) {
this.permissions.forEach(perm => {
isPermitted = this.userPermissions.find(userPerm => userPerm === perm) ? true : false;
});
}
return isPermitted;
} }
} }

View File

@ -1,4 +1,5 @@
import { BaseModel } from '../base.model'; import { BaseModel } from '../base.model';
import { Group } from './group';
/** /**
* Representation of a user in contrast to the operator. * Representation of a user in contrast to the operator.
@ -61,11 +62,38 @@ export class User extends BaseModel {
this.default_password = default_password; this.default_password = default_password;
} }
getGroups(): BaseModel | BaseModel[] { get groups(): Group[] {
return this.DS.get('users/group', ...this.groups_id); 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" // TODO read config values for "users_sort_by"
get short_name(): string { get short_name(): string {

View File

@ -8,7 +8,7 @@
everyone should see this everyone should see this
</div> </div>
<br/> <br/>
<div *appOsPerms="['agenda.can_see']"> <div *appOsPerms="'agenda.can_see'">
Only permitted users should see this Only permitted users should see this
</div> </div>
</div> </div>

View File

@ -18,11 +18,14 @@
<!-- forgot password button --> <!-- forgot password button -->
<br> <br>
<button class='forgot-password-button' mat-button>Forgot Password?</button> <button type="button" class='forgot-password-button' (click)="resetPassword()" mat-button>Forgot Password?</button>
<!-- login button --> <!-- login button -->
<br> <br>
<button mat-raised-button color="primary" class='login-button' type="submit">Login</button> <!-- TODO: Next to each other...-->
<button mat-raised-button color="primary" class='login-button' type="submit" translate>Login</button>
<button mat-raised-button *ngIf="areGuestsEnabled()" color="primary" class='login-button'
type="button" (click)="guestLogin()" translate>Login as Guest</button>
</form> </form>
</div> </div>

View File

@ -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 { Router } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { AuthService } from 'app/core/services/auth.service'; import { AuthService } from 'app/core/services/auth.service';
import { OperatorService } from 'app/core/services/operator.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 { FormControl, FormGroupDirective, NgForm, FormGroup, Validators, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; 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. * Custom error states. Might become part of the shared module later.
@ -38,31 +41,36 @@ export class ParentErrorStateMatcher implements ErrorStateMatcher {
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'] 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 * 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<SimpleSnackBar>;
/** /**
* Login Error Message if any * Login Error Message if any
*/ */
loginErrorMsg = ''; public loginErrorMsg = '';
/** /**
* Form group for the login form * Form group for the login form
*/ */
loginForm: FormGroup; public loginForm: FormGroup;
/** /**
* Custom Form validation * Custom Form validation
*/ */
parentErrorStateMatcher = new ParentErrorStateMatcher(); public parentErrorStateMatcher = new ParentErrorStateMatcher();
/** /**
* Show the Spinner if validation is in process * Show the Spinner if validation is in process
*/ */
inProcess = false; public inProcess = false;
/** /**
* Constructor for the login component * Constructor for the login component
@ -79,7 +87,10 @@ export class LoginComponent extends BaseComponent implements OnInit {
private authService: AuthService, private authService: AuthService,
private operator: OperatorService, private operator: OperatorService,
private router: Router, private router: Router,
private formBuilder: FormBuilder private formBuilder: FormBuilder,
private http: HttpClient,
private matSnackBar: MatSnackBar,
private OpenSlides: OpenSlidesService
) { ) {
super(titleService, translate); super(titleService, translate);
this.createForm(); this.createForm();
@ -91,23 +102,26 @@ export class LoginComponent extends BaseComponent implements OnInit {
* Set the title to "Log In" * Set the title to "Log In"
* Observes the operator, if a user was already logged in, recreate to user and skip the login * Observes the operator, if a user was already logged in, recreate to user and skip the login
*/ */
ngOnInit() { public ngOnInit(): void {
//this is necessary since the HTML document never uses the word "Log In" super.setTitle('Login');
const loginWord = this.translate.instant('Log In');
super.setTitle('Log In');
// if there is stored login information, try to login directly. this.http.get<any>(environment.urlPrefix + '/users/login/', {}).subscribe(response => {
this.operator.getObservable().subscribe(user => { this.installationNotice = this.matSnackBar.open(response.info_text, this.translate.instant('OK'), {
if (user && user.id) { duration: 5000
this.router.navigate(['/']); });
}
}); });
} }
public ngOnDestroy(): void {
if (this.installationNotice) {
this.installationNotice.dismiss();
}
}
/** /**
* Create the login Form * Create the login Form
*/ */
createForm() { public createForm(): void {
this.loginForm = this.formBuilder.group({ this.loginForm = this.formBuilder.group({
username: ['', [Validators.required, Validators.maxLength(128)]], username: ['', [Validators.required, Validators.maxLength(128)]],
password: ['', [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} * Send username and password to the {@link AuthService}
*/ */
formLogin(): void { public formLogin(): void {
this.loginErrorMsg = ''; this.loginErrorMsg = '';
this.inProcess = true; this.inProcess = true;
this.authService.login(this.loginForm.value.username, this.loginForm.value.password).subscribe(res => { 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({ this.loginForm.setErrors({
notFound: true notFound: true
}); });
this.loginErrorMsg = res.error.detail; this.loginErrorMsg = res.error.detail;
} else { } else {
this.inProcess = false; this.OpenSlides.afterLoginBootup(res.user_id);
if (res.user_id) { let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/';
const redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/'; if (redirect.includes('login')) {
this.router.navigate([redirect]); 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(['/']);
}
} }

View File

@ -1,7 +1,7 @@
<app-head-bar appName="Settings"></app-head-bar> <app-head-bar appName="Settings"></app-head-bar>
<mat-card class="os-card"> <mat-card class="os-card">
<div *appOsPerms="['core.can_manage_config']" class="app-content"> <div *appOsPerms="'core.can_manage_config'" class="app-content">
<mat-accordion> <mat-accordion>
<mat-expansion-panel> <mat-expansion-panel>

View File

@ -6,6 +6,7 @@ import { SiteComponent } from './site.component';
import { StartComponent } from './start/start.component'; import { StartComponent } from './start/start.component';
import { PrivacyPolicyComponent } from './privacy-policy/privacy-policy.component'; import { PrivacyPolicyComponent } from './privacy-policy/privacy-policy.component';
import { LegalNoticeComponent } from './legal-notice/legal-notice.component'; import { LegalNoticeComponent } from './legal-notice/legal-notice.component';
import { AuthGuard } from '../core/services/auth-guard.service';
// import { LoginComponent } from './login/login.component'; // import { LoginComponent } from './login/login.component';
/** /**
@ -37,7 +38,8 @@ const routes: Routes = [
loadChildren: './settings/settings.module#SettingsModule' loadChildren: './settings/settings.module#SettingsModule'
}, },
{ path: 'users', loadChildren: './users/users.module#UsersModule' } { path: 'users', loadChildren: './users/users.module#UsersModule' }
] ],
canActivateChild: [AuthGuard]
} }
]; ];

View File

@ -17,19 +17,23 @@
<fa-icon icon='globe-americas'></fa-icon> <fa-icon icon='globe-americas'></fa-icon>
<span> {{getLangName(this.translate.currentLang)}} </span> <span> {{getLangName(this.translate.currentLang)}} </span>
</a> </a>
<a (click)='logOutButton()' mat-list-item> <a *ngIf="isLoggedIn" (click)='editProfile()' mat-list-item>
<fa-icon icon='user-cog'></fa-icon> <fa-icon icon='user-cog'></fa-icon>
<span translate>Edit Profile</span> <span translate>Edit Profile</span>
</a> </a>
<a (click)='logOutButton()' mat-list-item> <a *ngIf="isLoggedIn" (click)='changePassword()' mat-list-item>
<fa-icon icon='key'></fa-icon> <fa-icon icon='key'></fa-icon>
<span translate>Change Password</span> <span translate>Change Password</span>
</a> </a>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<a (click)='logOutButton()' mat-list-item> <a *ngIf="isLoggedIn" (click)='logout()' mat-list-item>
<fa-icon icon='sign-out-alt'></fa-icon> <fa-icon icon='sign-out-alt'></fa-icon>
<span translate>Logout</span> <span translate>Logout</span>
</a> </a>
<a *ngIf="!isLoggedIn" routerLink='/login' mat-list-item>
<fa-icon icon='sign-out-alt'></fa-icon>
<span translate>Login</span>
</a>
</mat-nav-list> </mat-nav-list>
</mat-expansion-panel> </mat-expansion-panel>
<!-- TODO: Could use translate.getLangs() to fetch available languages--> <!-- TODO: Could use translate.getLangs() to fetch available languages-->
@ -41,39 +45,39 @@
<!-- navigation --> <!-- navigation -->
<mat-nav-list class='main-nav'> <mat-nav-list class='main-nav'>
<a [@navItemAnim] *appOsPerms="['core.can_see_frontpage']" mat-list-item routerLink='/' routerLinkActive='active' [routerLinkActiveOptions]="{exact: true}" <a [@navItemAnim] *appOsPerms="'core.can_see_frontpage'" mat-list-item routerLink='/' routerLinkActive='active' [routerLinkActiveOptions]="{exact: true}"
(click)='toggleSideNav()'> (click)='toggleSideNav()'>
<fa-icon icon='home'></fa-icon> <fa-icon icon='home'></fa-icon>
<span translate>Home</span> <span translate>Home</span>
</a> </a>
<a [@navItemAnim] *appOsPerms="['agenda.can_see']" mat-list-item routerLink='/agenda' routerLinkActive='active' (click)='toggleSideNav()'> <a [@navItemAnim] *appOsPerms="'agenda.can_see'" mat-list-item routerLink='/agenda' routerLinkActive='active' (click)='toggleSideNav()'>
<fa-icon icon='calendar'></fa-icon> <fa-icon icon='calendar'></fa-icon>
<span translate>Agenda</span> <span translate>Agenda</span>
</a> </a>
<a [@navItemAnim] *appOsPerms="['motions.can_see']" mat-list-item routerLink='/motions' routerLinkActive='active' (click)='toggleSideNav()'> <a [@navItemAnim] *appOsPerms="'motions.can_see'" mat-list-item routerLink='/motions' routerLinkActive='active' (click)='toggleSideNav()'>
<fa-icon icon='file-alt'></fa-icon> <fa-icon icon='file-alt'></fa-icon>
<span translate>Motions</span> <span translate>Motions</span>
</a> </a>
<a [@navItemAnim] *appOsPerms="['assignments.can_see']" mat-list-item routerLink='/assignments' routerLinkActive='active' <a [@navItemAnim] *appOsPerms="'assignments.can_see'" mat-list-item routerLink='/assignments' routerLinkActive='active'
(click)='vp.isMobile ? sideNav.toggle() : null'> (click)='vp.isMobile ? sideNav.toggle() : null'>
<fa-icon icon='chart-pie'></fa-icon> <fa-icon icon='chart-pie'></fa-icon>
<span translate>Assignments</span> <span translate>Assignments</span>
</a> </a>
<a [@navItemAnim] *appOsPerms="['users.can_see_name']" mat-list-item routerLink='/users' routerLinkActive='active' (click)='toggleSideNav()'> <a [@navItemAnim] *appOsPerms="'users.can_see_name'" mat-list-item routerLink='/users' routerLinkActive='active' (click)='toggleSideNav()'>
<fa-icon icon='user'></fa-icon> <fa-icon icon='user'></fa-icon>
<span translate>Participants</span> <span translate>Participants</span>
</a> </a>
<a [@navItemAnim] *appOsPerms="['mediafiles.can_see']" mat-list-item routerLink='/mediafiles' routerLinkActive='active' (click)='toggleSideNav()'> <a [@navItemAnim] *appOsPerms="'mediafiles.can_see'" mat-list-item routerLink='/mediafiles' routerLinkActive='active' (click)='toggleSideNav()'>
<fa-icon icon='paperclip'></fa-icon> <fa-icon icon='paperclip'></fa-icon>
<span translate>Files</span> <span translate>Files</span>
</a> </a>
<a [@navItemAnim] *appOsPerms="['core.can_manage_config']" mat-list-item routerLink='/settings' routerLinkActive='active' <a [@navItemAnim] *appOsPerms="'core.can_manage_config'" mat-list-item routerLink='/settings' routerLinkActive='active'
(click)='toggleSideNav()'> (click)='toggleSideNav()'>
<fa-icon icon='cog'></fa-icon> <fa-icon icon='cog'></fa-icon>
<span translate>Settings</span> <span translate>Settings</span>
</a> </a>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<a [@navItemAnim] *appOsPerms="['core.can_see_projector']" mat-list-item routerLink='/projector' routerLinkActive='active' <a [@navItemAnim] *appOsPerms="'core.can_see_projector'" mat-list-item routerLink='/projector' routerLinkActive='active'
(click)='toggleSideNav()'> (click)='toggleSideNav()'>
<fa-icon icon='video'></fa-icon> <fa-icon icon='video'></fa-icon>
<span translate>Projector</span> <span translate>Projector</span>

View File

@ -3,14 +3,12 @@ import { Router } from '@angular/router';
import { AuthService } from 'app/core/services/auth.service'; import { AuthService } from 'app/core/services/auth.service';
import { OperatorService } from 'app/core/services/operator.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 { TranslateService } from '@ngx-translate/core'; //showcase
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { pageTransition, navItemAnim } from 'app/shared/animations'; import { pageTransition, navItemAnim } from 'app/shared/animations';
import { MatDialog, MatSidenav } from '@angular/material'; import { MatDialog, MatSidenav } from '@angular/material';
import { ViewportService } from '../core/services/viewport.service'; import { ViewportService } from '../core/services/viewport.service';
import { CacheService } from '../core/services/cache.service';
@Component({ @Component({
selector: 'app-site', selector: 'app-site',
@ -27,30 +25,39 @@ export class SiteComponent extends BaseComponent implements OnInit {
/** /**
* Get the username from the operator (should be known already) * 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 * Constructor
* *
* @param authService * @param authService
* @param websocketService
* @param operator * @param operator
* @param router * @param vp
* @param breakpointObserver
* @param translate * @param translate
* @param dialog * @param dialog
*/ */
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private websocketService: WebsocketService,
private operator: OperatorService, private operator: OperatorService,
private router: Router,
public vp: ViewportService, public vp: ViewportService,
public translate: TranslateService, public translate: TranslateService,
public dialog: MatDialog, public dialog: MatDialog
private cacheService: CacheService
) { ) {
super(); 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) => { // this.translate.get('Motions').subscribe((res: string) => {
// console.log('translation of motions in the target language: ' + res); // 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<number>('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 * Function to log out the current user
*/ */
logOutButton() { logout() {
this.authService.logout().subscribe(); this.authService.logout();
this.router.navigate(['/login']);
} }
} }

View File

@ -5,9 +5,6 @@
<h4> {{welcomeTitle | translate}} </h4> <h4> {{welcomeTitle | translate}} </h4>
<span> {{welcomeText | translate}} </span> <span> {{welcomeText | translate}} </span>
<br/>
<!-- example to translate with parameters -->
<p>{{'Hello user' | translate:username}}</p>
<button mat-button (click)="DataStoreTest()">DataStoreTest</button> <button mat-button (click)="DataStoreTest()">DataStoreTest</button>
<br/> <br/>

View File

@ -20,7 +20,6 @@ import { MotionSubmitter } from '../../shared/models/motions/motion-submitter';
export class StartComponent extends BaseComponent implements OnInit { export class StartComponent extends BaseComponent implements OnInit {
welcomeTitle: string; welcomeTitle: string;
welcomeText: string; welcomeText: string;
username = { user: this.operator.username };
/** /**
* Constructor of the StartComponent * Constructor of the StartComponent

View File

@ -3,7 +3,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import { RootInjector } from 'app/core/rootInjector'; import { AppComponent } from 'app/app.component';
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
@ -12,6 +12,6 @@ if (environment.production) {
platformBrowserDynamic() platformBrowserDynamic()
.bootstrapModule(AppModule) .bootstrapModule(AppModule)
.then((moduleRef: NgModuleRef<AppModule>) => { .then((moduleRef: NgModuleRef<AppModule>) => {
RootInjector.injector = moduleRef.injector; AppComponent.bootstrapDone(moduleRef);
}) })
.catch(err => console.log(err)); .catch(err => console.log(err));