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 { 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: '' }
|
||||
];
|
||||
|
||||
|
@ -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!
|
||||
}
|
||||
}
|
||||
|
@ -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 { 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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,9 +95,7 @@ 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) => {
|
||||
this.cacheService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS').subscribe((store: JsonStorage) => {
|
||||
if (store != null) {
|
||||
// There is a store. Deserialize it
|
||||
this.JsonStore = store;
|
||||
@ -99,7 +107,7 @@ export class DataStoreService {
|
||||
if (maxChangeId == null) {
|
||||
maxChangeId = 0;
|
||||
}
|
||||
this.maxChangeId = maxChangeId;
|
||||
this._maxChangeId = maxChangeId;
|
||||
resolve(maxChangeId);
|
||||
});
|
||||
} else {
|
||||
@ -113,15 +121,17 @@ export class DataStoreService {
|
||||
/**
|
||||
* 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
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 { 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;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
// 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') {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
this.permissions = value;
|
||||
this.readUserPermissions();
|
||||
this.updateView();
|
||||
if (!value) {
|
||||
value = [];
|
||||
} else if (typeof value === 'string') {
|
||||
value = [value];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
});
|
||||
this.permissions = value;
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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;
|
||||
|
||||
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(['/']);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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/>
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user