|
| 1 | +/* Profile Editing Pane |
| 2 | +** |
| 3 | +** Unlike most panes, this is available any place whatever the real subject, |
| 4 | +** and allows the user to edit their own profile. |
| 5 | +** |
| 6 | +** Usage: paneRegistry.register('profile/profilePane') |
| 7 | +** or standalone script adding onto existing mashlib. |
| 8 | +*/ |
| 9 | + |
| 10 | +const nodeMode = (typeof module !== 'undefined') |
| 11 | +var panes, UI |
| 12 | + |
| 13 | +if (nodeMode) { |
| 14 | + UI = require('solid-ui') |
| 15 | + panes = require('pane-registry') |
| 16 | +} else { // Add to existing mashlib |
| 17 | + panes = window.panes |
| 18 | + UI = panes.UI |
| 19 | +} |
| 20 | + |
| 21 | +const kb = UI.store |
| 22 | +const ns = UI.ns |
| 23 | + |
| 24 | +const thisColor = '#418d99' |
| 25 | + |
| 26 | +const thisPane = { |
| 27 | + icon: UI.icons.iconBase + 'noun_15177.svg', // Looks like an A - could say it's for Applications? |
| 28 | + |
| 29 | + name: 'trustedApplications', |
| 30 | + |
| 31 | + label: function (subject) { |
| 32 | + var types = kb.findTypeURIs(subject) |
| 33 | + if (types[UI.ns.foaf('Person').uri] || types[UI.ns.vcard('Individual').uri]) { |
| 34 | + return 'Manage your trusted applications' |
| 35 | + } |
| 36 | + return null |
| 37 | + }, |
| 38 | + |
| 39 | + render: function (subject, dom) { |
| 40 | + var div = dom.createElement('div') |
| 41 | + div.classList.add('trusted-applications-pane') |
| 42 | + div.setAttribute('style', 'border: 0.3em solid ' + thisColor + '; border-radius: 0.5em; padding: 0.7em; margin-top:0.7em;') |
| 43 | + var table = div.appendChild(dom.createElement('table')) |
| 44 | + var main = table.appendChild(dom.createElement('tr')) |
| 45 | + var bottom = table.appendChild(dom.createElement('tr')) |
| 46 | + var statusArea = bottom.appendChild(dom.createElement('div')) |
| 47 | + statusArea.setAttribute('style', 'padding: 0.7em;') |
| 48 | + |
| 49 | + var context = { dom: dom, div: main, statusArea: statusArea, me: null } |
| 50 | + UI.authn.logInLoadProfile(context).then(context => { |
| 51 | + subject = context.me |
| 52 | + |
| 53 | + var profile = subject.doc() |
| 54 | + var editable = UI.store.updater.editable(profile.uri, kb) |
| 55 | + |
| 56 | + main.appendChild(createText('h3', 'Manage your trusted applications')) |
| 57 | + |
| 58 | + if (!editable) { |
| 59 | + main.appendChild(UI.widgets.errorMessageBlock(dom, `Your profile ${subject.doc().uri} is not editable, so we cannot do much here.`)) |
| 60 | + return |
| 61 | + } |
| 62 | + |
| 63 | + main.appendChild(createText('p', 'Here you can manage the applications you trust.')) |
| 64 | + |
| 65 | + const applicationsTable = createApplicationTable(subject) |
| 66 | + main.appendChild(applicationsTable) |
| 67 | + |
| 68 | + main.appendChild(createText('h4', 'Notes')) |
| 69 | + main.appendChild(createContainer('ol', [ |
| 70 | + main.appendChild(createText('li', 'Trusted applications will get access to all resources that you have access to.')), |
| 71 | + main.appendChild(createText('li', 'You can limit which modes they have by default.')), |
| 72 | + main.appendChild(createText('li', 'They will not gain more access than you have.')) |
| 73 | + ])) |
| 74 | + main.appendChild(createText('p', 'Application URLs must be valid URL. Examples are http://localhost:3000, https://trusted.app, and https://sub.trusted.app.')) |
| 75 | + }, err => { |
| 76 | + statusArea.appendChild(UI.widgets.errorMessageBlock(dom, err)) |
| 77 | + }) |
| 78 | + return div |
| 79 | + } // render() |
| 80 | +} // |
| 81 | + |
| 82 | +function createApplicationTable(subject) { |
| 83 | + var applicationsTable = createElement('table', { |
| 84 | + 'class': 'results' |
| 85 | + }) |
| 86 | + |
| 87 | + // creating headers |
| 88 | + var header = createContainer('tr', [ |
| 89 | + createText('th', 'Application URL'), |
| 90 | + createText('th', 'Access modes'), |
| 91 | + createText('th', 'Actions') |
| 92 | + ]) |
| 93 | + applicationsTable.appendChild(header) |
| 94 | + |
| 95 | + // creating rows |
| 96 | + kb.each(subject, ns.acl('trustedApp')) |
| 97 | + .flatMap(app => kb.each(app, ns.acl('origin')).map(origin => ({appModes: kb.each(app, ns.acl('mode')), origin}))) |
| 98 | + .sort(({origin: a}, {origin: b}) => a.value < b.value ? -1 : 1) |
| 99 | + .forEach(({appModes, origin}) => applicationsTable.appendChild(createApplicationEntry(subject, origin, appModes, updateTable))) |
| 100 | + |
| 101 | + // adding a row for new applications |
| 102 | + applicationsTable.appendChild(createApplicationEntry(subject, null, [ns.acl('Read')], updateTable)) |
| 103 | + |
| 104 | + return applicationsTable |
| 105 | + |
| 106 | + function updateTable() { |
| 107 | + applicationsTable.parentElement.replaceChild(createApplicationTable(subject), applicationsTable) |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +function createApplicationEntry(subject, origin, appModes, updateTable) { |
| 112 | + var trustedApplicationState = { origin, appModes, formElements: { modes: [] } } |
| 113 | + return createContainer('tr', [ |
| 114 | + createContainer('td', [ |
| 115 | + createElement('input', { |
| 116 | + 'class': 'textinput', |
| 117 | + placeholder: 'Write new URL here', |
| 118 | + value: origin ? origin.value : '' |
| 119 | + }, {}, (element) => trustedApplicationState.formElements.origin = element) |
| 120 | + ]), |
| 121 | + createContainer('td', createModesInput(trustedApplicationState)), |
| 122 | + createContainer('td', origin |
| 123 | + ? [ |
| 124 | + createText('button', 'Update', { |
| 125 | + 'class': 'controlButton', |
| 126 | + style: 'background: LightGreen;' |
| 127 | + }, { |
| 128 | + click: () => addOrEditApplication() |
| 129 | + }), |
| 130 | + createText('button', 'Delete', { |
| 131 | + 'class': 'controlButton', |
| 132 | + style: 'background: LightCoral;' |
| 133 | + }, { |
| 134 | + click: () => removeApplication() |
| 135 | + }) |
| 136 | + ] |
| 137 | + : [ |
| 138 | + createText('button', 'Add', { |
| 139 | + 'class': 'controlButton', |
| 140 | + style: 'background: LightGreen;' |
| 141 | + }, { |
| 142 | + click: () => addOrEditApplication() |
| 143 | + }) |
| 144 | + ]) |
| 145 | + ]) |
| 146 | + |
| 147 | + function addOrEditApplication() { |
| 148 | + let origin |
| 149 | + try { |
| 150 | + origin = $rdf.sym(trustedApplicationState.formElements.origin.value) |
| 151 | + } catch (err) { |
| 152 | + return alert('Please provide an application URL you want to trust') |
| 153 | + } |
| 154 | + |
| 155 | + // remove existing statements on same origin - if it exists |
| 156 | + kb.statementsMatching(null, ns.acl('origin'), origin).forEach(st => { |
| 157 | + kb.removeStatements([...kb.statementsMatching(null, ns.acl('trustedApp'), st.subject)]) |
| 158 | + kb.removeStatements([...kb.statementsMatching(st.subject)]) |
| 159 | + }) |
| 160 | + |
| 161 | + // add new triples |
| 162 | + const application = new $rdf.BlankNode() |
| 163 | + kb.add(subject, ns.acl('trustedApp'), application, subject) |
| 164 | + kb.add(application, ns.acl('origin'), origin, subject) |
| 165 | + trustedApplicationState.formElements.modes |
| 166 | + .filter(checkbox => checkbox.checked) |
| 167 | + .map(checkbox => $rdf.sym(checkbox.value)) |
| 168 | + .forEach(mode => { |
| 169 | + kb.add(application, ns.acl('mode'), mode) |
| 170 | + }) |
| 171 | + |
| 172 | + save() |
| 173 | + } |
| 174 | + |
| 175 | + function removeApplication() { |
| 176 | + let origin |
| 177 | + try { |
| 178 | + origin = $rdf.sym(trustedApplicationState.formElements.origin.value) |
| 179 | + } catch (err) { |
| 180 | + return alert('Please provide an application URL you want to trust') |
| 181 | + } |
| 182 | + |
| 183 | + // remove existing statements on same origin - if it exists |
| 184 | + kb.statementsMatching(null, ns.acl('origin'), origin).forEach(st => { |
| 185 | + kb.removeStatements([...kb.statementsMatching(null, ns.acl('trustedApp'), st.subject)]) |
| 186 | + kb.removeStatements([...kb.statementsMatching(st.subject)]) |
| 187 | + }) |
| 188 | + |
| 189 | + save() |
| 190 | + } |
| 191 | + |
| 192 | + function save() { |
| 193 | + // remove response triples - this should not be necessary, but do not know how to turn it off |
| 194 | + kb.statementsMatching(null, ns.link('response')).forEach(st => { |
| 195 | + kb.removeStatements([...kb.statementsMatching(st.subject)]) |
| 196 | + kb.removeStatements([...kb.statementsMatching(st.object)]) |
| 197 | + }) |
| 198 | + |
| 199 | + // serialize data |
| 200 | + $rdf.serialize(null, kb, subject.uri, 'text/turtle', (err, data) => { |
| 201 | + if (err) { |
| 202 | + alert('Something went wrong when preparing data for the server. Try again.') |
| 203 | + return |
| 204 | + } |
| 205 | + // save data to POD |
| 206 | + kb.fetcher.webOperation('PUT', subject.uri, { |
| 207 | + data, |
| 208 | + saveMetadata: false, |
| 209 | + contentType: 'text/turtle' |
| 210 | + }) |
| 211 | + .then(() => updateTable()) |
| 212 | + }) |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +function createElement(elementName, attributes = {}, eventListeners = {}, onCreated = null) { |
| 217 | + var element = document.createElement(elementName) |
| 218 | + if (onCreated) { |
| 219 | + onCreated(element) |
| 220 | + } |
| 221 | + Object.keys(attributes).forEach(attName => { |
| 222 | + element.setAttribute(attName, attributes[attName]) |
| 223 | + }) |
| 224 | + Object.keys(eventListeners).forEach(eventName => { |
| 225 | + element.addEventListener(eventName, eventListeners[eventName]) |
| 226 | + }) |
| 227 | + return element |
| 228 | +} |
| 229 | + |
| 230 | +function createContainer(elementName, children, attributes = {}, eventListeners = {}, onCreated = null) { |
| 231 | + var element = createElement(elementName, attributes, eventListeners, onCreated) |
| 232 | + children.forEach(child => element.appendChild(child)) |
| 233 | + return element |
| 234 | +} |
| 235 | + |
| 236 | +function createText(elementName, textContent, attributes = {}, eventListeners = {}, onCreated = null) { |
| 237 | + var element = createElement(elementName, attributes, eventListeners, onCreated) |
| 238 | + element.textContent = textContent |
| 239 | + return element |
| 240 | +} |
| 241 | + |
| 242 | +function createModesInput({ appModes, formElements }) { |
| 243 | + return ['Read', 'Write', 'Append', 'Control'].map(mode => { |
| 244 | + var isChecked = appModes.some(appMode => appMode.value === ns.acl(mode).value) |
| 245 | + return createContainer('label', [ |
| 246 | + createElement('input', { |
| 247 | + type: 'checkbox', |
| 248 | + ...(isChecked ? { checked: '' } : {}), |
| 249 | + value: ns.acl(mode).uri |
| 250 | + }, {}, (element) => formElements.modes.push(element)), |
| 251 | + createText('span', mode) |
| 252 | + ]) |
| 253 | + }) |
| 254 | +} |
| 255 | + |
| 256 | +if (nodeMode) { |
| 257 | + module.exports = thisPane |
| 258 | +} else { |
| 259 | + console.log('*** patching in live pane: ' + thisPane.name) |
| 260 | + panes.register(thisPane) |
| 261 | +} |
| 262 | +// ENDS |
0 commit comments