Add expiration dialog.

This commit is contained in:
Matias Arriola 2022-02-21 02:50:03 +00:00 committed by Paulo Veiga
parent 93aae24988
commit 24b9cadf80
24 changed files with 415 additions and 90 deletions

View File

@ -38,13 +38,14 @@ declare global {
} }
export type EditorPropsType = { export type EditorPropsType = {
initCallback?: (locale: string) => void; initCallback?: (locale: string, persistenceManager: PersistenceManager) => void;
mapId?: number; mapId?: number;
isTryMode: boolean; isTryMode: boolean;
readOnlyMode: boolean; readOnlyMode: boolean;
locale?: string; locale?: string;
onAction: (action: ToolbarActionType) => void; onAction: (action: ToolbarActionType) => void;
hotkeys?: boolean; hotkeys?: boolean;
persistenceManager: PersistenceManager;
}; };
const loadLocaleData = (locale: string) => { 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 ... // Change page title ...
document.title = `${global.mapTitle} | WiseMapping `; 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 params = new URLSearchParams(window.location.search.substring(1));
const zoomParam = Number.parseFloat(params.get('zoom')); const zoomParam = Number.parseFloat(params.get('zoom'));
const options = DesignerOptionsBuilder.buildOptions({ const options = DesignerOptionsBuilder.buildOptions({
persistenceManager: persistence, persistenceManager,
readOnly: Boolean(global.readOnly || false), readOnly: Boolean(global.readOnly || false),
mapId: String(global.mapId), mapId: String(global.mapId),
container: 'mindplot', container: 'mindplot',
@ -120,9 +103,10 @@ const Editor = ({
locale = 'en', locale = 'en',
onAction, onAction,
hotkeys = true, hotkeys = true,
persistenceManager,
}: EditorPropsType): React.ReactElement => { }: EditorPropsType): React.ReactElement => {
React.useEffect(() => { React.useEffect(() => {
initCallback(locale); initCallback(locale, persistenceManager);
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {

View File

@ -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;

View File

@ -20,10 +20,20 @@ import { $assert } from '@wisemapping/core-js';
import { Mindmap } from '..'; import { Mindmap } from '..';
import XMLSerializerFactory from './persistence/XMLSerializerFactory'; 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 { abstract class PersistenceManager {
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
static _instance: PersistenceManager; static _instance: PersistenceManager;
private _errorHandlers: PersistenceErrorCallback[] = [];
save(mindmap: Mindmap, editorProperties, saveHistory: boolean, events?) { save(mindmap: Mindmap, editorProperties, saveHistory: boolean, events?) {
$assert(mindmap, 'mindmap can not be null'); $assert(mindmap, 'mindmap can not be null');
$assert(editorProperties, 'editorProperties can not be null'); $assert(editorProperties, 'editorProperties can not be null');
@ -48,6 +58,24 @@ abstract class PersistenceManager {
return PersistenceManager.loadFromDom(mapId, domDocument); 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 discardChanges(mapId: string): void;
abstract loadMapDom(mapId: string): Document; abstract loadMapDom(mapId: string): Document;

View File

@ -18,7 +18,7 @@
import { $assert } from '@wisemapping/core-js'; import { $assert } from '@wisemapping/core-js';
import $ from 'jquery'; import $ from 'jquery';
import { $msg } from './Messages'; import { $msg } from './Messages';
import PersistenceManager from './PersistenceManager'; import PersistenceManager, { PersistenceError } from './PersistenceManager';
class RESTPersistenceManager extends PersistenceManager { class RESTPersistenceManager extends PersistenceManager {
private documentUrl: string; private documentUrl: string;
@ -76,7 +76,7 @@ class RESTPersistenceManager extends PersistenceManager {
method: 'PUT', method: 'PUT',
// Blob helps to resuce the memory on large payload. // Blob helps to resuce the memory on large payload.
body: new Blob([JSON.stringify(data)], { type: 'text/plain' }), 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) => { ).then(async (response: Response) => {
if (response.ok) { if (response.ok) {
@ -86,7 +86,7 @@ class RESTPersistenceManager extends PersistenceManager {
console.log(`Saving error: ${response.status}`); console.log(`Saving error: ${response.status}`);
let userMsg; let userMsg;
if (response.status === 405) { if (response.status === 405) {
userMsg = { severity: 'SEVERE', message: $msg('SESSION_EXPIRED') }; userMsg = { severity: 'SEVERE', message: $msg('SESSION_EXPIRED'), errorType: 'session-expired' };
} else { } else {
const responseText = await response.text(); const responseText = await response.text();
const contentType = response.headers['Content-Type']; const contentType = response.headers['Content-Type'];
@ -101,6 +101,7 @@ class RESTPersistenceManager extends PersistenceManager {
userMsg = persistence._buildError(serverMsg); userMsg = persistence._buildError(serverMsg);
} }
} }
this.triggerError(userMsg);
events.onError(userMsg); events.onError(userMsg);
} }
@ -109,9 +110,11 @@ class RESTPersistenceManager extends PersistenceManager {
clearTimeout(persistence.clearTimeout); clearTimeout(persistence.clearTimeout);
} }
persistence.onSave = false; persistence.onSave = false;
}).catch((error) => { }).catch(() => {
console.log(`Unexpected save error => ${error}`); const userMsg: PersistenceError = {
const userMsg = { severity: 'SEVERE', message: $msg('SAVE_COULD_NOT_BE_COMPLETED') }; severity: 'SEVERE', message: $msg('SAVE_COULD_NOT_BE_COMPLETED'), errorType: 'generic',
};
this.triggerError(userMsg);
events.onError(userMsg); events.onError(userMsg);
// Clear event timeout ... // Clear event timeout ...
@ -127,7 +130,7 @@ class RESTPersistenceManager extends PersistenceManager {
fetch(this.revertUrl.replace('{id}', mapId), fetch(this.revertUrl.replace('{id}', mapId),
{ {
method: 'POST', 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), this.lockUrl.replace('{id}', mapId),
{ {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain', 'X-CSRF-Token': this.getCSRFToken() },
body: 'false', body: 'false',
}, },
); );
@ -156,14 +159,17 @@ class RESTPersistenceManager extends PersistenceManager {
return { severity, message }; return { severity, message };
} }
private getCSRFToken(): string {
return document.head.querySelector('meta[name="_csrf"]').getAttribute('content');
}
loadMapDom(mapId: string): Document { loadMapDom(mapId: string): Document {
// Let's try to open one from the local directory ...
let xml: Document; let xml: Document;
$.ajax({ $.ajax({
url: `${this.documentUrl.replace('{id}', mapId)}/xml`, url: `${this.documentUrl.replace('{id}', mapId)}/xml`,
method: 'get', method: 'get',
async: false, 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) { success(responseText) {
xml = responseText; xml = responseText;
}, },

View File

@ -25,7 +25,8 @@ class AccountSettingsPanel extends ListToolbarPanel {
// Overwite default behaviour ... // Overwite default behaviour ...
}, },
setValue() { setValue() {
window.location = '/c/logout'; const elem = document.getElementById('logoutFrom');
elem.submit();
}, },
}; };
super(elemId, model); super(elemId, model);
@ -59,6 +60,7 @@ class AccountSettingsPanel extends ListToolbarPanel {
content[0].innerHTML = ` content[0].innerHTML = `
<p style='text-align:center;font-weight:bold;'>${global.accountName}</p> <p style='text-align:center;font-weight:bold;'>${global.accountName}</p>
<p>${global.accountEmail}</p> <p>${global.accountEmail}</p>
<form action="/c/logout" method='POST' id="logoutFrom"></form>
<div id="account-logout" model='logout' style='text-align:center'> <div id="account-logout" model='logout' style='text-align:center'>
Logout Logout
</div> </div>

View File

@ -22,6 +22,7 @@ import PersistenceManager from './components/PersistenceManager';
import Designer from './components/Designer'; import Designer from './components/Designer';
import LocalStorageManager from './components/LocalStorageManager'; import LocalStorageManager from './components/LocalStorageManager';
import RESTPersistenceManager from './components/RestPersistenceManager'; import RESTPersistenceManager from './components/RestPersistenceManager';
import MockPersistenceManager from './components/MockPersistenceManager';
import Menu from './components/widget/Menu'; import Menu from './components/widget/Menu';
import DesignerOptionsBuilder from './components/DesignerOptionsBuilder'; import DesignerOptionsBuilder from './components/DesignerOptionsBuilder';
import ImageExporterFactory from './components/export/ImageExporterFactory'; import ImageExporterFactory from './components/export/ImageExporterFactory';
@ -48,6 +49,7 @@ export {
DesignerBuilder, DesignerBuilder,
PersistenceManager, PersistenceManager,
RESTPersistenceManager, RESTPersistenceManager,
MockPersistenceManager,
LocalStorageManager, LocalStorageManager,
DesignerOptionsBuilder, DesignerOptionsBuilder,
buildDesigner, buildDesigner,

View File

@ -1,2 +1,3 @@
declare module '*.png'; declare module '*.png';
declare module '*.svg'; declare module '*.svg';
declare module '*.wxml';

View File

@ -17,6 +17,7 @@ import { ThemeProvider, Theme, StyledEngineProvider } from '@mui/material/styles
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import EditorPage from './components/editor-page'; import EditorPage from './components/editor-page';
import AppConfig from './classes/app-config'; import AppConfig from './classes/app-config';
import withSessionExpirationHandling from './components/HOCs/withSessionExpirationHandling';
declare module '@mui/styles/defaultTheme' { declare module '@mui/styles/defaultTheme' {
@ -43,6 +44,8 @@ const App = (): ReactElement => {
const istTryMode = global.memoryPersistence; const istTryMode = global.memoryPersistence;
const mapId = parseInt(global.mapId, 10); const mapId = parseInt(global.mapId, 10);
const EditorPageComponent = withSessionExpirationHandling(EditorPage);
return locale.message ? ( return locale.message ? (
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@ -80,13 +83,13 @@ const App = (): ReactElement => {
/> />
<Route <Route
exact path="/c/maps/" exact path="/c/maps/"
component={MapsPage} component={withSessionExpirationHandling(MapsPage)}
/> />
<Route exact path="/c/maps/:id/edit"> <Route exact path="/c/maps/:id/edit">
<EditorPage isTryMode={istTryMode} mapId={mapId} /> <EditorPageComponent isTryMode={istTryMode} mapId={mapId} />
</Route> </Route>
<Route exact path="/c/maps/:id/try"> <Route exact path="/c/maps/:id/try">
<EditorPage isTryMode={istTryMode} mapId={mapId} /> <EditorPageComponent isTryMode={istTryMode} mapId={mapId} />
</Route> </Route>
</Switch> </Switch>
</Router> </Router>

View File

@ -1,4 +1,3 @@
import { sessionExpired } from "../../redux/clientSlice";
import Client from "../client"; import Client from "../client";
import CacheDecoratorClient from "../client/cache-decorator-client"; import CacheDecoratorClient from "../client/cache-decorator-client";
import MockClient from "../client/mock-client"; import MockClient from "../client/mock-client";
@ -53,9 +52,7 @@ class _AppConfig {
const config = this.getInstance(); const config = this.getInstance();
let result: Client; let result: Client;
if (config.clientType == 'rest') { if (config.clientType == 'rest') {
result = new RestClient(config.apiBaseUrl, () => { result = new RestClient(config.apiBaseUrl);
sessionExpired();
});
console.log('Service using rest client. ' + JSON.stringify(config)); console.log('Service using rest client. ' + JSON.stringify(config));
} else { } else {
console.log('Warning:Service using mockservice client'); console.log('Warning:Service using mockservice client');

View File

@ -1,4 +1,4 @@
import { Mindmap } from '@wisemapping/mindplot'; import { Mindmap, PersistenceManager } from '@wisemapping/mindplot';
import Client, { import Client, {
AccountInfo, AccountInfo,
BasicMapInfo, BasicMapInfo,
@ -18,6 +18,10 @@ class CacheDecoratorClient implements Client {
this.client = client; this.client = client;
} }
onSessionExpired(callback?: () => void): () => void {
return this.client.onSessionExpired(callback);
}
fetchMindmap(id: number): Mindmap { fetchMindmap(id: number): Mindmap {
return this.client.fetchMindmap(id); return this.client.fetchMindmap(id);
} }
@ -125,6 +129,15 @@ class CacheDecoratorClient implements Client {
revertHistory(id: number, cid: number): Promise<void> { revertHistory(id: number, cid: number): Promise<void> {
return this.client.revertHistory(id, cid); return this.client.revertHistory(id, cid);
} }
buildPersistenceManager(): PersistenceManager {
return this.client.buildPersistenceManager();
}
removePersistenceManager(): void {
return this.client.removePersistenceManager();
}
} }
export default CacheDecoratorClient; export default CacheDecoratorClient;

View File

@ -45,7 +45,7 @@ const ClientHealthSentinel = (): React.ReactElement => {
<DialogActions> <DialogActions>
<Button type="button" color="primary" size="medium" onClick={handleOnClose}> <Button type="button" color="primary" size="medium" onClick={handleOnClose}>
<FormattedMessage id="action.close-button" defaultMessage="Close" /> <FormattedMessage id="login.signin" defaultMessage="Sign In" />
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -1,4 +1,4 @@
import { Mindmap } from '@wisemapping/mindplot'; import { Mindmap, PersistenceManager } from '@wisemapping/mindplot';
import { Locale, LocaleCode } from '../app-i18n'; import { Locale, LocaleCode } from '../app-i18n';
export type NewUser = { export type NewUser = {
@ -109,6 +109,10 @@ interface Client {
fetchMindmap(id:number): Mindmap; fetchMindmap(id:number): Mindmap;
buildPersistenceManager(): PersistenceManager;
removePersistenceManager(): void;
onSessionExpired(callback?: () => void): () => void;
} }
export default Client; export default Client;

View File

@ -0,0 +1,70 @@
<map name="3" version="tango">
<topic central="true" text="Welcome To WiseMapping" id="1" fontStyle=";;#ffffff;;;">
<icon id="sign_info"/>
<topic position="199,-112" order="0" id="30">
<text><![CDATA[5 min tutorial video ?
Follow the link !]]></text>
<link url="https://www.youtube.com/tv?vq=medium#/watch?v=rKxZwNKs9cE" urlType="url"/>
<icon id="hard_computer"/>
</topic>
<topic position="-167,-112" order="1" text="Try it Now!" id="11" fontStyle=";;#525c61;;;" bgColor="#250be3" brColor="#080559">
<icon id="face_surprise"/>
<topic position="-260,-141" order="0" text="Double Click" id="12" fontStyle=";;#525c61;;italic;"/>
<topic position="-278,-112" order="1" id="13">
<text><![CDATA[Press "enter" to add a
Sibling]]></text>
</topic>
<topic position="-271,-83" order="2" text="Drag map to move" id="14" fontStyle=";;#525c61;;italic;"/>
</topic>
<topic position="155,-18" order="2" text="Features" id="15" fontStyle=";;#525c61;;;">
<topic position="244,-80" order="0" text="Links to Sites" id="16" fontStyle=";6;#525c61;;;">
<link url="http://www.digg.com" urlType="url"/>
</topic>
<topic position="224,-30" order="1" text="Styles" id="31">
<topic position="285,-55" order="0" text="Fonts" id="17" fontStyle=";;#525c61;;;"/>
<topic position="299,-30" order="1" text="Topic Shapes" shape="line" id="19" fontStyle=";;#525c61;;;"/>
<topic position="295,-5" order="2" text="Topic Color" id="18" fontStyle=";;#525c61;;;"/>
</topic>
<topic position="229,20" order="2" text="Icons" id="20" fontStyle=";;#525c61;;;">
<icon id="object_rainbow"/>
</topic>
<topic position="249,45" order="3" text="History Changes" id="21" fontStyle=";;#525c61;;;">
<icon id="arrowc_turn_left"/>
</topic>
</topic>
<topic position="-176,-21" order="3" text="Mind Mapping" id="6" fontStyle=";;#525c61;;;" bgColor="#edabff">
<icon id="thumb_thumb_up"/>
<topic position="-293,-58" order="0" text="Share with Collegues" id="7" fontStyle=";;#525c61;;;"/>
<topic position="-266,-33" order="1" text="Online" id="8" fontStyle=";;#525c61;;;"/>
<topic position="-288,-8" order="2" text="Anyplace, Anytime" id="9" fontStyle=";;#525c61;;;"/>
<topic position="-266,17" order="3" text="Free!!!" id="10" fontStyle=";;#525c61;;;"/>
</topic>
<topic position="171,95" order="4" text="Productivity" id="2" fontStyle=";;#525c61;;;" bgColor="#d9b518">
<icon id="chart_bar"/>
<topic position="281,70" order="0" text="Share your ideas" id="3" fontStyle=";;#525c61;;;">
<icon id="bulb_light_on"/>
</topic>
<topic position="270,95" order="1" text="Brainstorming" id="4" fontStyle=";;#525c61;;;"/>
<topic position="256,120" order="2" text="Visual " id="5" fontStyle=";;#525c61;;;"/>
</topic>
<topic position="-191,54" order="5" text="Install In Your Server" id="27" fontStyle=";;#525c61;;;">
<icon id="hard_computer"/>
<topic position="-319,42" order="0" text="Open Source" id="29" fontStyle=";;#525c61;;;">
<icon id="soft_penguin"/>
<link url="http://www.wisemapping.org/" urlType="url"/>
</topic>
<topic position="-310,67" order="1" text="Download" id="28" fontStyle=";;#525c61;;;">
<link url="http://www.wisemapping.com/inyourserver.html" urlType="url"/>
</topic>
</topic>
<topic position="-169,117" order="7" text="Collaborate" id="32">
<icon id="people_group"/>
<topic position="-253,92" order="0" text="Embed" id="33"/>
<topic position="-254,117" order="1" text="Publish" id="34"/>
<topic position="-277,142" order="2" text="Share for Edition" id="35">
<icon id="mail_envelop"/>
</topic>
</topic>
</topic>
<relationship srcTopicId="30" destTopicId="11" lineType="3" srcCtrlPoint="-80,-56" destCtrlPoint="110,-116" endArrow="false" startArrow="true"/>
</map>

View File

@ -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 XMLSerializerTango from '@wisemapping/mindplot/src/components/persistence/XMLSerializerTango';
import Client, { import Client, {
AccountInfo, AccountInfo,
@ -11,6 +11,7 @@ import Client, {
Permission, Permission,
} from '..'; } from '..';
import { LocaleCode, localeFromStr } from '../../app-i18n'; import { LocaleCode, localeFromStr } from '../../app-i18n';
import exampleMap from './example-map.wxml';
const label1: Label = { const label1: Label = {
id: 1, id: 1,
@ -34,6 +35,7 @@ class MockClient implements Client {
private maps: MapInfo[] = []; private maps: MapInfo[] = [];
private labels: Label[] = []; private labels: Label[] = [];
private permissionsByMap: Map<number, Permission[]> = new Map(); private permissionsByMap: Map<number, Permission[]> = new Map();
private persistenceManager: PersistenceManager;
constructor() { constructor() {
// Remove, just for develop .... // Remove, just for develop ....
@ -110,6 +112,10 @@ class MockClient implements Client {
this.labels = [label1, label2, label3]; this.labels = [label1, label2, label3];
} }
onSessionExpired(callback?: () => void): () => void {
return callback;
}
fetchMindmap(id: number): Mindmap { fetchMindmap(id: number): Mindmap {
const parser = new DOMParser(); const parser = new DOMParser();
const xmlDoc = parser.parseFromString(` const xmlDoc = parser.parseFromString(`
@ -392,6 +398,21 @@ class MockClient implements Client {
console.log('email:' + email); console.log('email:' + email);
return Promise.resolve(); 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; export default MockClient;

View File

@ -1,5 +1,6 @@
import { LocalStorageManager, Mindmap } from '@wisemapping/mindplot'; import { LocalStorageManager, Mindmap, PersistenceManager, RESTPersistenceManager } from '@wisemapping/mindplot';
import axios from 'axios'; import { PersistenceError } from '@wisemapping/mindplot/src/components/PersistenceManager';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import Client, { import Client, {
ErrorInfo, ErrorInfo,
MapInfo, MapInfo,
@ -11,15 +12,46 @@ import Client, {
ImportMapInfo, ImportMapInfo,
Permission, Permission,
} from '..'; } from '..';
import { getCsrfToken } from '../../../utils';
import { LocaleCode, localeFromStr } from '../../app-i18n'; import { LocaleCode, localeFromStr } from '../../app-i18n';
export default class RestClient implements Client { export default class RestClient implements Client {
private baseUrl: string; private baseUrl: string;
private sessionExpired: () => void; private persistenceManager: PersistenceManager;
private axios: AxiosInstance;
constructor(baseUrl: string, sessionExpired: () => void) { private checkResponseForSessionExpired = <T>(error: { response?: AxiosResponse<T> }): Promise<{ response?: AxiosResponse<T> }> => {
// 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.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 { fetchMindmap(id: number): Mindmap {
@ -34,7 +66,7 @@ export default class RestClient implements Client {
deleteMapPermission(id: number, email: string): Promise<void> { deleteMapPermission(id: number, email: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.delete(`${this.baseUrl}/c/restful/maps/${id}/collabs?email=${encodeURIComponent(email)}`, { .delete(`${this.baseUrl}/c/restful/maps/${id}/collabs?email=${encodeURIComponent(email)}`, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -51,7 +83,7 @@ export default class RestClient implements Client {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
addMapPermissions(id: number, message: string, permissions: Permission[]): Promise<void> { addMapPermissions(id: number, message: string, permissions: Permission[]): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put( .put(
`${this.baseUrl}/c/restful/maps/${id}/collabs/`, `${this.baseUrl}/c/restful/maps/${id}/collabs/`,
{ {
@ -77,7 +109,7 @@ export default class RestClient implements Client {
success: (labels: Permission[]) => void, success: (labels: Permission[]) => void,
reject: (error: ErrorInfo) => void reject: (error: ErrorInfo) => void
) => { ) => {
axios this.axios
.get(`${this.baseUrl}/c/restful/maps/${id}/collabs`, { .get(`${this.baseUrl}/c/restful/maps/${id}/collabs`, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -104,7 +136,7 @@ export default class RestClient implements Client {
deleteAccount(): Promise<void> { deleteAccount(): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.delete(`${this.baseUrl}/c/restful/account`, { .delete(`${this.baseUrl}/c/restful/account`, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -121,12 +153,12 @@ export default class RestClient implements Client {
updateAccountInfo(firstname: string, lastname: string): Promise<void> { updateAccountInfo(firstname: string, lastname: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/c/restful/account/firstname`, firstname, { .put(`${this.baseUrl}/c/restful/account/firstname`, firstname, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
.then(() => { .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' }, headers: { 'Content-Type': 'text/plain' },
}); });
}) })
@ -144,7 +176,7 @@ export default class RestClient implements Client {
updateAccountPassword(pasword: string): Promise<void> { updateAccountPassword(pasword: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/c/restful/account/password`, pasword, { .put(`${this.baseUrl}/c/restful/account/password`, pasword, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -161,7 +193,7 @@ export default class RestClient implements Client {
updateAccountLanguage(locale: LocaleCode): Promise<void> { updateAccountLanguage(locale: LocaleCode): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/c/restful/account/locale`, locale, { .put(`${this.baseUrl}/c/restful/account/locale`, locale, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -179,7 +211,7 @@ export default class RestClient implements Client {
importMap(model: ImportMapInfo): Promise<number> { importMap(model: ImportMapInfo): Promise<number> {
const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.post( .post(
`${this.baseUrl}/c/restful/maps?title=${encodeURIComponent(model.title)}&description=${model.description ? model.description : '' `${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, success: (account: AccountInfo) => void,
reject: (error: ErrorInfo) => void reject: (error: ErrorInfo) => void
) => { ) => {
axios this.axios
.get(`${this.baseUrl}/c/restful/account`, { .get(`${this.baseUrl}/c/restful/account`, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -227,7 +259,7 @@ export default class RestClient implements Client {
deleteMaps(ids: number[]): Promise<void> { deleteMaps(ids: number[]): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.delete(`${this.baseUrl}/c/restful/maps/batch?ids=${ids.join()}`, { .delete(`${this.baseUrl}/c/restful/maps/batch?ids=${ids.join()}`, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -245,7 +277,7 @@ export default class RestClient implements Client {
updateMapToPublic(id: number, isPublic: boolean): Promise<void> { updateMapToPublic(id: number, isPublic: boolean): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/c/restful/maps/${id}/publish`, isPublic.toString(), { .put(`${this.baseUrl}/c/restful/maps/${id}/publish`, isPublic.toString(), {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -262,7 +294,7 @@ export default class RestClient implements Client {
revertHistory(id: number, hid: number): Promise<void> { revertHistory(id: number, hid: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.post(`${this.baseUrl}/c/restful/maps/${id}/history/${hid}`, null, { .post(`${this.baseUrl}/c/restful/maps/${id}/history/${hid}`, null, {
headers: { 'Content-Type': 'text/pain' }, headers: { 'Content-Type': 'text/pain' },
}) })
@ -282,7 +314,7 @@ export default class RestClient implements Client {
success: (historyList: ChangeHistory[]) => void, success: (historyList: ChangeHistory[]) => void,
reject: (error: ErrorInfo) => void reject: (error: ErrorInfo) => void
) => { ) => {
axios this.axios
.get(`${this.baseUrl}/c/restful/maps/${id}/history/`, { .get(`${this.baseUrl}/c/restful/maps/${id}/history/`, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -307,12 +339,12 @@ export default class RestClient implements Client {
renameMap(id: number, basicInfo: BasicMapInfo): Promise<void> { renameMap(id: number, basicInfo: BasicMapInfo): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/c/restful/maps/${id}/title`, basicInfo.title, { .put(`${this.baseUrl}/c/restful/maps/${id}/title`, basicInfo.title, {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
.then(() => { .then(() => {
return axios.put( return this.axios.put(
`${this.baseUrl}/c/restful/maps/${id}/description`, basicInfo.description || ' ', `${this.baseUrl}/c/restful/maps/${id}/description`, basicInfo.description || ' ',
{ headers: { 'Content-Type': 'text/plain' } } { headers: { 'Content-Type': 'text/plain' } }
); );
@ -332,7 +364,7 @@ export default class RestClient implements Client {
createMap(model: BasicMapInfo): Promise<number> { createMap(model: BasicMapInfo): Promise<number> {
const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.post( .post(
`${this.baseUrl}/c/restful/maps?title=${model.title}&description=${model.description ? model.description : '' `${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, success: (mapsInfo: MapInfo[]) => void,
reject: (error: ErrorInfo) => void reject: (error: ErrorInfo) => void
) => { ) => {
axios this.axios
.get(`${this.baseUrl}/c/restful/maps/`, { .get(`${this.baseUrl}/c/restful/maps/`, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -390,7 +422,7 @@ export default class RestClient implements Client {
registerNewUser(user: NewUser): Promise<void> { registerNewUser(user: NewUser): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.post(`${this.baseUrl}/service/users/`, JSON.stringify(user), { .post(`${this.baseUrl}/service/users/`, JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -408,7 +440,7 @@ export default class RestClient implements Client {
deleteMap(id: number): Promise<void> { deleteMap(id: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.delete(`${this.baseUrl}/c/restful/maps/${id}`, { .delete(`${this.baseUrl}/c/restful/maps/${id}`, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -425,7 +457,7 @@ export default class RestClient implements Client {
resetPassword(email: string): Promise<void> { resetPassword(email: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/service/users/resetPassword?email=${encodeURIComponent(email)}`, null, { .put(`${this.baseUrl}/service/users/resetPassword?email=${encodeURIComponent(email)}`, null, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -444,7 +476,7 @@ export default class RestClient implements Client {
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<number> { duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<number> {
const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.post(`${this.baseUrl}/c/restful/maps/${id}`, JSON.stringify(basicInfo), { .post(`${this.baseUrl}/c/restful/maps/${id}`, JSON.stringify(basicInfo), {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -462,9 +494,8 @@ export default class RestClient implements Client {
} }
updateStarred(id: number, starred: boolean): Promise<void> { updateStarred(id: number, starred: boolean): Promise<void> {
console.debug(`Starred => ${starred}`)
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.put(`${this.baseUrl}/c/restful/maps/${id}/starred`, starred.toString(), { .put(`${this.baseUrl}/c/restful/maps/${id}/starred`, starred.toString(), {
headers: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'text/plain' },
}) })
@ -485,7 +516,7 @@ export default class RestClient implements Client {
success: (labels: Label[]) => void, success: (labels: Label[]) => void,
reject: (error: ErrorInfo) => void reject: (error: ErrorInfo) => void
) => { ) => {
axios this.axios
.get(`${this.baseUrl}/c/restful/labels/`, { .get(`${this.baseUrl}/c/restful/labels/`, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -512,7 +543,7 @@ export default class RestClient implements Client {
createLabel(title: string, color: string): Promise<number> { createLabel(title: string, color: string): Promise<number> {
const handler = (success: (labelId: number) => void, reject: (error: ErrorInfo) => void) => { 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' }), { .post(`${this.baseUrl}/c/restful/labels`, JSON.stringify({ title, color, iconName: 'smile' }), {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -529,7 +560,7 @@ export default class RestClient implements Client {
deleteLabel(id: number): Promise<void> { deleteLabel(id: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.delete(`${this.baseUrl}/c/restful/labels/${id}`) .delete(`${this.baseUrl}/c/restful/labels/${id}`)
.then(() => { .then(() => {
success(); success();
@ -544,7 +575,7 @@ export default class RestClient implements Client {
addLabelToMap(labelId: number, mapId: number): Promise<void> { addLabelToMap(labelId: number, mapId: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), { .post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })
@ -561,7 +592,7 @@ export default class RestClient implements Client {
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> { deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios this.axios
.delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`) .delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`)
.then(() => { .then(() => {
success(); success();
@ -574,6 +605,45 @@ export default class RestClient implements Client {
return new Promise(handler); 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
private parseResponseOnError = (response: any): ErrorInfo => { private parseResponseOnError = (response: any): ErrorInfo => {
console.error("Backend error=>"); console.error("Backend error=>");

View File

@ -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<T>(Component: ComponentType<T>) {
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 (
<>
<ClientHealthSentinel />
<Component {...hocProps} />;
</>
);
};
}
export default withSessionExpirationHandling;

View File

@ -6,6 +6,9 @@ import AppI18n from '../../classes/app-i18n';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { hotkeysEnabled } from '../../redux/editorSlice'; import { hotkeysEnabled } from '../../redux/editorSlice';
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import Client from '../../classes/client';
import { activeInstance } from '../../redux/clientSlice';
import { PersistenceManager } from '@wisemapping/mindplot';
export type EditorPropsType = { export type EditorPropsType = {
mapId: number; mapId: number;
@ -16,13 +19,24 @@ const EditorPage = ({ mapId, ...props }: EditorPropsType): React.ReactElement =>
const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null); const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null);
const hotkeys = useSelector(hotkeysEnabled); const hotkeys = useSelector(hotkeysEnabled);
const userLocale = AppI18n.getUserLocale(); const userLocale = AppI18n.getUserLocale();
const client: Client = useSelector(activeInstance);
const [persistenceManager, setPersistenceManager] = React.useState<PersistenceManager>();
useEffect(() => { useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search); 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 <> return <>
<Editor {...props} onAction={setActiveDialog} locale={userLocale.code} hotkeys={hotkeys} /> <Editor {...props} onAction={setActiveDialog}
locale={userLocale.code} hotkeys={hotkeys}
persistenceManager={persistenceManager} />
{ {
activeDialog && activeDialog &&
<ActionDispatcher <ActionDispatcher

View File

@ -15,6 +15,7 @@ import SubmitButton from '../form/submit-button';
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { getCsrfToken, getCsrfTokenParameter } from '../../utils';
const ForgotPassword = () => { const ForgotPassword = () => {
const [email, setEmail] = useState<string>(''); const [email, setEmail] = useState<string>('');
@ -54,6 +55,7 @@ const ForgotPassword = () => {
<GlobalError error={error} /> <GlobalError error={error} />
<form onSubmit={handleOnSubmit}> <form onSubmit={handleOnSubmit}>
<input type='hidden' value={getCsrfToken()} name={getCsrfTokenParameter()} />
<Input <Input
type="email" type="email"
name="email" name="email"

View File

@ -11,6 +11,7 @@ import Typography from '@mui/material/Typography';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import { getCsrfToken, getCsrfTokenParameter } from '../../utils';
type ConfigStatusProps = { type ConfigStatusProps = {
enabled?: boolean; enabled?: boolean;
@ -89,6 +90,7 @@ const LoginPage = (): React.ReactElement => {
<FormControl> <FormControl>
<form action="/c/perform-login" method="POST"> <form action="/c/perform-login" method="POST">
<input type='hidden' value={getCsrfToken()} name={getCsrfTokenParameter()}/>
<Input <Input
name="username" name="username"
type="email" type="email"

View File

@ -54,7 +54,7 @@ const AccountInfoDialog = ({ onClose }: AccountInfoDialogProps): React.ReactElem
}, },
{ {
onSuccess: () => { onSuccess: () => {
window.location.href = '/c/logout'; window.location.href = '/c/login';
onClose(); onClose();
}, },
onError: (error) => { onError: (error) => {

View File

@ -28,6 +28,12 @@ const AccountMenu = (): React.ReactElement => {
setAnchorEl(null); setAnchorEl(null);
}; };
const handleLogout = (event: MouseEvent) => {
event.preventDefault();
const elem = document.getElementById('logoutFrom') as HTMLFormElement;
elem.submit();
};
const account = fetchAccount(); const account = fetchAccount();
return ( return (
<span> <span>
@ -77,7 +83,8 @@ const AccountMenu = (): React.ReactElement => {
</MenuItem> </MenuItem>
<MenuItem onClick={handleClose}> <MenuItem onClick={handleClose}>
<Link color="textSecondary" href="/c/logout"> <form action="/c/logout" method='POST' id="logoutFrom"></form>
<Link color="textSecondary" href="/c/logout" onClick={(e) => handleLogout(e)}>
<ListItemIcon> <ListItemIcon>
<ExitToAppOutlined fontSize="small" /> <ExitToAppOutlined fontSize="small" />
</ListItemIcon> </ListItemIcon>
@ -85,11 +92,13 @@ const AccountMenu = (): React.ReactElement => {
</Link> </Link>
</MenuItem> </MenuItem>
</Menu> </Menu>
{action == 'change-password' && ( {
action == 'change-password' && (
<ChangePasswordDialog onClose={() => setAction(undefined)} /> <ChangePasswordDialog onClose={() => setAction(undefined)} />
)} )
}
{action == 'account-info' && <AccountInfoDialog onClose={() => setAction(undefined)} />} {action == 'account-info' && <AccountInfoDialog onClose={() => setAction(undefined)} />}
</span> </span >
); );
}; };

View File

@ -15,7 +15,6 @@ import Client, { Label } from '../../classes/client';
import ActionDispatcher from './action-dispatcher'; import ActionDispatcher from './action-dispatcher';
import { ActionType } from './action-chooser'; import { ActionType } from './action-chooser';
import AccountMenu from './account-menu'; import AccountMenu from './account-menu';
import ClientHealthSentinel from '../../classes/client/client-health-sentinel';
import HelpMenu from './help-menu'; import HelpMenu from './help-menu';
import LanguageMenu from './language-menu'; import LanguageMenu from './language-menu';
import AppI18n, { Locales } from '../../classes/app-i18n'; import AppI18n, { Locales } from '../../classes/app-i18n';
@ -153,7 +152,6 @@ const MapsPage = (): ReactElement => {
messages={userLocale.message} messages={userLocale.message}
> >
<div className={classes.root}> <div className={classes.root}>
<ClientHealthSentinel />
<AppBar <AppBar
position="fixed" position="fixed"
className={clsx(classes.appBar, { className={clsx(classes.appBar, {

View File

@ -0,0 +1,15 @@
export const getCsrfToken = (): string | null => {
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');
};

View File

@ -30,6 +30,10 @@ module.exports = {
test: /\.(png|jpe?g|gif|svg)$/, test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/inline', type: 'asset/inline',
}, },
{
test: /\.wxml$/i,
type: 'asset/source',
},
], ],
}, },
optimization: { optimization: {