@openremote/core 1.0.2 → 1.2.0-snapshot.20240512154942
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -8
- package/lib/asset-mixin.d.ts +46 -0
- package/lib/asset-mixin.js +1 -0
- package/lib/asset-mixin.js.map +1 -0
- package/{dist → lib}/console.d.ts +21 -12
- package/lib/console.js +1 -0
- package/lib/console.js.map +1 -0
- package/lib/defaults.d.ts +15 -0
- package/lib/defaults.js +1 -0
- package/lib/defaults.js.map +1 -0
- package/{dist → lib}/event.d.ts +30 -16
- package/lib/event.js +1 -0
- package/lib/event.js.map +1 -0
- package/lib/index.d.ts +150 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/lib/util.d.ts +92 -0
- package/lib/util.js +1 -0
- package/lib/util.js.map +1 -0
- package/package.json +31 -14
- package/dist/asset-mixin.d.ts +0 -25
- package/dist/asset-mixin.js +0 -115
- package/dist/asset-mixin.js.map +0 -1
- package/dist/console.js +0 -446
- package/dist/console.js.map +0 -1
- package/dist/event.js +0 -454
- package/dist/event.js.map +0 -1
- package/dist/index.d.ts +0 -137
- package/dist/index.js +0 -683
- package/dist/index.js.map +0 -1
- package/dist/util.d.ts +0 -17
- package/dist/util.js +0 -72
- package/dist/util.js.map +0 -1
- package/src/asset-mixin.ts +0 -132
- package/src/console.d.ts +0 -56
- package/src/console.js +0 -451
- package/src/console.js.map +0 -1
- package/src/console.ts +0 -530
- package/src/event.d.ts +0 -75
- package/src/event.js +0 -410
- package/src/event.js.map +0 -1
- package/src/event.ts +0 -584
- package/src/index.d.ts +0 -110
- package/src/index.js +0 -525
- package/src/index.js.map +0 -1
- package/src/index.ts +0 -803
- package/src/util.d.ts +0 -15
- package/src/util.js +0 -46
- package/src/util.js.map +0 -1
- package/src/util.ts +0 -94
- package/tsconfig.json +0 -14
- package/tsconfig.tsbuildinfo +0 -9788
package/src/index.ts
DELETED
|
@@ -1,803 +0,0 @@
|
|
|
1
|
-
import "url-search-params-polyfill";
|
|
2
|
-
import {Console} from "./console";
|
|
3
|
-
import rest from "@openremote/rest";
|
|
4
|
-
import {IconSets} from "@openremote/or-icon";
|
|
5
|
-
import {AxiosRequestConfig} from "axios";
|
|
6
|
-
import {EventProvider, EventProviderFactory, EventProviderStatus, WebSocketEventProvider} from "./event";
|
|
7
|
-
import i18next from "i18next";
|
|
8
|
-
import i18nextXhr from "i18next-xhr-backend";
|
|
9
|
-
import {AssetDescriptor, AttributeDescriptor, AttributeValueDescriptor, MetaItemDescriptor, Asset} from "@openremote/model/dist";
|
|
10
|
-
|
|
11
|
-
export enum ORError {
|
|
12
|
-
NONE = "NONE",
|
|
13
|
-
MANAGER_FAILED_TO_LOAD = "MANAGER_FAILED_TO_LOAD",
|
|
14
|
-
KEYCLOAK_FAILED_TO_LOAD = "KEYCLOAK_FAILED_TO_LOAD",
|
|
15
|
-
AUTH_TYPE_UNSUPPORTED = "AUTH_TYPE_UNSUPPORTED",
|
|
16
|
-
CONSOLE_ERROR = "CONSOLE_INIT_ERROR",
|
|
17
|
-
EVENTS_CONNECTION_ERROR = "EVENTS_CONNECTION_ERROR"
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export enum Auth {
|
|
21
|
-
KEYCLOAK = "KEYCLOAK",
|
|
22
|
-
BASIC = "BASIC",
|
|
23
|
-
NONE = "NONE"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export enum OREvent {
|
|
27
|
-
ERROR = "ERROR",
|
|
28
|
-
READY = "READY",
|
|
29
|
-
CONSOLE_INIT = "CONSOLE_INIT",
|
|
30
|
-
CONSOLE_READY = "CONSOLE_READY",
|
|
31
|
-
EVENTS_CONNECTED = "EVENTS_CONNECTED",
|
|
32
|
-
EVENTS_CONNECTING = "EVENTS_CONNECTING",
|
|
33
|
-
EVENTS_DISCONNECTED = "EVENTS_DISCONNECTED",
|
|
34
|
-
TRANSLATE_INIT = "TRANSLATE_INIT",
|
|
35
|
-
TRANSLATE_LANGUAGE_CHANGED = "TRANSLATE_LANGUAGE_CHANGED"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export enum EventProviderType {
|
|
39
|
-
WEBSOCKET = "WEBSOCKET",
|
|
40
|
-
POLLING = "POLLING"
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface Credentials {
|
|
44
|
-
username: string;
|
|
45
|
-
password: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface LoginOptions {
|
|
49
|
-
redirectUrl?: string;
|
|
50
|
-
credentials?: Credentials;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface ManagerConfig {
|
|
54
|
-
managerUrl: string;
|
|
55
|
-
keycloakUrl?: string;
|
|
56
|
-
appVersion?: string;
|
|
57
|
-
auth?: Auth;
|
|
58
|
-
realm: string;
|
|
59
|
-
autoLogin?: boolean;
|
|
60
|
-
credentials?: Credentials;
|
|
61
|
-
consoleAutoEnable?: boolean;
|
|
62
|
-
eventProviderType?: EventProviderType;
|
|
63
|
-
pollingIntervalMillis?: number;
|
|
64
|
-
loadIcons?: boolean;
|
|
65
|
-
loadDescriptors?: boolean;
|
|
66
|
-
loadTranslations?: string[];
|
|
67
|
-
translationsLoadPath?: string;
|
|
68
|
-
configureTranslationsOptions?: (i18next: i18next.InitOptions) => void;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class AssetModelUtil {
|
|
72
|
-
|
|
73
|
-
public static _assetDescriptors: AssetDescriptor[] = [];
|
|
74
|
-
public static _attributeDescriptors: AttributeDescriptor[] = [];
|
|
75
|
-
public static _attributeValueDescriptors: AttributeValueDescriptor[] = [];
|
|
76
|
-
public static _metaItemDescriptors: MetaItemDescriptor[] = [];
|
|
77
|
-
|
|
78
|
-
public static getAssetDescriptors(): AssetDescriptor[] {
|
|
79
|
-
return this._assetDescriptors;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public static getAttributeDescriptors(): AttributeDescriptor[] {
|
|
83
|
-
return this._attributeDescriptors;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public static getAttributeValueDescriptors(): AttributeValueDescriptor[] {
|
|
87
|
-
return this._attributeValueDescriptors;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public static getMetaItemDescriptors(): MetaItemDescriptor[] {
|
|
91
|
-
return this._metaItemDescriptors;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
public static getAssetDescriptor(type?: string): AssetDescriptor | undefined {
|
|
95
|
-
if (!type) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return this._assetDescriptors.find((assetDescriptor) => {
|
|
100
|
-
return assetDescriptor.type === type;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
public static getAssetAttributeDescriptor(assetDescriptor?: AssetDescriptor, attributeName?: string): AttributeDescriptor | undefined {
|
|
105
|
-
if (!attributeName || !assetDescriptor || !assetDescriptor.attributeDescriptors) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return assetDescriptor.attributeDescriptors.find((attributeDescriptor) => attributeDescriptor.attributeName === attributeName);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
public static getAttributeDescriptor(attributeName?: string): AttributeDescriptor | undefined {
|
|
113
|
-
if (!attributeName) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return this._attributeDescriptors.find((attributeDescriptor) => {
|
|
118
|
-
return attributeDescriptor.attributeName === attributeName;
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
public static getAttributeValueDescriptor(name?: string): AttributeValueDescriptor | undefined {
|
|
123
|
-
if (!name) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return this._attributeValueDescriptors.find((attributeValueDescriptor) => {
|
|
128
|
-
return attributeValueDescriptor.name === name;
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
public static getMetaItemDescriptor(urn?: string): MetaItemDescriptor | undefined {
|
|
133
|
-
if (!urn) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return this._metaItemDescriptors.find((metaItemDescriptor) => {
|
|
138
|
-
return metaItemDescriptor.urn === urn;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public static attributeValueDescriptorsMatch(attributeValueDescriptor1: AttributeValueDescriptor, attributeValueDescriptor2: AttributeValueDescriptor) {
|
|
143
|
-
if (attributeValueDescriptor1 === attributeValueDescriptor2) {
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
if (!attributeValueDescriptor1 || !attributeValueDescriptor2) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
return attributeValueDescriptor1.name === attributeValueDescriptor2.name && attributeValueDescriptor1.valueType === attributeValueDescriptor2.valueType;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export type EventCallback = (event: OREvent) => any;
|
|
154
|
-
|
|
155
|
-
export class Manager implements EventProviderFactory {
|
|
156
|
-
|
|
157
|
-
get username() {
|
|
158
|
-
return this._username;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
get error() {
|
|
162
|
-
return this._error;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
get authenticated() {
|
|
166
|
-
return this._authenticated;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
get initialised() {
|
|
170
|
-
return this._config != null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
get ready() {
|
|
174
|
-
return this._ready;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
get config() {
|
|
178
|
-
return this._config;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
get roles() {
|
|
182
|
-
return this._roles;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
get managerVersion() {
|
|
186
|
-
return this._managerVersion;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
get isManagerAvailable() {
|
|
190
|
-
return this._managerVersion && this._managerVersion !== "";
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
get isError() {
|
|
194
|
-
return this._error != null && this._error !== ORError.NONE;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
get connectionStatus() {
|
|
198
|
-
return this._events && this._events.status;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
get console() {
|
|
202
|
-
return this._console;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
get events() {
|
|
206
|
-
return this._events;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
get language() {
|
|
210
|
-
return i18next.language;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
getEventProvider(): EventProvider | undefined {
|
|
214
|
-
return this.events;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
protected static normaliseConfig(config: ManagerConfig): ManagerConfig {
|
|
218
|
-
const normalisedConfig: ManagerConfig = Object.assign({}, config);
|
|
219
|
-
|
|
220
|
-
if (!normalisedConfig.managerUrl || normalisedConfig.managerUrl === "") {
|
|
221
|
-
// Assume manager is running on same host as this code
|
|
222
|
-
normalisedConfig.managerUrl = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port;
|
|
223
|
-
} else {
|
|
224
|
-
// Normalise by stripping any trailing slashes
|
|
225
|
-
normalisedConfig.managerUrl = normalisedConfig.managerUrl.replace(/\/+$/, "");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!normalisedConfig.realm || normalisedConfig.realm === "") {
|
|
229
|
-
// Assume master realm
|
|
230
|
-
normalisedConfig.realm = "master";
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (normalisedConfig.auth === Auth.KEYCLOAK) {
|
|
234
|
-
// Determine URL of keycloak server
|
|
235
|
-
if (!normalisedConfig.keycloakUrl || normalisedConfig.keycloakUrl === "") {
|
|
236
|
-
// Assume keycloak is running on same host as the manager
|
|
237
|
-
normalisedConfig.keycloakUrl = normalisedConfig.managerUrl + "/auth";
|
|
238
|
-
} else {
|
|
239
|
-
// Normalise by stripping any trailing slashes
|
|
240
|
-
normalisedConfig.keycloakUrl = normalisedConfig.keycloakUrl.replace(/\/+$/, "");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (normalisedConfig.consoleAutoEnable === undefined) {
|
|
245
|
-
normalisedConfig.consoleAutoEnable = true;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (!normalisedConfig.eventProviderType) {
|
|
249
|
-
normalisedConfig.eventProviderType = EventProviderType.WEBSOCKET;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!normalisedConfig.pollingIntervalMillis || normalisedConfig.pollingIntervalMillis < 5000) {
|
|
253
|
-
normalisedConfig.pollingIntervalMillis = 10000;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (normalisedConfig.loadIcons === undefined) {
|
|
257
|
-
normalisedConfig.loadIcons = true;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (normalisedConfig.loadTranslations === undefined) {
|
|
261
|
-
normalisedConfig.loadTranslations = ["or"];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (normalisedConfig.translationsLoadPath === undefined) {
|
|
265
|
-
normalisedConfig.translationsLoadPath = "locales/{{lng}}/{{ns}}.json";
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (normalisedConfig.loadDescriptors === undefined) {
|
|
269
|
-
normalisedConfig.loadDescriptors = true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return normalisedConfig;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private _error: ORError = ORError.NONE;
|
|
276
|
-
private _config!: ManagerConfig;
|
|
277
|
-
private _authenticated: boolean = false;
|
|
278
|
-
private _ready: boolean = false;
|
|
279
|
-
private _name: string = "";
|
|
280
|
-
private _username: string = "";
|
|
281
|
-
private _keycloak: any = null;
|
|
282
|
-
private _roles: string[] = [];
|
|
283
|
-
private _keycloakUpdateTokenInterval?: number = undefined;
|
|
284
|
-
private _managerVersion: string = "";
|
|
285
|
-
private _listeners: EventCallback[] = [];
|
|
286
|
-
private _console!: Console;
|
|
287
|
-
private _events?: EventProvider;
|
|
288
|
-
|
|
289
|
-
public isManagerSameOrigin(): boolean {
|
|
290
|
-
if (!this.initialised) {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const managerUrl = new URL(this._config.managerUrl);
|
|
295
|
-
const windowUrl = window.location;
|
|
296
|
-
return managerUrl.protocol === windowUrl.protocol
|
|
297
|
-
&& managerUrl.hostname === windowUrl.hostname
|
|
298
|
-
&& managerUrl.port === windowUrl.port;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
public addListener(callback: EventCallback) {
|
|
302
|
-
const index = this._listeners.indexOf(callback);
|
|
303
|
-
if (index < 0) {
|
|
304
|
-
this._listeners.push(callback);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
public removeListener(callback: EventCallback) {
|
|
309
|
-
const index = this._listeners.indexOf(callback);
|
|
310
|
-
if (index >= 0) {
|
|
311
|
-
this._listeners.splice(index, 1);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
public async init(config: ManagerConfig): Promise<boolean> {
|
|
316
|
-
if (this._config) {
|
|
317
|
-
console.log("Already initialised");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
this._config = Manager.normaliseConfig(config);
|
|
321
|
-
|
|
322
|
-
let success = await this.doAuthInit();
|
|
323
|
-
|
|
324
|
-
if (success) {
|
|
325
|
-
success = await this.doInit();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (success) {
|
|
329
|
-
success = this.doRestApiInit();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (success) {
|
|
333
|
-
success = await this.doConsoleInit();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (success) {
|
|
337
|
-
success = await this.doTranslateInit();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (success) {
|
|
341
|
-
success = await this.doDescriptorsInit();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// TODO: Reinstate this once websocket supports anonymous connections
|
|
345
|
-
// if (success) {
|
|
346
|
-
// success = await this.doEventsSubscriptionInit();
|
|
347
|
-
// }
|
|
348
|
-
|
|
349
|
-
if (success) {
|
|
350
|
-
this._ready = true;
|
|
351
|
-
this._emitEvent(OREvent.READY);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return success;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
protected async doInit(): Promise<boolean> {
|
|
358
|
-
// Check manager exists by calling the info endpoint
|
|
359
|
-
try {
|
|
360
|
-
const json = await new Promise<any>((resolve, reject) => {
|
|
361
|
-
const oReq = new XMLHttpRequest();
|
|
362
|
-
oReq.addEventListener("load", () => {
|
|
363
|
-
resolve(JSON.parse(oReq.responseText));
|
|
364
|
-
});
|
|
365
|
-
oReq.addEventListener("error", () => {
|
|
366
|
-
reject(new Error("Failed to contact the manager"));
|
|
367
|
-
});
|
|
368
|
-
oReq.open("GET", this._config.managerUrl + "/api/master/info");
|
|
369
|
-
oReq.send();
|
|
370
|
-
});
|
|
371
|
-
this._managerVersion = json && json.version ? json.version : "";
|
|
372
|
-
|
|
373
|
-
// Async load material design icons if requested
|
|
374
|
-
if (this._config.loadIcons) {
|
|
375
|
-
const mdiIconSet = await import(/* webpackChunkName: "mdi-icons" */ "@openremote/or-icon/dist/mdi-icons");
|
|
376
|
-
IconSets.addIconSet("mdi", mdiIconSet.default);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return true;
|
|
380
|
-
} catch (e) {
|
|
381
|
-
// TODO: Implement auto retry?
|
|
382
|
-
console.error("Failed to contact the manager", e);
|
|
383
|
-
this._setError(ORError.MANAGER_FAILED_TO_LOAD);
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
protected async doTranslateInit(): Promise<boolean> {
|
|
389
|
-
|
|
390
|
-
i18next.on("initialized", (options) => {
|
|
391
|
-
this._emitEvent(OREvent.TRANSLATE_INIT);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
i18next.on("languageChanged", () => {
|
|
395
|
-
this._emitEvent(OREvent.TRANSLATE_LANGUAGE_CHANGED);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const initOptions: i18next.InitOptions = {
|
|
399
|
-
lng: "en",
|
|
400
|
-
fallbackLng: "en",
|
|
401
|
-
defaultNS: "app",
|
|
402
|
-
fallbackNS: "or",
|
|
403
|
-
ns: this.config.loadTranslations,
|
|
404
|
-
backend: {
|
|
405
|
-
loadPath: (langs: string[], namespaces: string[]) => {
|
|
406
|
-
if (namespaces.length === 1 && namespaces[0] === "or") {
|
|
407
|
-
return this.config.managerUrl + "/shared/locales/{{lng}}/{{ns}}.json";
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (this.config.translationsLoadPath) {
|
|
411
|
-
return this.config.translationsLoadPath;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return "locales/{{lng}}/{{ns}}.json";
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
if (this.config.configureTranslationsOptions) {
|
|
420
|
-
this.config.configureTranslationsOptions(initOptions);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
await i18next.use(i18nextXhr).init(initOptions);
|
|
425
|
-
} catch (e) {
|
|
426
|
-
console.error(e);
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
protected async doDescriptorsInit(): Promise<boolean> {
|
|
434
|
-
if (!this.config.loadDescriptors) {
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
const assetDescriptorResponse = await rest.api.AssetModelResource.getAssetDescriptors();
|
|
440
|
-
const attributeDescriptorResponse = await rest.api.AssetModelResource.getAttributeDescriptors();
|
|
441
|
-
const attributeValueDescriptorResponse = await rest.api.AssetModelResource.getAttributeValueDescriptors();
|
|
442
|
-
const metaItemDescriptorResponse = await rest.api.AssetModelResource.getMetaItemDescriptors();
|
|
443
|
-
|
|
444
|
-
AssetModelUtil._assetDescriptors = assetDescriptorResponse.data;
|
|
445
|
-
AssetModelUtil._attributeDescriptors = attributeDescriptorResponse.data;
|
|
446
|
-
AssetModelUtil._attributeValueDescriptors = attributeValueDescriptorResponse.data;
|
|
447
|
-
AssetModelUtil._metaItemDescriptors = metaItemDescriptorResponse.data;
|
|
448
|
-
} catch (e) {
|
|
449
|
-
console.error(e);
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
protected async doAuthInit(): Promise<boolean> {
|
|
456
|
-
let success = true;
|
|
457
|
-
switch (this._config.auth) {
|
|
458
|
-
case Auth.BASIC:
|
|
459
|
-
// TODO: Implement Basic auth support
|
|
460
|
-
if (this._config.credentials) {
|
|
461
|
-
rest.setBasicAuth(this._config.credentials.username, this._config.credentials.password);
|
|
462
|
-
}
|
|
463
|
-
this._setError(ORError.AUTH_TYPE_UNSUPPORTED);
|
|
464
|
-
success = false;
|
|
465
|
-
break;
|
|
466
|
-
case Auth.KEYCLOAK:
|
|
467
|
-
success = await this.loadAndInitialiseKeycloak();
|
|
468
|
-
// Add interceptor to inject authorization header on each request
|
|
469
|
-
rest.addRequestInterceptor(
|
|
470
|
-
(config: AxiosRequestConfig) => {
|
|
471
|
-
if (!config.headers.Authorization) {
|
|
472
|
-
const token = this.getKeycloakToken();
|
|
473
|
-
|
|
474
|
-
if (token) {
|
|
475
|
-
config.headers.Authorization = "Bearer " + token;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return config;
|
|
480
|
-
}
|
|
481
|
-
);
|
|
482
|
-
break;
|
|
483
|
-
case Auth.NONE:
|
|
484
|
-
// Nothing for us to do here
|
|
485
|
-
break;
|
|
486
|
-
default:
|
|
487
|
-
this._setError(ORError.AUTH_TYPE_UNSUPPORTED);
|
|
488
|
-
success = false;
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return success;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
protected doRestApiInit(): boolean {
|
|
496
|
-
rest.setTimeout(10000);
|
|
497
|
-
rest.initialise(this.getApiBaseUrl());
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
protected async doEventsSubscriptionInit(): Promise<boolean> {
|
|
502
|
-
let connected = false;
|
|
503
|
-
|
|
504
|
-
switch (this._config.eventProviderType) {
|
|
505
|
-
case EventProviderType.WEBSOCKET:
|
|
506
|
-
this._events = new WebSocketEventProvider(this._config.managerUrl);
|
|
507
|
-
this._events.subscribeStatusChange((status: EventProviderStatus) => this._onEventProviderStatusChanged(status));
|
|
508
|
-
connected = await this._events.connect();
|
|
509
|
-
break;
|
|
510
|
-
case EventProviderType.POLLING:
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (!connected) {
|
|
515
|
-
this._setError(ORError.EVENTS_CONNECTION_ERROR);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return connected;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
protected _onEventProviderStatusChanged(status: EventProviderStatus) {
|
|
522
|
-
switch (status) {
|
|
523
|
-
case EventProviderStatus.DISCONNECTED:
|
|
524
|
-
this._emitEvent(OREvent.EVENTS_DISCONNECTED);
|
|
525
|
-
break;
|
|
526
|
-
case EventProviderStatus.CONNECTED:
|
|
527
|
-
this._emitEvent(OREvent.EVENTS_CONNECTED);
|
|
528
|
-
break;
|
|
529
|
-
case EventProviderStatus.CONNECTING:
|
|
530
|
-
this._emitEvent(OREvent.EVENTS_CONNECTING);
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
protected async doConsoleInit(): Promise<boolean> {
|
|
536
|
-
try {
|
|
537
|
-
let orConsole = new Console(this._config.realm, this._config.consoleAutoEnable!, () => {
|
|
538
|
-
this._emitEvent(OREvent.CONSOLE_READY);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
this._console = orConsole;
|
|
542
|
-
|
|
543
|
-
await orConsole.initialise();
|
|
544
|
-
this._emitEvent(OREvent.CONSOLE_INIT);
|
|
545
|
-
return true;
|
|
546
|
-
} catch (e) {
|
|
547
|
-
this._setError(ORError.CONSOLE_ERROR);
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
public logout(redirectUrl?: string) {
|
|
553
|
-
if (this._keycloak) {
|
|
554
|
-
if (this.console.isMobile) {
|
|
555
|
-
this.console.storeData("REFRESH_TOKEN", null);
|
|
556
|
-
}
|
|
557
|
-
const options = redirectUrl && redirectUrl !== "" ? {redirectUri: redirectUrl} : null;
|
|
558
|
-
this._keycloak.logout(options);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
public login(options?: LoginOptions) {
|
|
563
|
-
if (!this.initialised) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
switch (this._config.auth) {
|
|
567
|
-
case Auth.BASIC:
|
|
568
|
-
if (options && options.credentials) {
|
|
569
|
-
this._config.credentials = Object.assign({}, options.credentials);
|
|
570
|
-
}
|
|
571
|
-
const username = this._config.credentials ? this._config.credentials.username : null;
|
|
572
|
-
const password = this._config.credentials ? this._config.credentials.password : null;
|
|
573
|
-
|
|
574
|
-
if (username && password && username !== "" && password !== "") {
|
|
575
|
-
// TODO: Perform some request to check basic auth credentials
|
|
576
|
-
this._setAuthenticated(true);
|
|
577
|
-
}
|
|
578
|
-
break;
|
|
579
|
-
case Auth.KEYCLOAK:
|
|
580
|
-
if (this._keycloak) {
|
|
581
|
-
const keycloakOptions: any = {};
|
|
582
|
-
if (options && options.redirectUrl && options.redirectUrl !== "") {
|
|
583
|
-
keycloakOptions.redirectUri = options.redirectUrl;
|
|
584
|
-
}
|
|
585
|
-
if (this.isMobile()) {
|
|
586
|
-
keycloakOptions.scope = "offline_access";
|
|
587
|
-
}
|
|
588
|
-
this._keycloak.login(keycloakOptions);
|
|
589
|
-
}
|
|
590
|
-
break;
|
|
591
|
-
case Auth.NONE:
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
public isSuperUser() {
|
|
597
|
-
return this.hasRole("admin");
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
public getApiBaseUrl() {
|
|
601
|
-
let baseUrl = this._config.managerUrl;
|
|
602
|
-
baseUrl += "/api/" + this._config.realm + "/";
|
|
603
|
-
return baseUrl;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
public getAppName(): string {
|
|
607
|
-
let pathArr = location.pathname.split('/');
|
|
608
|
-
return pathArr.length >= 1 ? pathArr[1] : "";
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
public hasRole(role: string) {
|
|
612
|
-
return this._roles && this._roles.indexOf(role) >= 0;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
public getAuthorizationHeader(): string | undefined {
|
|
616
|
-
if (this._keycloak && this.authenticated) {
|
|
617
|
-
return "Bearer " + this._keycloak.token;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return undefined;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
public getKeycloakToken(): string | undefined {
|
|
624
|
-
if (this._keycloak && this.authenticated) {
|
|
625
|
-
return this._keycloak.token;
|
|
626
|
-
}
|
|
627
|
-
return undefined;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
public getRealm(): string | undefined {
|
|
631
|
-
if (this._config) {
|
|
632
|
-
return this._config.realm;
|
|
633
|
-
}
|
|
634
|
-
return undefined;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
protected isMobile(): boolean {
|
|
638
|
-
return this.console && this.console.isMobile;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
protected _onAuthenticated() {
|
|
642
|
-
// If native shell is enabled, we need an offline refresh token
|
|
643
|
-
if (this.console && this.console.isMobile && this.config.auth === Auth.KEYCLOAK) {
|
|
644
|
-
|
|
645
|
-
if (this._keycloak.refreshTokenParsed.typ === "Offline") {
|
|
646
|
-
console.debug("Storing offline refresh token");
|
|
647
|
-
this.console.storeData("REFRESH_TOKEN", this._keycloak.refreshToken);
|
|
648
|
-
} else {
|
|
649
|
-
this.login();
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// NOTE: The below works with Keycloak 2.x JS API - They made breaking changes in newer versions
|
|
655
|
-
// so this will need updating.
|
|
656
|
-
protected async loadAndInitialiseKeycloak(): Promise<boolean> {
|
|
657
|
-
|
|
658
|
-
// Load the keycloak JS API
|
|
659
|
-
const promise = new Promise<Event>((resolve, reject) => {
|
|
660
|
-
// Load keycloak script from keycloak server
|
|
661
|
-
const scriptElement = document.createElement("script");
|
|
662
|
-
scriptElement.src = this._config.keycloakUrl + "/js/keycloak.js";
|
|
663
|
-
scriptElement.onload = (e) => resolve(e);
|
|
664
|
-
scriptElement.onerror = (e) => reject(e);
|
|
665
|
-
document.querySelector("head")!.appendChild(scriptElement);
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
try {
|
|
669
|
-
await promise;
|
|
670
|
-
|
|
671
|
-
// Should have Keycloak global var now
|
|
672
|
-
if (!(window as any).Keycloak) {
|
|
673
|
-
this._setError(ORError.KEYCLOAK_FAILED_TO_LOAD);
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Initialise keycloak
|
|
678
|
-
this._keycloak = (window as any).Keycloak({
|
|
679
|
-
clientId: "openremote",
|
|
680
|
-
realm: this._config.realm,
|
|
681
|
-
url: this._config.keycloakUrl
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
this._keycloak.onAuthSuccess = () => {
|
|
685
|
-
if (keycloakPromise) {
|
|
686
|
-
keycloakPromise(true);
|
|
687
|
-
}
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
this._keycloak.onAuthError = () => {
|
|
691
|
-
this._setAuthenticated(false);
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
// There's a bug in some Keycloak versions which means the init promise doesn't resolve
|
|
695
|
-
// so putting a check in place; wrap keycloak promise in proper ES6 promise
|
|
696
|
-
let keycloakPromise: any = null;
|
|
697
|
-
try {
|
|
698
|
-
// Try to use a stored offline refresh token if defined
|
|
699
|
-
const offlineToken = await this._getNativeOfflineRefreshToken();
|
|
700
|
-
|
|
701
|
-
const authenticated = await new Promise<boolean>(((resolve, reject) => {
|
|
702
|
-
keycloakPromise = resolve;
|
|
703
|
-
this._keycloak.init({
|
|
704
|
-
checkLoginIframe: false, // Doesn't work well with offline tokens or periodic token updates
|
|
705
|
-
onLoad: this._config.autoLogin ? "login-required" : "check-sso",
|
|
706
|
-
refreshToken: offlineToken
|
|
707
|
-
}).success((auth: boolean) => {
|
|
708
|
-
resolve(auth);
|
|
709
|
-
}).error(() => {
|
|
710
|
-
reject();
|
|
711
|
-
});
|
|
712
|
-
}));
|
|
713
|
-
|
|
714
|
-
keycloakPromise = null;
|
|
715
|
-
|
|
716
|
-
if (authenticated) {
|
|
717
|
-
|
|
718
|
-
this._name = this._keycloak.tokenParsed.name;
|
|
719
|
-
this._username = this._keycloak.tokenParsed.preferred_username;
|
|
720
|
-
this._roles = this._keycloak.resourceAccess.openremote.roles;
|
|
721
|
-
|
|
722
|
-
// Update the access token every 10s (note keycloak will only update if expiring within configured
|
|
723
|
-
// time period.
|
|
724
|
-
if (this._keycloakUpdateTokenInterval) {
|
|
725
|
-
clearInterval(this._keycloakUpdateTokenInterval);
|
|
726
|
-
delete this._keycloakUpdateTokenInterval;
|
|
727
|
-
}
|
|
728
|
-
this._keycloakUpdateTokenInterval = window.setInterval(() => {
|
|
729
|
-
this.updateKeycloakAccessToken();
|
|
730
|
-
}, 10000);
|
|
731
|
-
this._onAuthenticated();
|
|
732
|
-
}
|
|
733
|
-
this._setAuthenticated(authenticated);
|
|
734
|
-
return true;
|
|
735
|
-
} catch (e) {
|
|
736
|
-
console.error(e);
|
|
737
|
-
keycloakPromise = null;
|
|
738
|
-
this._setAuthenticated(false);
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
|
-
} catch (error) {
|
|
742
|
-
this._setError(ORError.KEYCLOAK_FAILED_TO_LOAD);
|
|
743
|
-
return false;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
protected updateKeycloakAccessToken(): Promise<boolean> {
|
|
748
|
-
// Access token must be good for X more seconds, should be half of Constants.ACCESS_TOKEN_LIFESPAN_SECONDS
|
|
749
|
-
return new Promise<boolean>(() => {
|
|
750
|
-
this._keycloak.updateToken(30)
|
|
751
|
-
.success((tokenRefreshed: boolean) => {
|
|
752
|
-
// If refreshed from server, it means the refresh token was still good for another access token
|
|
753
|
-
console.debug("Access token update success, refreshed from server: " + tokenRefreshed);
|
|
754
|
-
return tokenRefreshed;
|
|
755
|
-
})
|
|
756
|
-
.error(() => {
|
|
757
|
-
// Refresh token expired (either SSO max session duration or offline idle timeout), see
|
|
758
|
-
// IDENTITY_SESSION_MAX_MINUTES and IDENTITY_SESSION_OFFLINE_TIMEOUT_MINUTES server config
|
|
759
|
-
console.info("Access token update failed, refresh token expired, login required");
|
|
760
|
-
this._keycloak.clearToken();
|
|
761
|
-
this._keycloak.login();
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
protected async _getNativeOfflineRefreshToken(): Promise<string | null> {
|
|
767
|
-
if (this.console && this.console.isMobile) {
|
|
768
|
-
return await this.console.retrieveData("REFRESH_TOKEN");
|
|
769
|
-
}
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
protected _emitEvent(event: OREvent) {
|
|
774
|
-
window.setTimeout(() => {
|
|
775
|
-
let listeners = this._listeners.slice();
|
|
776
|
-
for (const listener of listeners) {
|
|
777
|
-
listener(event);
|
|
778
|
-
}
|
|
779
|
-
}, 0);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
protected _setError(error: ORError) {
|
|
783
|
-
this._error = error;
|
|
784
|
-
this._emitEvent(OREvent.ERROR);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// TODO: Remove events logic once websocket supports anonymous connections
|
|
788
|
-
protected _setAuthenticated(authenticated: boolean) {
|
|
789
|
-
this._authenticated = authenticated;
|
|
790
|
-
if (authenticated) {
|
|
791
|
-
if (!this._events) {
|
|
792
|
-
this.doEventsSubscriptionInit();
|
|
793
|
-
}
|
|
794
|
-
} else {
|
|
795
|
-
if (this._events) {
|
|
796
|
-
this._events.disconnect();
|
|
797
|
-
}
|
|
798
|
-
this._events = undefined;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
export default new Manager();
|