From de83274cdbaf048934b49788947b559483af5eaf Mon Sep 17 00:00:00 2001 From: louisduhalberruer Date: Wed, 19 Jun 2024 14:27:29 +0200 Subject: [PATCH] FIX: Auth session should be persisted after refresh (closes #163) --- frontend/src/components/Menu.js | 70 ++++++++++++++++++ frontend/src/hyperglosae.js | 127 ++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 frontend/src/components/Menu.js create mode 100644 frontend/src/hyperglosae.js diff --git a/frontend/src/components/Menu.js b/frontend/src/components/Menu.js new file mode 100644 index 00000000..5c41680e --- /dev/null +++ b/frontend/src/components/Menu.js @@ -0,0 +1,70 @@ +import '../styles/Menu.css'; + +import Navbar from 'react-bootstrap/Navbar'; +import Container from 'react-bootstrap/Container'; +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; +import Form from 'react-bootstrap/Form'; +import { Link } from 'react-router-dom'; +import { useState } from 'react'; + +function Menu({backend}) { + return ( + + + + + Index + + + + + + ); +} + +function Authentication({backend}) { + const [credentials, setCredentials] = useState({ + ...backend.credentials + }); + + let handleSubmit = (e) => { + e.preventDefault(); + let credentials = Object.fromEntries(new FormData(e.target).entries()); + backend.authenticate(credentials) + .then(() => setCredentials(credentials)); + }; + + if (credentials.name) return ( + + {credentials.name} + + ); + return ( +
+ + + + + + + + + + + +
+ ); +} + +export default Menu; diff --git a/frontend/src/hyperglosae.js b/frontend/src/hyperglosae.js new file mode 100644 index 00000000..dc0b564d --- /dev/null +++ b/frontend/src/hyperglosae.js @@ -0,0 +1,127 @@ +import {Buffer} from 'buffer'; + +const service = 'http://localhost:5984/hyperglosae'; + +const LOCALSTORAGE_BASIC_AUTH_KEY = 'hyperglosae-basic-auth'; + +function Hyperglosae(logger) { + this.credentials = {}; + + let persistBasicAuth = () => { + if (!this.credentials) return; + localStorage.setItem(LOCALSTORAGE_BASIC_AUTH_KEY, JSON.stringify(this.credentials)); + }; + + let retrieveBasicAuth = () => { + if (localStorage.getItem(LOCALSTORAGE_BASIC_AUTH_KEY)) { + try { + const basicAuth = JSON.parse(localStorage.getItem(LOCALSTORAGE_BASIC_AUTH_KEY)); + this.credentials = {...this.credentials, ...basicAuth}; + } catch { + console.error('Unable to parse basic auth'); + localStorage.removeItem(LOCALSTORAGE_BASIC_AUTH_KEY); + } + } + }; + + retrieveBasicAuth(); + + this.getView = ({view, id, options = []}) => + fetch(`${ + service + }/_design/app/_view/${ + view + }?${ + id ? `startkey=["${id}"]&endkey=["${id}",{}]` : '' + }&${ + options.map(x => x + '=true').join('&') + }`) + .then(x => x.json()) + .then(x => x.rows); + + this.getDocument = (id) => + fetch(`${service}/${id}`) + .then(x => x.json()); + + let basicAuthentication = ({force}) => { + retrieveBasicAuth(); + let { base64} = this.credentials; + if (!force && !base64) return ({}); + return ({ + 'Authorization': 'Basic ' + base64 + }); + }; + + this.putDocument = (doc) => + fetch(`${service}/${doc._id}`, { + method: 'PUT', + headers: basicAuthentication({force: false}), + body: JSON.stringify(doc) + }) + .then(x => x.json()) + .then(x => { + if (x.reason) { + logger(x.reason); + throw new Error(x.reason); + } + }); + + this.authenticate = ({name, password}) => { + this.credentials = { + base64: Buffer.from(`${name}:${password}`).toString('base64'), + name + }; + persistBasicAuth(); + + return fetch(`${service}`, { + method: 'GET', + headers: basicAuthentication({force: true}) + }) + .then(x => x.json()) + .then(x => { + if (x.reason) { + this.credentials = {}; + localStorage.removeItem(LOCALSTORAGE_BASIC_AUTH_KEY); + logger(x.reason); + throw new Error(x.reason); + } + }); + }; + + this.refreshMetadata = (id, callback) => { + this.getView({view: 'metadata', id, options: ['include_docs']}) + .then( + (rows) => { + let documents = rows.map(x => x.doc); + callback(documents); + } + ); + }; + + this.refreshContent = (id, callback) => { + this.getView({view: 'content', id, options: ['include_docs']}) + .then( + (rows) => { + callback(rows); + }, + (error) => { + console.log(error.message); + } + ); + }; + + this.refreshDocuments = (callback) => { + this.getView({view: 'all_documents', options: ['group']}) + .then((rows) => { + callback( + rows.map( + ({value}) => ({...value.metadata, referenced: value.referenced}) + ) + ); + }); + }; + + return this; +} + +export default Hyperglosae;