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
This commit is contained in:
FinnStutzenstein 2018-08-28 11:07:10 +02:00
parent 4f463470fc
commit 4e41e8c603
24 changed files with 689 additions and 451 deletions

View File

@ -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: '' }
];

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 { 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<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 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<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 { 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: '<perm>'}` or
* `data: {basePerm: ['<perm1>', '<perm2>']}` 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);
}
}

View File

@ -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<any> {
const user: any = {
public login(username: string, password: string): Observable<LoginResponse> {
const user = {
username: username,
password: password
};
return this.http.post<any>(environment.urlPrefix + '/users/login/', user).pipe(
tap(resp => this.operator.storeUser(resp.user)),
return this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user).pipe(
tap((response: LoginResponse) => {
this.operator.user = new User().deserialize(response.user);
}),
catchError(this.handleError())
);
) as Observable<LoginResponse>;
}
/**
* 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<any> {
this.operator.clear();
return this.http.post<any>(environment.urlPrefix + '/users/logout/', {});
public logout(): void {
this.operator.user = null;
this.http.post<any>(environment.urlPrefix + '/users/logout/', {}).subscribe(() => {
this.OpenSlides.reboot();
});
}
}

View File

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

View File

@ -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<number> {
// This promise will be resolved with the maximal change id of the cache.
return new Promise<number>(resolve => {
this.cacheService
.get<SerializedStorage>(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<number>(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<JsonStorage>(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<number>(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);
}
}

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 { 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<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
*/
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<any> {
return this.http.get<any>(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<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())
);
) as Observable<WhoAmIResponse>;
}
/**
* 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);
}
}

View File

@ -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<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}.
@ -39,33 +62,86 @@ export class WebsocketService {
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 = {};
// 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<void> {
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<T>(type: string, content: T): void {
if (!this.websocketSubject) {
public send<T>(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') {

View File

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

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 { 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 *appOsPerms=".." ..> ... < /div>
* @example <div *appOsPerms="'perm'" ..> ... < /div>
* @example <div *appOsPerms="['perm1', 'perm2']" ..> ... < /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 `<div>`)
@ -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);
}
}

View File

@ -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 {

View File

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

View File

@ -18,11 +18,14 @@
<!-- forgot password button -->
<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 -->
<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>
</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 { 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<SimpleSnackBar>;
/**
* 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<any>(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(['/']);
}
}

View File

@ -1,7 +1,7 @@
<app-head-bar appName="Settings"></app-head-bar>
<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-expansion-panel>

View File

@ -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]
}
];

View File

@ -17,19 +17,23 @@
<fa-icon icon='globe-americas'></fa-icon>
<span> {{getLangName(this.translate.currentLang)}} </span>
</a>
<a (click)='logOutButton()' mat-list-item>
<a *ngIf="isLoggedIn" (click)='editProfile()' mat-list-item>
<fa-icon icon='user-cog'></fa-icon>
<span translate>Edit Profile</span>
</a>
<a (click)='logOutButton()' mat-list-item>
<a *ngIf="isLoggedIn" (click)='changePassword()' mat-list-item>
<fa-icon icon='key'></fa-icon>
<span translate>Change Password</span>
</a>
<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>
<span translate>Logout</span>
</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-expansion-panel>
<!-- TODO: Could use translate.getLangs() to fetch available languages-->
@ -41,39 +45,39 @@
<!-- navigation -->
<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()'>
<fa-icon icon='home'></fa-icon>
<span translate>Home</span>
</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>
<span translate>Agenda</span>
</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>
<span translate>Motions</span>
</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'>
<fa-icon icon='chart-pie'></fa-icon>
<span translate>Assignments</span>
</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>
<span translate>Participants</span>
</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>
<span translate>Files</span>
</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()'>
<fa-icon icon='cog'></fa-icon>
<span translate>Settings</span>
</a>
<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()'>
<fa-icon icon='video'></fa-icon>
<span translate>Projector</span>

View File

@ -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<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
*/
logOutButton() {
this.authService.logout().subscribe();
this.router.navigate(['/login']);
logout() {
this.authService.logout();
}
}

View File

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

View File

@ -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

View File

@ -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<AppModule>) => {
RootInjector.injector = moduleRef.injector;
AppComponent.bootstrapDone(moduleRef);
})
.catch(err => console.log(err));