Merge pull request #3948 from FinnStutzenstein/changeIdClient
ChangeId for the client
This commit is contained in:
commit
85b91e7101
@ -10,7 +10,7 @@
|
|||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"compodoc": "./node_modules/.bin/compodoc --hideGenerator -p src/tsconfig.app.json -n 'OpenSlides Documentation' -d ../Compodoc -s -w -t -o --port",
|
"compodoc": "./node_modules/.bin/compodoc --hideGenerator -p src/tsconfig.app.json -n 'OpenSlides Documentation' -d ../Compodoc -s -w -t -o --port",
|
||||||
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/{en,de,fr}.json --clean --sort --format-indentation ' ' --format namespaced-json",
|
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/{en,de,fr}.json --clean --sort --format-indentation ' ' --format namespaced-json",
|
||||||
"format:fix": "pretty-quick --staged"
|
"prettify": "pretty-quick"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
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 { User } from '../../shared/models/users/user';
|
||||||
import { OpenSlidesService } from './openslides.service';
|
import { OpenSlidesService } from './openslides.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { HttpService } from './http.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data returned by a post request to the login route.
|
* The data returned by a post request to the login route.
|
||||||
@ -28,13 +27,16 @@ export class AuthService extends OpenSlidesComponent {
|
|||||||
* Initializes the httpClient and the {@link OperatorService}.
|
* Initializes the httpClient and the {@link OperatorService}.
|
||||||
*
|
*
|
||||||
* Calls `super()` from the parent class.
|
* Calls `super()` from the parent class.
|
||||||
* @param http HttpClient
|
* @param http HttpService to send requests to the server
|
||||||
* @param operator who is using OpenSlides
|
* @param operator Who is using OpenSlides
|
||||||
|
* @param OpenSlides The openslides service
|
||||||
|
* @param router To navigate
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private http: HttpClient,
|
private http: HttpService,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
private OpenSlides: OpenSlidesService
|
private OpenSlides: OpenSlidesService,
|
||||||
|
private router: Router
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -48,18 +50,16 @@ export class AuthService extends OpenSlidesComponent {
|
|||||||
*
|
*
|
||||||
* @param username
|
* @param username
|
||||||
* @param password
|
* @param password
|
||||||
|
* @returns The login response.
|
||||||
*/
|
*/
|
||||||
public login(username: string, password: string): Observable<LoginResponse> {
|
public async login(username: string, password: string): Promise<LoginResponse> {
|
||||||
const user = {
|
const user = {
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password
|
||||||
};
|
};
|
||||||
return this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user).pipe(
|
const response = await this.http.post<LoginResponse>(environment.urlPrefix + '/users/login/', user);
|
||||||
tap((response: LoginResponse) => {
|
|
||||||
this.operator.user = new User(response.user);
|
this.operator.user = new User(response.user);
|
||||||
}),
|
return response;
|
||||||
catchError(this.handleError())
|
|
||||||
) as Observable<LoginResponse>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,10 +68,14 @@ export class AuthService extends OpenSlidesComponent {
|
|||||||
* Will clear the current {@link OperatorService} and
|
* Will clear the current {@link OperatorService} and
|
||||||
* send a `post`-request to `/apps/users/logout/'`
|
* send a `post`-request to `/apps/users/logout/'`
|
||||||
*/
|
*/
|
||||||
public logout(): void {
|
public async logout(): Promise<void> {
|
||||||
this.operator.user = null;
|
this.operator.user = null;
|
||||||
this.http.post<any>(environment.urlPrefix + '/users/logout/', {}).subscribe(() => {
|
try {
|
||||||
|
await this.http.post(environment.urlPrefix + '/users/logout/', {});
|
||||||
|
} catch (e) {
|
||||||
|
// We do nothing on failures. Reboot OpenSlides anyway.
|
||||||
|
}
|
||||||
|
this.router.navigate(['/']);
|
||||||
this.OpenSlides.reboot();
|
this.OpenSlides.reboot();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { WebsocketService } from './websocket.service';
|
|||||||
|
|
||||||
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
||||||
import { DataStoreService } from './data-store.service';
|
import { DataStoreService } from './data-store.service';
|
||||||
|
import { BaseModel } from '../../shared/models/base/base-model';
|
||||||
|
|
||||||
interface AutoupdateFormat {
|
interface AutoupdateFormat {
|
||||||
/**
|
/**
|
||||||
@ -22,9 +23,19 @@ interface AutoupdateFormat {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current change id for this autoupdate
|
* The lower change id bond for this autoupdate
|
||||||
*/
|
*/
|
||||||
change_id: number;
|
from_change_id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The upper change id bound for this autoupdate
|
||||||
|
*/
|
||||||
|
to_change_id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag, if this autoupdate contains all data. If so, the DS needs to be resetted.
|
||||||
|
*/
|
||||||
|
all_data: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,14 +52,16 @@ export class AutoupdateService extends OpenSlidesComponent {
|
|||||||
/**
|
/**
|
||||||
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||||
* @param websocketService
|
* @param websocketService
|
||||||
|
* @param DS
|
||||||
|
* @param modelMapper
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private DS: DataStoreService,
|
private DS: DataStoreService,
|
||||||
private modelMapper: CollectionStringModelMapperService
|
private modelMapper: CollectionStringModelMapperService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
websocketService.getOberservable<AutoupdateFormat>('autoupdate').subscribe(response => {
|
this.websocketService.getOberservable<AutoupdateFormat>('autoupdate').subscribe(response => {
|
||||||
this.storeResponse(response);
|
this.storeResponse(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -56,37 +69,87 @@ export class AutoupdateService extends OpenSlidesComponent {
|
|||||||
/**
|
/**
|
||||||
* Handle the answer of incoming data via {@link WebsocketService}.
|
* Handle the answer of incoming data via {@link WebsocketService}.
|
||||||
*
|
*
|
||||||
* Bundles the data per action and collection. THis speeds up the caching in the DataStore.
|
|
||||||
*
|
|
||||||
* Detects the Class of an incomming model, creates a new empty object and assigns
|
* Detects the Class of an incomming model, creates a new empty object and assigns
|
||||||
* the data to it using the deserialize function.
|
* the data to it using the deserialize function. Also models that are flagged as deleted
|
||||||
|
* will be removed from the data store.
|
||||||
*
|
*
|
||||||
* Saves models in DataStore.
|
* Handles the change ids of all autoupdates.
|
||||||
*/
|
*/
|
||||||
public storeResponse(autoupdate: AutoupdateFormat): void {
|
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
|
||||||
// Delete the removed objects from the DataStore
|
console.log('got autoupdate', autoupdate);
|
||||||
Object.keys(autoupdate.deleted).forEach(collection => {
|
|
||||||
this.DS.remove(collection, autoupdate.deleted[collection], autoupdate.change_id);
|
if (autoupdate.all_data) {
|
||||||
|
await this.storeAllData(autoupdate);
|
||||||
|
} else {
|
||||||
|
await this.storePartialAutoupdate(autoupdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all data from the autoupdate. This means, that the DS is resettet and filled with just the
|
||||||
|
* given data from the autoupdate.
|
||||||
|
* @param autoupdate The autoupdate
|
||||||
|
*/
|
||||||
|
private async storeAllData(autoupdate: AutoupdateFormat): Promise<void> {
|
||||||
|
let elements: BaseModel[] = [];
|
||||||
|
Object.keys(autoupdate.changed).forEach(collection => {
|
||||||
|
elements = elements.concat(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
||||||
});
|
});
|
||||||
|
await this.DS.set(elements, autoupdate.to_change_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles a normal autoupdate that is not a full update (all_data=false).
|
||||||
|
* @param autoupdate The autoupdate
|
||||||
|
*/
|
||||||
|
private async storePartialAutoupdate(autoupdate: AutoupdateFormat): Promise<void> {
|
||||||
|
const maxChangeId = this.DS.maxChangeId;
|
||||||
|
|
||||||
|
if (autoupdate.from_change_id <= maxChangeId && autoupdate.to_change_id <= maxChangeId) {
|
||||||
|
console.log('ignore');
|
||||||
|
return; // Ignore autoupdates, that lay full behind our changeid.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal autoupdate
|
||||||
|
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
||||||
|
// Delete the removed objects from the DataStore
|
||||||
|
for (const collection of Object.keys(autoupdate.deleted)) {
|
||||||
|
await this.DS.remove(collection, autoupdate.deleted[collection]);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the objects to the DataStore.
|
// Add the objects to the DataStore.
|
||||||
Object.keys(autoupdate.changed).forEach(collection => {
|
for (const collection of Object.keys(autoupdate.changed)) {
|
||||||
|
await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('new max change id', autoupdate.to_change_id);
|
||||||
|
await this.DS.flushToStorage(autoupdate.to_change_id);
|
||||||
|
} else {
|
||||||
|
// autoupdate fully in the future. we are missing something!
|
||||||
|
this.requestChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates baseModels for each plain object
|
||||||
|
* @param collection The collection all models have to be from.
|
||||||
|
* @param models All models that should be mapped to BaseModels
|
||||||
|
* @returns A list of basemodels constructed from the given models.
|
||||||
|
*/
|
||||||
|
private mapObjectsToBaseModels(collection: string, models: object[]): BaseModel[] {
|
||||||
const targetClass = this.modelMapper.getModelConstructor(collection);
|
const targetClass = this.modelMapper.getModelConstructor(collection);
|
||||||
if (!targetClass) {
|
if (!targetClass) {
|
||||||
throw new Error(`Unregistered resource ${collection}`);
|
throw new Error(`Unregistered resource ${collection}`);
|
||||||
}
|
}
|
||||||
this.DS.add(autoupdate.changed[collection].map(model => new targetClass(model)), autoupdate.change_id);
|
return models.map(model => new targetClass(model));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a WebSocket request to the Server with the maxChangeId of the DataStore.
|
* Sends a WebSocket request to the Server with the maxChangeId of the DataStore.
|
||||||
* The server should return an autoupdate with all new data.
|
* The server should return an autoupdate with all new data.
|
||||||
*
|
|
||||||
* TODO: Wait for changeIds to be implemented on the server.
|
|
||||||
*/
|
*/
|
||||||
public requestChanges(): void {
|
public requestChanges(): void {
|
||||||
console.log('requesting changed objects');
|
console.log('requesting changed objects with DS max change id', this.DS.maxChangeId + 1);
|
||||||
// this.websocketService.send('changeIdRequest', this.DS.maxChangeId);
|
this.websocketService.send('getElements', { change_id: this.DS.maxChangeId + 1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { TestBed, inject } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { CacheService } from './cache.service';
|
|
||||||
|
|
||||||
describe('WebsocketService', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
providers: [CacheService]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', inject([CacheService], (service: CacheService) => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
}));
|
|
||||||
});
|
|
@ -1,151 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { LocalStorage } from '@ngx-pwa/local-storage';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Container objects for the setQueue.
|
|
||||||
*/
|
|
||||||
interface SetContainer {
|
|
||||||
key: string;
|
|
||||||
item: any;
|
|
||||||
callback: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Container objects for the removeQueue.
|
|
||||||
*/
|
|
||||||
interface RemoveContainer {
|
|
||||||
key: string;
|
|
||||||
callback: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an async API to an key-value store using ngx-pwa which is internally
|
|
||||||
* using IndexedDB or localStorage as a fallback.
|
|
||||||
*/
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class CacheService {
|
|
||||||
/**
|
|
||||||
* The queue of waiting set requests. Just one request (with the same key, which is
|
|
||||||
* an often case) at the time can be handeled. The SetContainer encapsulates the key,
|
|
||||||
* item and callback.
|
|
||||||
*/
|
|
||||||
private setQueue: SetContainer[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The queue of waiting remove requests. Same reason for the queue es the @name _setQueue.
|
|
||||||
*/
|
|
||||||
private removeQueue: RemoveContainer[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor to create the CacheService. Needs the localStorage service.
|
|
||||||
* @param localStorage
|
|
||||||
*/
|
|
||||||
public constructor(private localStorage: LocalStorage) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the item into the store asynchronously.
|
|
||||||
* @param key
|
|
||||||
* @param item
|
|
||||||
* @param callback An optional callback that is called on success
|
|
||||||
*/
|
|
||||||
public set(key: string, item: any, callback?: (value: boolean) => void): void {
|
|
||||||
if (!callback) {
|
|
||||||
callback = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put the set request into the queue
|
|
||||||
const queueObj: SetContainer = {
|
|
||||||
key: key,
|
|
||||||
item: item,
|
|
||||||
callback: callback
|
|
||||||
};
|
|
||||||
this.setQueue.unshift(queueObj);
|
|
||||||
|
|
||||||
// If this is the only object, put it into the cache.
|
|
||||||
if (this.setQueue.length === 1) {
|
|
||||||
this.localStorage.setItem(key, item).subscribe(this._setCallback.bind(this), this._error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets called, if a set of the first item in the queue was successful.
|
|
||||||
* @param value success
|
|
||||||
*/
|
|
||||||
private _setCallback(success: boolean): void {
|
|
||||||
// Call the callback and remove the object from the queue
|
|
||||||
this.setQueue[0].callback(success);
|
|
||||||
this.setQueue.pop();
|
|
||||||
// If there are objects left, insert the first one into the cache.
|
|
||||||
if (this.setQueue.length > 0) {
|
|
||||||
const queueObj = this.setQueue[0];
|
|
||||||
this.localStorage.setItem(queueObj.key, queueObj.item).subscribe(this._setCallback.bind(this), this._error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get a value from the store. You need to subscribe to the request to retrieve the value.
|
|
||||||
* @param key The key to get the value from
|
|
||||||
*/
|
|
||||||
public get<T>(key: string): Observable<T> {
|
|
||||||
return this.localStorage.getItem<T>(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the key from the store.
|
|
||||||
* @param key The key to remove the value from
|
|
||||||
* @param callback An optional callback that is called on success
|
|
||||||
*/
|
|
||||||
public remove(key: string, callback?: (value: boolean) => void): void {
|
|
||||||
if (!callback) {
|
|
||||||
callback = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put the remove request into the queue
|
|
||||||
const queueObj: RemoveContainer = {
|
|
||||||
key: key,
|
|
||||||
callback: callback
|
|
||||||
};
|
|
||||||
this.removeQueue.unshift(queueObj);
|
|
||||||
|
|
||||||
// If this is the only object, remove it from the cache.
|
|
||||||
if (this.removeQueue.length === 1) {
|
|
||||||
this.localStorage.removeItem(key).subscribe(this._removeCallback.bind(this), this._error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets called, if a remove of the first item in the queue was successfull.
|
|
||||||
* @param value success
|
|
||||||
*/
|
|
||||||
private _removeCallback(success: boolean): void {
|
|
||||||
// Call the callback and remove the object from the queue
|
|
||||||
this.removeQueue[0].callback(success);
|
|
||||||
this.removeQueue.pop();
|
|
||||||
// If there are objects left, remove the first one from the cache.
|
|
||||||
if (this.removeQueue.length > 0) {
|
|
||||||
const queueObj = this.removeQueue[0];
|
|
||||||
this.localStorage.removeItem(queueObj.key).subscribe(this._removeCallback.bind(this), this._error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the whole cache
|
|
||||||
* @param callback An optional callback that is called on success
|
|
||||||
*/
|
|
||||||
public clear(callback?: (value: boolean) => void): void {
|
|
||||||
if (!callback) {
|
|
||||||
callback = () => {};
|
|
||||||
}
|
|
||||||
this.localStorage.clear().subscribe(callback, this._error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* First error catching function.
|
|
||||||
*/
|
|
||||||
private _error(): void {
|
|
||||||
console.error('caching error', arguments);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
|
||||||
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
|
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
|
||||||
import { CacheService } from './cache.service';
|
import { StorageService } from './storage.service';
|
||||||
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
import { CollectionStringModelMapperService } from './collectionStringModelMapper.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,7 +64,7 @@ export class DataStoreService {
|
|||||||
* all cases equal!
|
* all cases equal!
|
||||||
*/
|
*/
|
||||||
private modelStore: ModelStorage = {};
|
private modelStore: ModelStorage = {};
|
||||||
private JsonStore: JsonStorage = {};
|
private jsonStore: JsonStorage = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable subject for changed models in the datastore.
|
* Observable subject for changed models in the datastore.
|
||||||
@ -107,42 +107,51 @@ export class DataStoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty constructor for dataStore
|
* @param storageService use StorageService to preserve the DataStore.
|
||||||
* @param cacheService use CacheService to cache the DataStore.
|
* @param modelMapper
|
||||||
*/
|
*/
|
||||||
public constructor(private cacheService: CacheService, private modelMapper: CollectionStringModelMapperService) {}
|
public constructor(
|
||||||
|
private storageService: StorageService,
|
||||||
|
private modelMapper: CollectionStringModelMapperService
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the DataStore from cache and instantiate all models out of the serialized version.
|
* Gets the DataStore from cache and instantiate all models out of the serialized version.
|
||||||
|
* @returns The max change id.
|
||||||
*/
|
*/
|
||||||
public initFromCache(): Promise<number> {
|
public async initFromStorage(): 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 => {
|
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
|
||||||
this.cacheService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS').subscribe((store: JsonStorage) => {
|
if (store) {
|
||||||
if (store != null) {
|
console.log('init from storage:', store);
|
||||||
// There is a store. Deserialize it
|
// There is a store. Deserialize it
|
||||||
this.JsonStore = store;
|
this.jsonStore = store;
|
||||||
this.modelStore = this.deserializeJsonStore(this.JsonStore);
|
this.modelStore = this.deserializeJsonStore(this.jsonStore);
|
||||||
// Get the maxChangeId from the cache
|
// Get the maxChangeId from the cache
|
||||||
this.cacheService
|
let maxChangeId = await this.storageService.get<number>(DataStoreService.cachePrefix + 'maxChangeId');
|
||||||
.get<number>(DataStoreService.cachePrefix + 'maxChangeId')
|
if (!maxChangeId) {
|
||||||
.subscribe((maxChangeId: number) => {
|
|
||||||
if (maxChangeId == null) {
|
|
||||||
maxChangeId = 0;
|
maxChangeId = 0;
|
||||||
}
|
}
|
||||||
this._maxChangeId = maxChangeId;
|
this._maxChangeId = maxChangeId;
|
||||||
resolve(maxChangeId);
|
|
||||||
|
// update observers
|
||||||
|
Object.keys(this.modelStore).forEach(collection => {
|
||||||
|
Object.keys(this.modelStore[collection]).forEach(id => {
|
||||||
|
this.changedSubject.next(this.modelStore[collection][id]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No store here, so get all data from the server.
|
this.jsonStore = {};
|
||||||
resolve(0);
|
this.modelStore = {};
|
||||||
|
this._maxChangeId = 0;
|
||||||
}
|
}
|
||||||
});
|
return this.maxChangeId;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deserialze the given serializedStorage and returns a Storage.
|
* Deserialze the given serializedStorage and returns a Storage.
|
||||||
|
* @param serializedStore The store to deserialize
|
||||||
|
* @returns The serialized storage
|
||||||
*/
|
*/
|
||||||
private deserializeJsonStore(serializedStore: JsonStorage): ModelStorage {
|
private deserializeJsonStore(serializedStore: JsonStorage): ModelStorage {
|
||||||
const storage: ModelStorage = {};
|
const storage: ModelStorage = {};
|
||||||
@ -161,17 +170,21 @@ export class DataStoreService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the complete DataStore and Cache.
|
* Clears the complete DataStore and Cache.
|
||||||
* @param callback
|
|
||||||
*/
|
*/
|
||||||
public clear(callback?: (value: boolean) => void): void {
|
public async clear(): Promise<void> {
|
||||||
|
console.log('DS clear');
|
||||||
this.modelStore = {};
|
this.modelStore = {};
|
||||||
this.JsonStore = {};
|
this.jsonStore = {};
|
||||||
this._maxChangeId = 0;
|
this._maxChangeId = 0;
|
||||||
this.cacheService.remove(DataStoreService.cachePrefix + 'DS', () => {
|
await this.storageService.remove(DataStoreService.cachePrefix + 'DS');
|
||||||
this.cacheService.remove(DataStoreService.cachePrefix + 'maxChangeId', callback);
|
await this.storageService.remove(DataStoreService.cachePrefix + 'maxChangeId');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the collection _string_ based on the model given. If a string is given, it's just returned.
|
||||||
|
* @param collectionType Either a Model constructor or a string.
|
||||||
|
* @returns the collection string
|
||||||
|
*/
|
||||||
private getCollectionString<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string): string {
|
private getCollectionString<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string): string {
|
||||||
if (typeof collectionType === 'string') {
|
if (typeof collectionType === 'string') {
|
||||||
return collectionType;
|
return collectionType;
|
||||||
@ -262,62 +275,78 @@ export class DataStoreService {
|
|||||||
* Add one or multiple models to dataStore.
|
* Add one or multiple models to dataStore.
|
||||||
*
|
*
|
||||||
* @param models BaseModels to add to the store
|
* @param models BaseModels to add to the store
|
||||||
* @param changeId The changeId of this update
|
* @param changeId The changeId of this update. If given, the storage will be flushed to the
|
||||||
* @example this.DS.add([new User(1)], changeId)
|
* cache. Else one can call {@method flushToStorage} to do this manually.
|
||||||
* @example this.DS.add([new User(2), new User(3)], changeId)
|
* @example this.DS.add([new User(1)])
|
||||||
|
* @example this.DS.add([new User(2), new User(3)])
|
||||||
* @example this.DS.add(arrayWithUsers, changeId)
|
* @example this.DS.add(arrayWithUsers, changeId)
|
||||||
*/
|
*/
|
||||||
public add(models: BaseModel[], changeId: number): void {
|
public async add(models: BaseModel[], changeId?: number): Promise<void> {
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
const collectionString = model.collectionString;
|
const collection = model.collectionString;
|
||||||
if (this.modelStore[collectionString] === undefined) {
|
if (this.modelStore[collection] === undefined) {
|
||||||
this.modelStore[collectionString] = {};
|
this.modelStore[collection] = {};
|
||||||
}
|
}
|
||||||
this.modelStore[collectionString][model.id] = model;
|
this.modelStore[collection][model.id] = model;
|
||||||
|
|
||||||
if (this.JsonStore[collectionString] === undefined) {
|
if (this.jsonStore[collection] === undefined) {
|
||||||
this.JsonStore[collectionString] = {};
|
this.jsonStore[collection] = {};
|
||||||
}
|
}
|
||||||
this.JsonStore[collectionString][model.id] = JSON.stringify(model);
|
this.jsonStore[collection][model.id] = JSON.stringify(model);
|
||||||
this.changedSubject.next(model);
|
this.changedSubject.next(model);
|
||||||
});
|
});
|
||||||
this.storeToCache(changeId);
|
if (changeId) {
|
||||||
|
await this.flushToStorage(changeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* removes one or multiple models from dataStore.
|
* removes one or multiple models from dataStore.
|
||||||
*
|
*
|
||||||
* @param Type The desired BaseModel type to be read from the datastore
|
* @param collectionString The desired BaseModel type to be removed from the datastore
|
||||||
* @param ids A list of IDs of BaseModels to remove from the datastore
|
* @param ids A list of IDs of BaseModels to remove from the datastore
|
||||||
* @param changeId The changeId of this update
|
* @param changeId The changeId of this update. If given, the storage will be flushed to the
|
||||||
* @example this.DS.remove('users/user', [myUser.id, 3, 4], 38213)
|
* cache. Else one can call {@method flushToStorage} to do this manually.
|
||||||
|
* @example this.DS.remove('users/user', [myUser.id, 3, 4])
|
||||||
*/
|
*/
|
||||||
public remove(collectionString: string, ids: number[], changeId: number): void {
|
public async remove(collectionString: string, ids: number[], changeId?: number): Promise<void> {
|
||||||
ids.forEach(id => {
|
ids.forEach(id => {
|
||||||
if (this.modelStore[collectionString]) {
|
if (this.modelStore[collectionString]) {
|
||||||
delete this.modelStore[collectionString][id];
|
delete this.modelStore[collectionString][id];
|
||||||
}
|
}
|
||||||
if (this.JsonStore[collectionString]) {
|
if (this.jsonStore[collectionString]) {
|
||||||
delete this.JsonStore[collectionString][id];
|
delete this.jsonStore[collectionString][id];
|
||||||
}
|
}
|
||||||
this.deletedSubject.next({
|
this.deletedSubject.next({
|
||||||
collection: collectionString,
|
collection: collectionString,
|
||||||
id: id
|
id: id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.storeToCache(changeId);
|
if (changeId) {
|
||||||
|
await this.flushToStorage(changeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the DataStore and set the given models as the new content.
|
||||||
|
* @param models A list of models to set the DataStore to.
|
||||||
|
* @param newMaxChangeId Optional. If given, the max change id will be updated.
|
||||||
|
*/
|
||||||
|
public async set(models: BaseModel[], newMaxChangeId?: number): Promise<void> {
|
||||||
|
this.modelStore = {};
|
||||||
|
this.jsonStore = {};
|
||||||
|
await this.add(models, newMaxChangeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the cache by inserting the serialized DataStore. Also changes the chageId, if it's larger
|
* Updates the cache by inserting the serialized DataStore. Also changes the chageId, if it's larger
|
||||||
* @param changeId The changeId from the update. If it's the highest change id seen, it will be set into the cache.
|
* @param changeId The changeId from the update. If it's the highest change id seen, it will be set into the cache.
|
||||||
*/
|
*/
|
||||||
private storeToCache(changeId: number): void {
|
public async flushToStorage(changeId: number): Promise<void> {
|
||||||
this.cacheService.set(DataStoreService.cachePrefix + 'DS', this.JsonStore);
|
console.log('flush to storage');
|
||||||
if (changeId > this._maxChangeId) {
|
|
||||||
this._maxChangeId = changeId;
|
this._maxChangeId = changeId;
|
||||||
this.cacheService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
|
||||||
}
|
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
17
client/src/app/core/services/offline.service.spec.ts
Normal file
17
client/src/app/core/services/offline.service.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { OfflineService } from './offline.service';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('OfflineService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
providers: [OfflineService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([OfflineService], (service: OfflineService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
68
client/src/app/core/services/offline.service.ts
Normal file
68
client/src/app/core/services/offline.service.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||||
|
import { DataStoreService } from './data-store.service';
|
||||||
|
import { WhoAmIResponse } from './operator.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service handles everything connected with being offline.
|
||||||
|
*
|
||||||
|
* TODO: This is just a stub. Needs to be done in the future; Maybe we cancel this whole concept
|
||||||
|
* of this service. We'll see whats happens here..
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class OfflineService extends OpenSlidesComponent {
|
||||||
|
private _offline = false;
|
||||||
|
|
||||||
|
public get offline(): boolean {
|
||||||
|
return this._offline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||||
|
* @param DS
|
||||||
|
*/
|
||||||
|
public constructor(private DS: DataStoreService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the offline flag. Restores the DataStoreService to the last known configuration.
|
||||||
|
*/
|
||||||
|
public async goOfflineBecauseFailedWhoAmI(): Promise<void> {
|
||||||
|
this._offline = true;
|
||||||
|
console.log('offline because whoami failed.');
|
||||||
|
|
||||||
|
// TODO: Init the DS from cache.
|
||||||
|
await this.DS.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Should be somehow connected to the websocket service.
|
||||||
|
*/
|
||||||
|
public goOfflineBecauseConnectionLost(): void {
|
||||||
|
this._offline = true;
|
||||||
|
console.log('offline because connection lost.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Should be somehow connected to the websocket service.
|
||||||
|
*/
|
||||||
|
public goOnline(): void {
|
||||||
|
this._offline = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last cached WhoAmI response.
|
||||||
|
*/
|
||||||
|
public getLastWhoAmI(): WhoAmIResponse {
|
||||||
|
// TODO: use a cached WhoAmI response.
|
||||||
|
return {
|
||||||
|
user_id: null,
|
||||||
|
guest_enabled: false,
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { Router } from '@angular/router';
|
|||||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||||
import { WebsocketService } from './websocket.service';
|
import { WebsocketService } from './websocket.service';
|
||||||
import { OperatorService } from './operator.service';
|
import { OperatorService } from './operator.service';
|
||||||
import { CacheService } from './cache.service';
|
import { StorageService } from './storage.service';
|
||||||
import { AutoupdateService } from './autoupdate.service';
|
import { AutoupdateService } from './autoupdate.service';
|
||||||
import { DataStoreService } from './data-store.service';
|
import { DataStoreService } from './data-store.service';
|
||||||
|
|
||||||
@ -21,15 +21,16 @@ export class OpenSlidesService extends OpenSlidesComponent {
|
|||||||
public redirectUrl: string;
|
public redirectUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor to create the NotifyService. Registers itself to the WebsocketService.
|
* Constructor to create the OpenSlidesService. Registers itself to the WebsocketService.
|
||||||
* @param cacheService
|
* @param storageService
|
||||||
* @param operator
|
* @param operator
|
||||||
* @param websocketService
|
* @param websocketService
|
||||||
* @param router
|
* @param router
|
||||||
* @param autoupdateService
|
* @param autoupdateService
|
||||||
|
* @param DS
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private cacheService: CacheService,
|
private storageService: StorageService,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -51,54 +52,51 @@ export class OpenSlidesService extends OpenSlidesComponent {
|
|||||||
* the bootup-sequence: Do a whoami request and if it was successful, do
|
* the bootup-sequence: Do a whoami request and if it was successful, do
|
||||||
* {@method afterLoginBootup}. If not, redirect the user to the login page.
|
* {@method afterLoginBootup}. If not, redirect the user to the login page.
|
||||||
*/
|
*/
|
||||||
public bootup(): void {
|
public async bootup(): Promise<void> {
|
||||||
// start autoupdate if the user is logged in:
|
// start autoupdate if the user is logged in:
|
||||||
this.operator.whoAmI().subscribe(resp => {
|
const response = await this.operator.whoAmI();
|
||||||
this.operator.guestsEnabled = resp.guest_enabled;
|
this.operator.guestsEnabled = response.guest_enabled;
|
||||||
if (!resp.user && !resp.guest_enabled) {
|
if (!response.user && !response.guest_enabled) {
|
||||||
this.redirectUrl = location.pathname;
|
this.redirectUrl = location.pathname;
|
||||||
// Goto login, if the user isn't login and guests are not allowed
|
// Goto login, if the user isn't login and guests are not allowed
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
} else {
|
} else {
|
||||||
this.afterLoginBootup(resp.user_id);
|
await this.afterLoginBootup(response.user_id);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the login bootup-sequence: Check (and maybe clear) the cache und setup the DataStore
|
* the login bootup-sequence: Check (and maybe clear) the cache und setup the DataStore
|
||||||
* and websocket.
|
* and websocket. This "login" also may be the "login" of an anonymous when he is using
|
||||||
|
* OpenSlides as a guest.
|
||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
public afterLoginBootup(userId: number): void {
|
public async afterLoginBootup(userId: number): Promise<void> {
|
||||||
// Else, check, which user was logged in last time
|
// Else, check, which user was logged in last time
|
||||||
this.cacheService.get<number>('lastUserLoggedIn').subscribe((id: number) => {
|
const lastUserId = await this.storageService.get<number>('lastUserLoggedIn');
|
||||||
// if the user id changed, reset the cache.
|
console.log('user transition:', lastUserId, '->', userId);
|
||||||
if (userId !== id) {
|
// if the user changed, reset the cache and save the new user.
|
||||||
this.DS.clear((value: boolean) => {
|
if (userId !== lastUserId) {
|
||||||
this.setupDataStoreAndWebSocket();
|
await this.DS.clear();
|
||||||
});
|
await this.storageService.set('lastUserLoggedIn', userId);
|
||||||
this.cacheService.set('lastUserLoggedIn', userId);
|
|
||||||
} else {
|
|
||||||
this.setupDataStoreAndWebSocket();
|
|
||||||
}
|
}
|
||||||
});
|
await this.setupDataStoreAndWebSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init DS from cache and after this start the websocket service.
|
* Init DS from cache and after this start the websocket service.
|
||||||
*/
|
*/
|
||||||
private setupDataStoreAndWebSocket(): void {
|
private async setupDataStoreAndWebSocket(): Promise<void> {
|
||||||
this.DS.initFromCache().then((changeId: number) => {
|
let changeId = await this.DS.initFromStorage();
|
||||||
this.websocketService.connect(
|
console.log('change ID on DS setup', changeId);
|
||||||
false,
|
if (changeId > 0) {
|
||||||
changeId
|
changeId += 1;
|
||||||
);
|
}
|
||||||
});
|
this.websocketService.connect({ changeId: changeId }); // Request changes after changeId.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SHuts down OpenSlides. The websocket is closed and the operator is not set.
|
* Shuts OpenSlides down. The websocket is closed and the operator is not set.
|
||||||
*/
|
*/
|
||||||
public shutdown(): void {
|
public shutdown(): void {
|
||||||
this.websocketService.close();
|
this.websocketService.close();
|
||||||
@ -108,32 +106,31 @@ export class OpenSlidesService extends OpenSlidesComponent {
|
|||||||
/**
|
/**
|
||||||
* Shutdown and bootup.
|
* Shutdown and bootup.
|
||||||
*/
|
*/
|
||||||
public reboot(): void {
|
public async reboot(): Promise<void> {
|
||||||
this.shutdown();
|
this.shutdown();
|
||||||
this.bootup();
|
await this.bootup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that the operator is the same as it was before a reconnect.
|
* Verify that the operator is the same as it was before. Should be alled on a reconnect.
|
||||||
*/
|
*/
|
||||||
private checkOperator(): void {
|
private async checkOperator(): Promise<void> {
|
||||||
this.operator.whoAmI().subscribe(resp => {
|
const response = await this.operator.whoAmI();
|
||||||
// User logged off.
|
// User logged off.
|
||||||
if (!resp.user && !resp.guest_enabled) {
|
if (!response.user && !response.guest_enabled) {
|
||||||
this.shutdown();
|
this.shutdown();
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
(this.operator.user && this.operator.user.id !== resp.user_id) ||
|
(this.operator.user && this.operator.user.id !== response.user_id) ||
|
||||||
(!this.operator.user && resp.user_id)
|
(!this.operator.user && response.user_id)
|
||||||
) {
|
) {
|
||||||
// user changed
|
// user changed
|
||||||
this.reboot();
|
await this.reboot();
|
||||||
} else {
|
} else {
|
||||||
// User is still the same, but check for missed autoupdates.
|
// User is still the same, but check for missed autoupdates.
|
||||||
this.autoupdateService.requestChanges();
|
this.autoupdateService.requestChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, BehaviorSubject } from 'rxjs';
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { tap, catchError } from 'rxjs/operators';
|
|
||||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||||
import { Group } from 'app/shared/models/users/group';
|
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';
|
||||||
import { DataStoreService } from './data-store.service';
|
import { DataStoreService } from './data-store.service';
|
||||||
|
import { OfflineService } from './offline.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permissions on the client are just strings. This makes clear, that
|
* Permissions on the client are just strings. This makes clear, that
|
||||||
@ -17,7 +17,7 @@ export type Permission = string;
|
|||||||
/**
|
/**
|
||||||
* Response format of the WHoAMI request.
|
* Response format of the WHoAMI request.
|
||||||
*/
|
*/
|
||||||
interface WhoAmIResponse {
|
export interface WhoAmIResponse {
|
||||||
user_id: number;
|
user_id: number;
|
||||||
guest_enabled: boolean;
|
guest_enabled: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
@ -77,9 +77,14 @@ export class OperatorService extends OpenSlidesComponent {
|
|||||||
private operatorSubject: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
private operatorSubject: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets up an observer for watching changes in the DS. If the operator user or groups are changed,
|
||||||
|
* the operator's permissions are updated.
|
||||||
|
*
|
||||||
* @param http HttpClient
|
* @param http HttpClient
|
||||||
|
* @param DS
|
||||||
|
* @param offlineService
|
||||||
*/
|
*/
|
||||||
public constructor(private http: HttpClient, private DS: DataStoreService) {
|
public constructor(private http: HttpClient, private DS: DataStoreService, private offlineService: OfflineService) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.DS.changeObservable.subscribe(newModel => {
|
this.DS.changeObservable.subscribe(newModel => {
|
||||||
@ -101,16 +106,21 @@ export class OperatorService extends OpenSlidesComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `/apps/users/whoami` to find out the real operator.
|
* Calls `/apps/users/whoami` to find out the real operator.
|
||||||
|
* @returns The response of the WhoAmI request.
|
||||||
*/
|
*/
|
||||||
public whoAmI(): Observable<WhoAmIResponse> {
|
public async whoAmI(): Promise<WhoAmIResponse> {
|
||||||
return this.http.get<WhoAmIResponse>(environment.urlPrefix + '/users/whoami/').pipe(
|
try {
|
||||||
tap((response: WhoAmIResponse) => {
|
const response = await this.http.get<WhoAmIResponse>(environment.urlPrefix + '/users/whoami/').toPromise();
|
||||||
if (response && response.user_id) {
|
if (response && response.user) {
|
||||||
this.user = new User(response.user);
|
this.user = new User(response.user);
|
||||||
}
|
}
|
||||||
}),
|
return response;
|
||||||
catchError(this.handleError())
|
} catch (e) {
|
||||||
) as Observable<WhoAmIResponse>;
|
// TODO: Implement the offline service. Currently a guest-whoami response is returned and
|
||||||
|
// the DS cleared.
|
||||||
|
this.offlineService.goOfflineBecauseFailedWhoAmI();
|
||||||
|
return this.offlineService.getLastWhoAmI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
15
client/src/app/core/services/storage.service.spec.ts
Normal file
15
client/src/app/core/services/storage.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
|
describe('StorageService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [StorageService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([StorageService], (service: StorageService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
61
client/src/app/core/services/storage.service.ts
Normal file
61
client/src/app/core/services/storage.service.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { LocalStorage } from '@ngx-pwa/local-storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an async API to an key-value store using ngx-pwa which is internally
|
||||||
|
* using IndexedDB or localStorage as a fallback.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class StorageService {
|
||||||
|
/**
|
||||||
|
* Constructor to create the StorageService. Needs the localStorage service.
|
||||||
|
* @param localStorage
|
||||||
|
*/
|
||||||
|
public constructor(private localStorage: LocalStorage) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the item into the store asynchronously.
|
||||||
|
* @param key
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public async set(key: string, item: any): Promise<void> {
|
||||||
|
if (item === null || item === undefined) {
|
||||||
|
await this.remove(key); // You cannot do a setItem with null or undefined...
|
||||||
|
} else {
|
||||||
|
if (!(await this.localStorage.setItem(key, item).toPromise())) {
|
||||||
|
throw new Error('Could not set the item.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a value from the store. You need to subscribe to the request to retrieve the value.
|
||||||
|
* @param key The key to get the value from
|
||||||
|
* @returns The requested value to the key
|
||||||
|
*/
|
||||||
|
public async get<T>(key: string): Promise<T> {
|
||||||
|
return await this.localStorage.getItem<T>(key).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the key from the store.
|
||||||
|
* @param key The key to remove the value from
|
||||||
|
*/
|
||||||
|
public async remove(key: string): Promise<void> {
|
||||||
|
if (!(await this.localStorage.removeItem(key).toPromise())) {
|
||||||
|
throw new Error('Could not delete the item.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the whole cache
|
||||||
|
*/
|
||||||
|
public async clear(): Promise<void> {
|
||||||
|
console.log('clear storage');
|
||||||
|
if (!(await this.localStorage.clear().toPromise())) {
|
||||||
|
throw new Error('Could not clear the storage.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -69,9 +69,17 @@ export class WebsocketService {
|
|||||||
*/
|
*/
|
||||||
private subjects: { [type: string]: Subject<any> } = {};
|
private subjects: { [type: string]: Subject<any> } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves, if the service is in retry mode to get a connection to a previos connection lost.
|
||||||
|
*/
|
||||||
|
private retry = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor that handles the router
|
* Constructor that handles the router
|
||||||
* @param router the URL Router
|
* @param router the URL Router
|
||||||
|
* @param matSnackBar
|
||||||
|
* @param zone
|
||||||
|
* @param translate
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -85,34 +93,46 @@ export class WebsocketService {
|
|||||||
*
|
*
|
||||||
* Uses NgZone to let all callbacks run in the angular context.
|
* Uses NgZone to let all callbacks run in the angular context.
|
||||||
*/
|
*/
|
||||||
public connect(retry: boolean = false, changeId?: number): void {
|
public connect(
|
||||||
|
options: {
|
||||||
|
changeId?: number;
|
||||||
|
enableAutoupdates?: boolean;
|
||||||
|
} = {}
|
||||||
|
): void {
|
||||||
if (this.websocket) {
|
if (this.websocket) {
|
||||||
return;
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set defaults
|
||||||
|
options = Object.assign(options, {
|
||||||
|
enableAutoupdates: true
|
||||||
|
});
|
||||||
|
|
||||||
const queryParams: QueryParams = {
|
const queryParams: QueryParams = {
|
||||||
'change_id': 0,
|
autoupdate: options.enableAutoupdates
|
||||||
'autoupdate': true,
|
|
||||||
};
|
};
|
||||||
// comment-in if changes IDs are supported on server side.
|
|
||||||
/*if (changeId !== undefined) {
|
if (options.changeId !== undefined) {
|
||||||
queryParams.changeId = changeId.toString();
|
queryParams.change_id = options.changeId;
|
||||||
}*/
|
}
|
||||||
|
|
||||||
// Create the websocket
|
// 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);
|
||||||
|
console.log('connect to', socketProtocol + socketServer + socketPath);
|
||||||
this.websocket = new WebSocket(socketProtocol + socketServer + socketPath);
|
this.websocket = new WebSocket(socketProtocol + socketServer + socketPath);
|
||||||
|
|
||||||
// connection established. If this connect attept was a retry,
|
// connection established. If this connect attept was a retry,
|
||||||
// The error notice will be removed and the reconnectSubject is published.
|
// The error notice will be removed and the reconnectSubject is published.
|
||||||
this.websocket.onopen = (event: Event) => {
|
this.websocket.onopen = (event: Event) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (retry) {
|
if (this.retry) {
|
||||||
if (this.connectionErrorNotice) {
|
if (this.connectionErrorNotice) {
|
||||||
this.connectionErrorNotice.dismiss();
|
this.connectionErrorNotice.dismiss();
|
||||||
this.connectionErrorNotice = null;
|
this.connectionErrorNotice = null;
|
||||||
}
|
}
|
||||||
|
this.retry = false;
|
||||||
this._reconnectEvent.emit();
|
this._reconnectEvent.emit();
|
||||||
}
|
}
|
||||||
this._connectEvent.emit();
|
this._connectEvent.emit();
|
||||||
@ -151,7 +171,8 @@ export class WebsocketService {
|
|||||||
// A random retry timeout between 2000 and 5000 ms.
|
// A random retry timeout between 2000 and 5000 ms.
|
||||||
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.connect((retry = true));
|
this.retry = true;
|
||||||
|
this.connect({ enableAutoupdates: true });
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -222,7 +243,9 @@ export class WebsocketService {
|
|||||||
|
|
||||||
const keys: string[] = Object.keys(queryParams);
|
const keys: string[] = Object.keys(queryParams);
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) {
|
||||||
path += '?' + keys
|
path +=
|
||||||
|
'?' +
|
||||||
|
keys
|
||||||
.map(key => {
|
.map(key => {
|
||||||
return key + '=' + queryParams[key].toString();
|
return key + '=' + queryParams[key].toString();
|
||||||
})
|
})
|
||||||
|
@ -127,26 +127,27 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
|||||||
*
|
*
|
||||||
* Send username and password to the {@link AuthService}
|
* Send username and password to the {@link AuthService}
|
||||||
*/
|
*/
|
||||||
public formLogin(): void {
|
public async formLogin(): Promise<void> {
|
||||||
this.loginErrorMsg = '';
|
this.loginErrorMsg = '';
|
||||||
this.inProcess = true;
|
this.inProcess = true;
|
||||||
this.authService.login(this.loginForm.value.username, this.loginForm.value.password).subscribe(res => {
|
try {
|
||||||
|
const res = await this.authService.login(this.loginForm.value.username, this.loginForm.value.password);
|
||||||
this.inProcess = false;
|
this.inProcess = false;
|
||||||
|
|
||||||
if (res instanceof HttpErrorResponse) {
|
|
||||||
this.loginForm.setErrors({
|
|
||||||
notFound: true
|
|
||||||
});
|
|
||||||
this.loginErrorMsg = res.error.detail;
|
|
||||||
} else {
|
|
||||||
this.OpenSlides.afterLoginBootup(res.user_id);
|
this.OpenSlides.afterLoginBootup(res.user_id);
|
||||||
let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/';
|
let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/';
|
||||||
if (redirect.includes('login')) {
|
if (redirect.includes('login')) {
|
||||||
redirect = '/';
|
redirect = '/';
|
||||||
}
|
}
|
||||||
this.router.navigate([redirect]);
|
this.router.navigate([redirect]);
|
||||||
}
|
} catch (e) {
|
||||||
|
if (e instanceof HttpErrorResponse) {
|
||||||
|
this.loginForm.setErrors({
|
||||||
|
notFound: true
|
||||||
});
|
});
|
||||||
|
this.loginErrorMsg = e.error.detail;
|
||||||
|
this.inProcess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user