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:
parent
4f463470fc
commit
4e41e8c603
@ -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: '' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { Injector } from '@angular/core';
|
|
||||||
|
|
||||||
export class RootInjector {
|
|
||||||
constructor() {}
|
|
||||||
public static injector: Injector;
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/', {});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,9 +95,7 @@ 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')
|
|
||||||
.subscribe((store: SerializedStorage) => {
|
|
||||||
if (store != null) {
|
if (store != null) {
|
||||||
// There is a store. Deserialize it
|
// There is a store. Deserialize it
|
||||||
this.JsonStore = store;
|
this.JsonStore = store;
|
||||||
@ -99,7 +107,7 @@ export class DataStoreService {
|
|||||||
if (maxChangeId == null) {
|
if (maxChangeId == null) {
|
||||||
maxChangeId = 0;
|
maxChangeId = 0;
|
||||||
}
|
}
|
||||||
this.maxChangeId = maxChangeId;
|
this._maxChangeId = maxChangeId;
|
||||||
resolve(maxChangeId);
|
resolve(maxChangeId);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -113,15 +121,17 @@ export class DataStoreService {
|
|||||||
/**
|
/**
|
||||||
* 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);
|
||||||
|
if (target) {
|
||||||
Object.keys(serializedStore[collectionString]).forEach(id => {
|
Object.keys(serializedStore[collectionString]).forEach(id => {
|
||||||
const data = JSON.parse(serializedStore[collectionString][id]);
|
const data = JSON.parse(serializedStore[collectionString][id]);
|
||||||
storage[collectionString][id] = new target().deserialize(data);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
client/src/app/core/services/openslides.service.spec.ts
Normal file
15
client/src/app/core/services/openslides.service.spec.ts
Normal 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();
|
||||||
|
}));
|
||||||
|
});
|
135
client/src/app/core/services/openslides.service.ts
Normal file
135
client/src/app/core/services/openslides.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
this.permissions = defaultGroup.permissions;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
/**
|
const permissionSet = new Set();
|
||||||
* if the operator has the corresponding ID, set the group
|
this.user.groups.forEach(group => {
|
||||||
* @param newGroup potential group that the operator has.
|
group.permissions.forEach(permission => {
|
||||||
*/
|
permissionSet.add(permission);
|
||||||
private addGroup(newGroup: Group): void {
|
});
|
||||||
if (this.groups_id.includes(newGroup.id as number)) {
|
});
|
||||||
this.groups.push(newGroup);
|
this.permissions = Array.from(permissionSet.values());
|
||||||
// inform the observers about new groups (appOsPerms)
|
|
||||||
this.setObservable(newGroup);
|
|
||||||
}
|
}
|
||||||
}
|
// publish changes in the operator.
|
||||||
|
this.operatorSubject.next(this.user);
|
||||||
/**
|
|
||||||
* get the user that corresponds to operator.
|
|
||||||
*/
|
|
||||||
get user(): User {
|
|
||||||
return this._user;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// create message id if not given. Required by the server.
|
||||||
|
if (!message.id) {
|
||||||
|
message.id = '';
|
||||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
message.id += possible.charAt(Math.floor(Math.random() * possible.length));
|
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') {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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.userPermissions = [...this.userPermissions, ...content.permissions];
|
|
||||||
this.updateView();
|
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) {
|
||||||
this.permissions = value;
|
if (!value) {
|
||||||
this.readUserPermissions();
|
value = [];
|
||||||
this.updateView();
|
} else if (typeof value === 'string') {
|
||||||
|
value = [value];
|
||||||
}
|
}
|
||||||
|
this.permissions = value;
|
||||||
/**
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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(['/']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/>
|
||||||
|
@ -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
|
||||||
|
@ -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));
|
||||||
|
Loading…
Reference in New Issue
Block a user