From 24b9cadf809576512d6496a32084b8eb2b598475 Mon Sep 17 00:00:00 2001 From: Matias Arriola Date: Mon, 21 Feb 2022 02:50:03 +0000 Subject: [PATCH] Add expiration dialog. --- packages/editor/src/index.tsx | 28 +--- .../src/components/MockPersistenceManager.ts | 48 ++++++ .../src/components/PersistenceManager.ts | 28 ++++ .../src/components/RestPersistenceManager.ts | 26 ++-- .../components/widget/AccountSettingsPanel.js | 4 +- packages/mindplot/src/index.ts | 2 + packages/webapp/src/@types/index.d.ts | 1 + packages/webapp/src/app.tsx | 9 +- .../webapp/src/classes/app-config/index.ts | 5 +- .../client/cache-decorator-client/index.ts | 17 ++- .../client/client-health-sentinel/index.tsx | 2 +- packages/webapp/src/classes/client/index.ts | 6 +- .../client/mock-client/example-map.wxml | 70 +++++++++ .../src/classes/client/mock-client/index.ts | 25 +++- .../src/classes/client/rest-client/index.ts | 138 +++++++++++++----- .../HOCs/withSessionExpirationHandling.tsx | 32 ++++ .../src/components/editor-page/index.tsx | 18 ++- .../components/forgot-password-page/index.tsx | 2 + .../src/components/login-page/index.tsx | 2 + .../account-info-dialog/index.tsx | 2 +- .../maps-page/account-menu/index.tsx | 19 ++- .../webapp/src/components/maps-page/index.tsx | 2 - packages/webapp/src/utils.ts | 15 ++ packages/webapp/webpack.common.js | 4 + 24 files changed, 415 insertions(+), 90 deletions(-) create mode 100644 packages/mindplot/src/components/MockPersistenceManager.ts create mode 100644 packages/webapp/src/classes/client/mock-client/example-map.wxml create mode 100644 packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx create mode 100644 packages/webapp/src/utils.ts diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 997a7bad..13a6e37f 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -38,13 +38,14 @@ declare global { } export type EditorPropsType = { - initCallback?: (locale: string) => void; + initCallback?: (locale: string, persistenceManager: PersistenceManager) => void; mapId?: number; isTryMode: boolean; readOnlyMode: boolean; locale?: string; onAction: (action: ToolbarActionType) => void; hotkeys?: boolean; + persistenceManager: PersistenceManager; }; const loadLocaleData = (locale: string) => { @@ -62,33 +63,15 @@ const loadLocaleData = (locale: string) => { } } -const initMindplot = (locale: string) => { +const initMindplot = (locale: string, persistenceManager: PersistenceManager) => { // Change page title ... document.title = `${global.mapTitle} | WiseMapping `; - // Configure persistence manager ... - let persistence: PersistenceManager; - if (!global.memoryPersistence && !global.readOnly) { - persistence = new RESTPersistenceManager({ - documentUrl: '/c/restful/maps/{id}/document', - revertUrl: '/c/restful/maps/{id}/history/latest', - lockUrl: '/c/restful/maps/{id}/lock', - timestamp: global.lockTimestamp, - session: global.lockSession, - }); - } else { - persistence = new LocalStorageManager( - `/c/restful/maps/{id}/${global.historyId ? `${global.historyId}/` : ''}document/xml${!global.isAuth ? '-pub' : '' - }`, - true - ); - } - const params = new URLSearchParams(window.location.search.substring(1)); const zoomParam = Number.parseFloat(params.get('zoom')); const options = DesignerOptionsBuilder.buildOptions({ - persistenceManager: persistence, + persistenceManager, readOnly: Boolean(global.readOnly || false), mapId: String(global.mapId), container: 'mindplot', @@ -120,9 +103,10 @@ const Editor = ({ locale = 'en', onAction, hotkeys = true, + persistenceManager, }: EditorPropsType): React.ReactElement => { React.useEffect(() => { - initCallback(locale); + initCallback(locale, persistenceManager); }, []); React.useEffect(() => { diff --git a/packages/mindplot/src/components/MockPersistenceManager.ts b/packages/mindplot/src/components/MockPersistenceManager.ts new file mode 100644 index 00000000..f139dc20 --- /dev/null +++ b/packages/mindplot/src/components/MockPersistenceManager.ts @@ -0,0 +1,48 @@ +/* + * Copyright [2022] [wisemapping] + * + * Licensed under WiseMapping Public License, Version 1.0 (the "License"). + * It is basically the Apache License, Version 2.0 (the "License") plus the + * "powered by wisemapping" text requirement on every single page; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the license at + * + * http://www.wisemapping.org/license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import $ from 'jquery'; +import { $assert } from '@wisemapping/core-js'; +import PersistenceManager from './PersistenceManager'; + +class MockPersistenceManager extends PersistenceManager { + private exampleMap: string; + + constructor(exampleMapAsXml: string) { + super(); + $assert(exampleMapAsXml, 'The test map must be set'); + this.exampleMap = exampleMapAsXml; + } + + saveMapXml(): void { + // Ignore, no implementation required ... + } + + discardChanges() { + // Ignore, no implementation required ... + } + + loadMapDom() { + return $.parseXML(this.exampleMap); + } + + unlockMap(): void { + // Ignore, no implementation required ... + } +} + +export default MockPersistenceManager; diff --git a/packages/mindplot/src/components/PersistenceManager.ts b/packages/mindplot/src/components/PersistenceManager.ts index a56bb2c7..c235ec01 100644 --- a/packages/mindplot/src/components/PersistenceManager.ts +++ b/packages/mindplot/src/components/PersistenceManager.ts @@ -20,10 +20,20 @@ import { $assert } from '@wisemapping/core-js'; import { Mindmap } from '..'; import XMLSerializerFactory from './persistence/XMLSerializerFactory'; +export type PersistenceError = { + severity: string; + message: string; + errorType?: 'session-expired' | 'bad-request' | 'generic'; +}; + +export type PersistenceErrorCallback = (error: PersistenceError) => void; + abstract class PersistenceManager { // eslint-disable-next-line no-use-before-define static _instance: PersistenceManager; + private _errorHandlers: PersistenceErrorCallback[] = []; + save(mindmap: Mindmap, editorProperties, saveHistory: boolean, events?) { $assert(mindmap, 'mindmap can not be null'); $assert(editorProperties, 'editorProperties can not be null'); @@ -48,6 +58,24 @@ abstract class PersistenceManager { return PersistenceManager.loadFromDom(mapId, domDocument); } + triggerError(error: PersistenceError) { + this._errorHandlers.forEach((handler) => handler(error)); + } + + addErrorHandler(callback: PersistenceErrorCallback) { + this._errorHandlers.push(callback); + } + + removeErrorHandler(callback?: PersistenceErrorCallback) { + if (!callback) { + this._errorHandlers.length = 0; + } + const index = this._errorHandlers.findIndex((handler) => handler === callback); + if (index !== -1) { + this._errorHandlers.splice(index, 1); + } + } + abstract discardChanges(mapId: string): void; abstract loadMapDom(mapId: string): Document; diff --git a/packages/mindplot/src/components/RestPersistenceManager.ts b/packages/mindplot/src/components/RestPersistenceManager.ts index 7eb076a9..d97da659 100644 --- a/packages/mindplot/src/components/RestPersistenceManager.ts +++ b/packages/mindplot/src/components/RestPersistenceManager.ts @@ -18,7 +18,7 @@ import { $assert } from '@wisemapping/core-js'; import $ from 'jquery'; import { $msg } from './Messages'; -import PersistenceManager from './PersistenceManager'; +import PersistenceManager, { PersistenceError } from './PersistenceManager'; class RESTPersistenceManager extends PersistenceManager { private documentUrl: string; @@ -76,7 +76,7 @@ class RESTPersistenceManager extends PersistenceManager { method: 'PUT', // Blob helps to resuce the memory on large payload. body: new Blob([JSON.stringify(data)], { type: 'text/plain' }), - headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json' }, + headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json', 'X-CSRF-Token': this.getCSRFToken() }, }, ).then(async (response: Response) => { if (response.ok) { @@ -86,7 +86,7 @@ class RESTPersistenceManager extends PersistenceManager { console.log(`Saving error: ${response.status}`); let userMsg; if (response.status === 405) { - userMsg = { severity: 'SEVERE', message: $msg('SESSION_EXPIRED') }; + userMsg = { severity: 'SEVERE', message: $msg('SESSION_EXPIRED'), errorType: 'session-expired' }; } else { const responseText = await response.text(); const contentType = response.headers['Content-Type']; @@ -101,6 +101,7 @@ class RESTPersistenceManager extends PersistenceManager { userMsg = persistence._buildError(serverMsg); } } + this.triggerError(userMsg); events.onError(userMsg); } @@ -109,9 +110,11 @@ class RESTPersistenceManager extends PersistenceManager { clearTimeout(persistence.clearTimeout); } persistence.onSave = false; - }).catch((error) => { - console.log(`Unexpected save error => ${error}`); - const userMsg = { severity: 'SEVERE', message: $msg('SAVE_COULD_NOT_BE_COMPLETED') }; + }).catch(() => { + const userMsg: PersistenceError = { + severity: 'SEVERE', message: $msg('SAVE_COULD_NOT_BE_COMPLETED'), errorType: 'generic', + }; + this.triggerError(userMsg); events.onError(userMsg); // Clear event timeout ... @@ -127,7 +130,7 @@ class RESTPersistenceManager extends PersistenceManager { fetch(this.revertUrl.replace('{id}', mapId), { method: 'POST', - headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json' }, + headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json', 'X-CSRF-Token': this.getCSRFToken() }, }); } @@ -136,7 +139,7 @@ class RESTPersistenceManager extends PersistenceManager { this.lockUrl.replace('{id}', mapId), { method: 'PUT', - headers: { 'Content-Type': 'text/plain' }, + headers: { 'Content-Type': 'text/plain', 'X-CSRF-Token': this.getCSRFToken() }, body: 'false', }, ); @@ -156,14 +159,17 @@ class RESTPersistenceManager extends PersistenceManager { return { severity, message }; } + private getCSRFToken(): string { + return document.head.querySelector('meta[name="_csrf"]').getAttribute('content'); + } + loadMapDom(mapId: string): Document { - // Let's try to open one from the local directory ... let xml: Document; $.ajax({ url: `${this.documentUrl.replace('{id}', mapId)}/xml`, method: 'get', async: false, - headers: { 'Content-Type': 'text/plain', Accept: 'application/xml' }, + headers: { 'Content-Type': 'text/plain', Accept: 'application/xml', 'X-CSRF-Token': this.getCSRFToken() }, success(responseText) { xml = responseText; }, diff --git a/packages/mindplot/src/components/widget/AccountSettingsPanel.js b/packages/mindplot/src/components/widget/AccountSettingsPanel.js index a91d05bc..29e4ebea 100644 --- a/packages/mindplot/src/components/widget/AccountSettingsPanel.js +++ b/packages/mindplot/src/components/widget/AccountSettingsPanel.js @@ -25,7 +25,8 @@ class AccountSettingsPanel extends ListToolbarPanel { // Overwite default behaviour ... }, setValue() { - window.location = '/c/logout'; + const elem = document.getElementById('logoutFrom'); + elem.submit(); }, }; super(elemId, model); @@ -59,6 +60,7 @@ class AccountSettingsPanel extends ListToolbarPanel { content[0].innerHTML = `

${global.accountName}

${global.accountEmail}

+
Logout
diff --git a/packages/mindplot/src/index.ts b/packages/mindplot/src/index.ts index ed95a2ee..550eb69d 100644 --- a/packages/mindplot/src/index.ts +++ b/packages/mindplot/src/index.ts @@ -22,6 +22,7 @@ import PersistenceManager from './components/PersistenceManager'; import Designer from './components/Designer'; import LocalStorageManager from './components/LocalStorageManager'; import RESTPersistenceManager from './components/RestPersistenceManager'; +import MockPersistenceManager from './components/MockPersistenceManager'; import Menu from './components/widget/Menu'; import DesignerOptionsBuilder from './components/DesignerOptionsBuilder'; import ImageExporterFactory from './components/export/ImageExporterFactory'; @@ -48,6 +49,7 @@ export { DesignerBuilder, PersistenceManager, RESTPersistenceManager, + MockPersistenceManager, LocalStorageManager, DesignerOptionsBuilder, buildDesigner, diff --git a/packages/webapp/src/@types/index.d.ts b/packages/webapp/src/@types/index.d.ts index e238d01c..5d1d9747 100644 --- a/packages/webapp/src/@types/index.d.ts +++ b/packages/webapp/src/@types/index.d.ts @@ -1,2 +1,3 @@ declare module '*.png'; declare module '*.svg'; +declare module '*.wxml'; \ No newline at end of file diff --git a/packages/webapp/src/app.tsx b/packages/webapp/src/app.tsx index a07d70d5..0a9cee3b 100644 --- a/packages/webapp/src/app.tsx +++ b/packages/webapp/src/app.tsx @@ -17,6 +17,7 @@ import { ThemeProvider, Theme, StyledEngineProvider } from '@mui/material/styles import ReactGA from 'react-ga'; import EditorPage from './components/editor-page'; import AppConfig from './classes/app-config'; +import withSessionExpirationHandling from './components/HOCs/withSessionExpirationHandling'; declare module '@mui/styles/defaultTheme' { @@ -43,6 +44,8 @@ const App = (): ReactElement => { const istTryMode = global.memoryPersistence; const mapId = parseInt(global.mapId, 10); + const EditorPageComponent = withSessionExpirationHandling(EditorPage); + return locale.message ? ( @@ -80,13 +83,13 @@ const App = (): ReactElement => { /> - + - + diff --git a/packages/webapp/src/classes/app-config/index.ts b/packages/webapp/src/classes/app-config/index.ts index 7524aed4..d586f1c0 100644 --- a/packages/webapp/src/classes/app-config/index.ts +++ b/packages/webapp/src/classes/app-config/index.ts @@ -1,4 +1,3 @@ -import { sessionExpired } from "../../redux/clientSlice"; import Client from "../client"; import CacheDecoratorClient from "../client/cache-decorator-client"; import MockClient from "../client/mock-client"; @@ -53,9 +52,7 @@ class _AppConfig { const config = this.getInstance(); let result: Client; if (config.clientType == 'rest') { - result = new RestClient(config.apiBaseUrl, () => { - sessionExpired(); - }); + result = new RestClient(config.apiBaseUrl); console.log('Service using rest client. ' + JSON.stringify(config)); } else { console.log('Warning:Service using mockservice client'); diff --git a/packages/webapp/src/classes/client/cache-decorator-client/index.ts b/packages/webapp/src/classes/client/cache-decorator-client/index.ts index 8c988e4e..e1106151 100644 --- a/packages/webapp/src/classes/client/cache-decorator-client/index.ts +++ b/packages/webapp/src/classes/client/cache-decorator-client/index.ts @@ -1,4 +1,4 @@ -import { Mindmap } from '@wisemapping/mindplot'; +import { Mindmap, PersistenceManager } from '@wisemapping/mindplot'; import Client, { AccountInfo, BasicMapInfo, @@ -17,7 +17,11 @@ class CacheDecoratorClient implements Client { constructor(client: Client) { this.client = client; } - + + onSessionExpired(callback?: () => void): () => void { + return this.client.onSessionExpired(callback); + } + fetchMindmap(id: number): Mindmap { return this.client.fetchMindmap(id); } @@ -125,6 +129,15 @@ class CacheDecoratorClient implements Client { revertHistory(id: number, cid: number): Promise { return this.client.revertHistory(id, cid); } + + buildPersistenceManager(): PersistenceManager { + return this.client.buildPersistenceManager(); + } + + removePersistenceManager(): void { + return this.client.removePersistenceManager(); + } + } export default CacheDecoratorClient; \ No newline at end of file diff --git a/packages/webapp/src/classes/client/client-health-sentinel/index.tsx b/packages/webapp/src/classes/client/client-health-sentinel/index.tsx index f442306f..468882bb 100644 --- a/packages/webapp/src/classes/client/client-health-sentinel/index.tsx +++ b/packages/webapp/src/classes/client/client-health-sentinel/index.tsx @@ -45,7 +45,7 @@ const ClientHealthSentinel = (): React.ReactElement => { diff --git a/packages/webapp/src/classes/client/index.ts b/packages/webapp/src/classes/client/index.ts index 09728cb3..f64ba644 100644 --- a/packages/webapp/src/classes/client/index.ts +++ b/packages/webapp/src/classes/client/index.ts @@ -1,4 +1,4 @@ -import { Mindmap } from '@wisemapping/mindplot'; +import { Mindmap, PersistenceManager } from '@wisemapping/mindplot'; import { Locale, LocaleCode } from '../app-i18n'; export type NewUser = { @@ -109,6 +109,10 @@ interface Client { fetchMindmap(id:number): Mindmap; + buildPersistenceManager(): PersistenceManager; + removePersistenceManager(): void; + + onSessionExpired(callback?: () => void): () => void; } export default Client; diff --git a/packages/webapp/src/classes/client/mock-client/example-map.wxml b/packages/webapp/src/classes/client/mock-client/example-map.wxml new file mode 100644 index 00000000..6d0ab9c5 --- /dev/null +++ b/packages/webapp/src/classes/client/mock-client/example-map.wxml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webapp/src/classes/client/mock-client/index.ts b/packages/webapp/src/classes/client/mock-client/index.ts index 57376cea..a52e094f 100644 --- a/packages/webapp/src/classes/client/mock-client/index.ts +++ b/packages/webapp/src/classes/client/mock-client/index.ts @@ -1,4 +1,4 @@ -import { Mindmap } from '@wisemapping/mindplot'; +import { Mindmap, MockPersistenceManager, PersistenceManager } from '@wisemapping/mindplot'; import XMLSerializerTango from '@wisemapping/mindplot/src/components/persistence/XMLSerializerTango'; import Client, { AccountInfo, @@ -11,6 +11,7 @@ import Client, { Permission, } from '..'; import { LocaleCode, localeFromStr } from '../../app-i18n'; +import exampleMap from './example-map.wxml'; const label1: Label = { id: 1, @@ -34,7 +35,8 @@ class MockClient implements Client { private maps: MapInfo[] = []; private labels: Label[] = []; private permissionsByMap: Map = new Map(); - + private persistenceManager: PersistenceManager; + constructor() { // Remove, just for develop .... function createMapInfo( @@ -109,6 +111,10 @@ class MockClient implements Client { this.labels = [label1, label2, label3]; } + + onSessionExpired(callback?: () => void): () => void { + return callback; + } fetchMindmap(id: number): Mindmap { const parser = new DOMParser(); @@ -392,6 +398,21 @@ class MockClient implements Client { console.log('email:' + email); return Promise.resolve(); } + + buildPersistenceManager(): PersistenceManager { + if (this.persistenceManager){ + return this.persistenceManager; + } + const persistence: PersistenceManager = new MockPersistenceManager(exampleMap); + this.persistenceManager = persistence; + return persistence; + } + + removePersistenceManager(): void { + if (this.persistenceManager) { + delete this.persistenceManager; + } + } } export default MockClient; diff --git a/packages/webapp/src/classes/client/rest-client/index.ts b/packages/webapp/src/classes/client/rest-client/index.ts index 0cb227e6..74fc6d84 100644 --- a/packages/webapp/src/classes/client/rest-client/index.ts +++ b/packages/webapp/src/classes/client/rest-client/index.ts @@ -1,5 +1,6 @@ -import { LocalStorageManager, Mindmap } from '@wisemapping/mindplot'; -import axios from 'axios'; +import { LocalStorageManager, Mindmap, PersistenceManager, RESTPersistenceManager } from '@wisemapping/mindplot'; +import { PersistenceError } from '@wisemapping/mindplot/src/components/PersistenceManager'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; import Client, { ErrorInfo, MapInfo, @@ -11,15 +12,46 @@ import Client, { ImportMapInfo, Permission, } from '..'; +import { getCsrfToken } from '../../../utils'; import { LocaleCode, localeFromStr } from '../../app-i18n'; export default class RestClient implements Client { private baseUrl: string; - private sessionExpired: () => void; + private persistenceManager: PersistenceManager; + private axios: AxiosInstance; - constructor(baseUrl: string, sessionExpired: () => void) { + private checkResponseForSessionExpired = (error: { response?: AxiosResponse }): Promise<{ response?: AxiosResponse }> => { + // TODO: Improve session timeout response and response handling + if (error.response && error.response.status === 405) { + this.sessionExpired(); + } + return Promise.reject(error); + }; + + constructor(baseUrl: string) { this.baseUrl = baseUrl; - this.sessionExpired = sessionExpired; + this.axios = axios.create({ maxRedirects: 0 }); + const csrfToken = getCsrfToken(); + if (csrfToken) { + this.axios.defaults.headers['X-CSRF-TOKEN'] = csrfToken; + } else { + console.warn('csrf token not found in html head'); + } + this.axios.interceptors.response.use((r) => r, (r) => this.checkResponseForSessionExpired(r)); + } + + private _onSessionExpired : () => void; + onSessionExpired(callback?: () => void): () => void { + if (callback) { + this._onSessionExpired = callback; + } + return this._onSessionExpired; + } + + private sessionExpired() { + if (this._onSessionExpired) { + this._onSessionExpired(); + } } fetchMindmap(id: number): Mindmap { @@ -34,7 +66,7 @@ export default class RestClient implements Client { deleteMapPermission(id: number, email: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .delete(`${this.baseUrl}/c/restful/maps/${id}/collabs?email=${encodeURIComponent(email)}`, { headers: { 'Content-Type': 'text/plain' }, }) @@ -51,7 +83,7 @@ export default class RestClient implements Client { // eslint-disable-next-line @typescript-eslint/no-unused-vars addMapPermissions(id: number, message: string, permissions: Permission[]): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put( `${this.baseUrl}/c/restful/maps/${id}/collabs/`, { @@ -77,7 +109,7 @@ export default class RestClient implements Client { success: (labels: Permission[]) => void, reject: (error: ErrorInfo) => void ) => { - axios + this.axios .get(`${this.baseUrl}/c/restful/maps/${id}/collabs`, { headers: { 'Content-Type': 'text/plain' }, }) @@ -104,7 +136,7 @@ export default class RestClient implements Client { deleteAccount(): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .delete(`${this.baseUrl}/c/restful/account`, { headers: { 'Content-Type': 'text/plain' }, }) @@ -121,12 +153,12 @@ export default class RestClient implements Client { updateAccountInfo(firstname: string, lastname: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/c/restful/account/firstname`, firstname, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { - return axios.put(`${this.baseUrl}/c/restful/account/lastname`, lastname, { + return this.axios.put(`${this.baseUrl}/c/restful/account/lastname`, lastname, { headers: { 'Content-Type': 'text/plain' }, }); }) @@ -144,7 +176,7 @@ export default class RestClient implements Client { updateAccountPassword(pasword: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/c/restful/account/password`, pasword, { headers: { 'Content-Type': 'text/plain' }, }) @@ -161,7 +193,7 @@ export default class RestClient implements Client { updateAccountLanguage(locale: LocaleCode): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/c/restful/account/locale`, locale, { headers: { 'Content-Type': 'text/plain' }, }) @@ -179,7 +211,7 @@ export default class RestClient implements Client { importMap(model: ImportMapInfo): Promise { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post( `${this.baseUrl}/c/restful/maps?title=${encodeURIComponent(model.title)}&description=${model.description ? model.description : '' }`, @@ -203,7 +235,7 @@ export default class RestClient implements Client { success: (account: AccountInfo) => void, reject: (error: ErrorInfo) => void ) => { - axios + this.axios .get(`${this.baseUrl}/c/restful/account`, { headers: { 'Content-Type': 'application/json' }, }) @@ -227,7 +259,7 @@ export default class RestClient implements Client { deleteMaps(ids: number[]): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .delete(`${this.baseUrl}/c/restful/maps/batch?ids=${ids.join()}`, { headers: { 'Content-Type': 'text/plain' }, }) @@ -245,7 +277,7 @@ export default class RestClient implements Client { updateMapToPublic(id: number, isPublic: boolean): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/c/restful/maps/${id}/publish`, isPublic.toString(), { headers: { 'Content-Type': 'text/plain' }, }) @@ -262,7 +294,7 @@ export default class RestClient implements Client { revertHistory(id: number, hid: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post(`${this.baseUrl}/c/restful/maps/${id}/history/${hid}`, null, { headers: { 'Content-Type': 'text/pain' }, }) @@ -282,7 +314,7 @@ export default class RestClient implements Client { success: (historyList: ChangeHistory[]) => void, reject: (error: ErrorInfo) => void ) => { - axios + this.axios .get(`${this.baseUrl}/c/restful/maps/${id}/history/`, { headers: { 'Content-Type': 'application/json' }, }) @@ -307,12 +339,12 @@ export default class RestClient implements Client { renameMap(id: number, basicInfo: BasicMapInfo): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/c/restful/maps/${id}/title`, basicInfo.title, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { - return axios.put( + return this.axios.put( `${this.baseUrl}/c/restful/maps/${id}/description`, basicInfo.description || ' ', { headers: { 'Content-Type': 'text/plain' } } ); @@ -332,7 +364,7 @@ export default class RestClient implements Client { createMap(model: BasicMapInfo): Promise { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post( `${this.baseUrl}/c/restful/maps?title=${model.title}&description=${model.description ? model.description : '' }`, @@ -356,7 +388,7 @@ export default class RestClient implements Client { success: (mapsInfo: MapInfo[]) => void, reject: (error: ErrorInfo) => void ) => { - axios + this.axios .get(`${this.baseUrl}/c/restful/maps/`, { headers: { 'Content-Type': 'application/json' }, }) @@ -390,7 +422,7 @@ export default class RestClient implements Client { registerNewUser(user: NewUser): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post(`${this.baseUrl}/service/users/`, JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }) @@ -408,7 +440,7 @@ export default class RestClient implements Client { deleteMap(id: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .delete(`${this.baseUrl}/c/restful/maps/${id}`, { headers: { 'Content-Type': 'application/json' }, }) @@ -425,7 +457,7 @@ export default class RestClient implements Client { resetPassword(email: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/service/users/resetPassword?email=${encodeURIComponent(email)}`, null, { headers: { 'Content-Type': 'application/json' }, }) @@ -444,7 +476,7 @@ export default class RestClient implements Client { duplicateMap(id: number, basicInfo: BasicMapInfo): Promise { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post(`${this.baseUrl}/c/restful/maps/${id}`, JSON.stringify(basicInfo), { headers: { 'Content-Type': 'application/json' }, }) @@ -462,9 +494,8 @@ export default class RestClient implements Client { } updateStarred(id: number, starred: boolean): Promise { - console.debug(`Starred => ${starred}`) const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .put(`${this.baseUrl}/c/restful/maps/${id}/starred`, starred.toString(), { headers: { 'Content-Type': 'text/plain' }, }) @@ -485,7 +516,7 @@ export default class RestClient implements Client { success: (labels: Label[]) => void, reject: (error: ErrorInfo) => void ) => { - axios + this.axios .get(`${this.baseUrl}/c/restful/labels/`, { headers: { 'Content-Type': 'application/json' }, }) @@ -512,7 +543,7 @@ export default class RestClient implements Client { createLabel(title: string, color: string): Promise { const handler = (success: (labelId: number) => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post(`${this.baseUrl}/c/restful/labels`, JSON.stringify({ title, color, iconName: 'smile' }), { headers: { 'Content-Type': 'application/json' }, }) @@ -529,7 +560,7 @@ export default class RestClient implements Client { deleteLabel(id: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .delete(`${this.baseUrl}/c/restful/labels/${id}`) .then(() => { success(); @@ -544,7 +575,7 @@ export default class RestClient implements Client { addLabelToMap(labelId: number, mapId: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), { headers: { 'Content-Type': 'application/json' }, }) @@ -561,7 +592,7 @@ export default class RestClient implements Client { deleteLabelFromMap(labelId: number, mapId: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { - axios + this.axios .delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`) .then(() => { success(); @@ -574,6 +605,45 @@ export default class RestClient implements Client { return new Promise(handler); } + private onPersistenceManagerError(error: PersistenceError) { + if (error.errorType === 'session-expired') { + this.sessionExpired(); + } + } + + buildPersistenceManager(): PersistenceManager { + if (this.persistenceManager){ + return this.persistenceManager; + } + // TODO: Move globals out, make urls configurable + let persistence: PersistenceManager; + if (!global.memoryPersistence && !global.readOnly) { + persistence = new RESTPersistenceManager({ + documentUrl: '/c/restful/maps/{id}/document', + revertUrl: '/c/restful/maps/{id}/history/latest', + lockUrl: '/c/restful/maps/{id}/lock', + timestamp: global.lockTimestamp, + session: global.lockSession, + }); + } else { + persistence = new LocalStorageManager( + `/c/restful/maps/{id}/${global.historyId ? `${global.historyId}/` : ''}document/xml${!global.isAuth ? '-pub' : '' + }`, + true + ); + } + persistence.addErrorHandler((err) => this.onPersistenceManagerError(err)); + this.persistenceManager = persistence; + return persistence; + } + + removePersistenceManager(): void { + if (this.persistenceManager) { + this.persistenceManager.removeErrorHandler(); + delete this.persistenceManager; + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private parseResponseOnError = (response: any): ErrorInfo => { console.error("Backend error=>"); diff --git a/packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx b/packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx new file mode 100644 index 00000000..2e2fbda9 --- /dev/null +++ b/packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx @@ -0,0 +1,32 @@ +/* eslint-disable react/display-name */ +import React, { ComponentType, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Client from '../../classes/client'; +import ClientHealthSentinel from '../../classes/client/client-health-sentinel'; +import { activeInstance, sessionExpired } from '../../redux/clientSlice'; + +function withSessionExpirationHandling(Component: ComponentType) { + return (hocProps: T): React.ReactElement => { + const client: Client = useSelector(activeInstance); + const dispatch = useDispatch(); + + useEffect(() => { + if (client) { + client.onSessionExpired(() => { + dispatch(sessionExpired()); + }); + } else { + console.warn('Session expiration wont be handled because could not find client'); + } + }, []); + + return ( + <> + + ; + + ); + }; +} + +export default withSessionExpirationHandling; diff --git a/packages/webapp/src/components/editor-page/index.tsx b/packages/webapp/src/components/editor-page/index.tsx index d757e3a5..5ce49495 100644 --- a/packages/webapp/src/components/editor-page/index.tsx +++ b/packages/webapp/src/components/editor-page/index.tsx @@ -6,6 +6,9 @@ import AppI18n from '../../classes/app-i18n'; import { useSelector } from 'react-redux'; import { hotkeysEnabled } from '../../redux/editorSlice'; import ReactGA from 'react-ga'; +import Client from '../../classes/client'; +import { activeInstance } from '../../redux/clientSlice'; +import { PersistenceManager } from '@wisemapping/mindplot'; export type EditorPropsType = { mapId: number; @@ -16,13 +19,24 @@ const EditorPage = ({ mapId, ...props }: EditorPropsType): React.ReactElement => const [activeDialog, setActiveDialog] = React.useState(null); const hotkeys = useSelector(hotkeysEnabled); const userLocale = AppI18n.getUserLocale(); + const client: Client = useSelector(activeInstance); + const [persistenceManager, setPersistenceManager] = React.useState(); useEffect(() => { ReactGA.pageview(window.location.pathname + window.location.search); + const persistence = client.buildPersistenceManager(); + setPersistenceManager(persistence); + return () => client.removePersistenceManager(); }, []); - + + if (!persistenceManager) { + // persistenceManager must be ready for the editor to work + return null; + } return <> - + { activeDialog && { const [email, setEmail] = useState(''); @@ -54,6 +55,7 @@ const ForgotPassword = () => {
+ { + { - window.location.href = '/c/logout'; + window.location.href = '/c/login'; onClose(); }, onError: (error) => { diff --git a/packages/webapp/src/components/maps-page/account-menu/index.tsx b/packages/webapp/src/components/maps-page/account-menu/index.tsx index 21650d9b..c33f0fda 100644 --- a/packages/webapp/src/components/maps-page/account-menu/index.tsx +++ b/packages/webapp/src/components/maps-page/account-menu/index.tsx @@ -28,6 +28,12 @@ const AccountMenu = (): React.ReactElement => { setAnchorEl(null); }; + const handleLogout = (event: MouseEvent) => { + event.preventDefault(); + const elem = document.getElementById('logoutFrom') as HTMLFormElement; + elem.submit(); + }; + const account = fetchAccount(); return ( @@ -77,7 +83,8 @@ const AccountMenu = (): React.ReactElement => { - + + handleLogout(e)}> @@ -85,11 +92,13 @@ const AccountMenu = (): React.ReactElement => { - {action == 'change-password' && ( - setAction(undefined)} /> - )} + { + action == 'change-password' && ( + setAction(undefined)} /> + ) + } {action == 'account-info' && setAction(undefined)} />} - + ); }; diff --git a/packages/webapp/src/components/maps-page/index.tsx b/packages/webapp/src/components/maps-page/index.tsx index fe09229a..bf9d5a28 100644 --- a/packages/webapp/src/components/maps-page/index.tsx +++ b/packages/webapp/src/components/maps-page/index.tsx @@ -15,7 +15,6 @@ import Client, { Label } from '../../classes/client'; import ActionDispatcher from './action-dispatcher'; import { ActionType } from './action-chooser'; import AccountMenu from './account-menu'; -import ClientHealthSentinel from '../../classes/client/client-health-sentinel'; import HelpMenu from './help-menu'; import LanguageMenu from './language-menu'; import AppI18n, { Locales } from '../../classes/app-i18n'; @@ -153,7 +152,6 @@ const MapsPage = (): ReactElement => { messages={userLocale.message} >
- { + const meta = document.head.querySelector('meta[name="_csrf"]'); + if (!meta) { + return null; + } + return meta.getAttribute('content'); +}; + +export const getCsrfTokenParameter = (): string | null => { + const meta = document.head.querySelector('meta[name="_csrf_parameter"]'); + if (!meta) { + return null; + } + return meta.getAttribute('content'); +}; \ No newline at end of file diff --git a/packages/webapp/webpack.common.js b/packages/webapp/webpack.common.js index cf63e18f..4c3e57bd 100644 --- a/packages/webapp/webpack.common.js +++ b/packages/webapp/webpack.common.js @@ -30,6 +30,10 @@ module.exports = { test: /\.(png|jpe?g|gif|svg)$/, type: 'asset/inline', }, + { + test: /\.wxml$/i, + type: 'asset/source', + }, ], }, optimization: {