Skip to content

Commit

Permalink
Merge pull request #1392 from cityremade/user_update_endpoint
Browse files Browse the repository at this point in the history
Changes required to implement user update with payload;
  • Loading branch information
RobAndrewHurst authored Aug 19, 2024
2 parents 201b5f7 + f009234 commit 1425188
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 107 deletions.
9 changes: 2 additions & 7 deletions express.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ app.use(process.env.DIR || '', express.static('tests'))

app.use(cookieParser())

const _api = require('./api/api')

const api = (req, res) => _api(req, res)

const api = require('./api/api')

app.get(`${process.env.DIR || ''}/api/provider/:provider?`, api)

Expand All @@ -49,9 +46,7 @@ app.get(`${process.env.DIR || ''}/api/workspace/:key?`, api)

app.get(`${process.env.DIR || ''}/api/user/:method?/:key?`, api)

app.post(`${process.env.DIR || ''}/api/user/:method?/:key?`, express.urlencoded({ extended: true }), api)

//sudo ./caddy_linux_amd64 reverse-proxy --from localhost:443 --to localhost:3000
app.post(`${process.env.DIR || ''}/api/user/:method?`, [express.urlencoded({ extended: true }), express.json({ limit: '5mb' })], api)

app.get(`${process.env.DIR || ''}/saml/metadata`, api)

Expand Down
5 changes: 4 additions & 1 deletion lib/ui/elements/pills.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ function add(val) {
data-value=${val}
title=${mapp.dictionary.pill_component_remove}
class="primary-background"
onclick=${e => component.remove(val)}
onclick=${e => {
e.stopPropagation();
component.remove(val)
}}
>✕`)

if (!component.pills.has(val)) component.pills.add(val); // add to selection
Expand Down
16 changes: 8 additions & 8 deletions lib/utils/_utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

// import from node modules
import {render, html, svg} from 'uhtml'
import { render, html, svg } from 'uhtml'

import * as stats from 'simple-statistics'

Expand All @@ -31,11 +31,11 @@ const compose = (...fns) => {

import convert from './convert.mjs'

import {copyToClipboard} from './copyToClipboard.mjs'
import { copyToClipboard } from './copyToClipboard.mjs'

import {dataURLtoBlob} from './dataURLtoBlob.mjs'
import { dataURLtoBlob } from './dataURLtoBlob.mjs'

import {default as hexa} from './hexa.mjs'
import { default as hexa } from './hexa.mjs'

import loadPlugins from './loadPlugins.mjs'

Expand All @@ -45,7 +45,7 @@ import merge from './merge.mjs'

import paramString from './paramString.mjs'

import {polygonIntersectFeatures} from './polygonIntersectFeatures.mjs'
import { polygonIntersectFeatures } from './polygonIntersectFeatures.mjs'

import promiseAll from './promiseAll.mjs'

Expand All @@ -59,9 +59,9 @@ import * as userIndexedDB from './userIndexedDB.mjs'

import * as gazetteer from './gazetteer.mjs'

import {default as verticeGeoms} from './verticeGeoms.mjs'
import { default as verticeGeoms } from './verticeGeoms.mjs'

import xhr from './xhr.mjs'
import { xhr } from './xhr.mjs'

import { formatNumericValue, unformatStringValue } from './numericFormatter.mjs';

Expand All @@ -83,7 +83,7 @@ export default {
hexa,
loadPlugins,
merge,
olScript: ()=>mapp.ol.load(),
olScript: () => mapp.ol.load(),
paramString,
polygonIntersectFeatures,
promiseAll,
Expand Down
85 changes: 44 additions & 41 deletions lib/utils/xhr.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,61 +29,64 @@ The method is assumed to be 'POST' if a params.body is provided.

const requestMap = new Map()

export default params => new Promise(resolve => {
export function xhr(params) {

// Return if params are falsy.
if (!params) {
console.error(`xhr params are falsy.`)
return;
}
return new Promise(resolve => {

// Set params as object with url from string.
params = typeof params === 'string' ? { url: params } : params
// Return if params are falsy.
if (!params) {
console.error(`xhr params are falsy.`)
return;
}

// A request url must be provided.
if (!params.url) {
console.error(`no xhr request url has been provided.`)
return;
};
// Set params as object with url from string.
params = typeof params === 'string' ? { url: params } : params

// Check whether a request with the same params has already been resolved.
if (params.cache && requestMap.has(params)) return resolve(requestMap.get(params))
// A request url must be provided.
if (!params.url) {
console.error(`no xhr request url has been provided.`)
return;
};

// Assign 'GET' as default method if no body is provided.
params.method ??= params.body ? 'POST' : 'GET'
// Check whether a request with the same params has already been resolved.
if (params.cache && requestMap.has(params)) return resolve(requestMap.get(params))

const xhr = new XMLHttpRequest()
// Assign 'GET' as default method if no body is provided.
params.method ??= params.body ? 'POST' : 'GET'

xhr.open(params.method, params.url)
const xhr = new XMLHttpRequest()

// Use requestHeader: null to prevent assignment of requestHeader.
if (params.requestHeader !== null) {
xhr.open(params.method, params.url)

// Butter (spread) over requestHeader.
const requestHeader = {
'Content-Type': 'application/json',
...params.requestHeader
}
// Use requestHeader: null to prevent assignment of requestHeader.
if (params.requestHeader !== null) {

Object.entries(requestHeader).forEach(entry => xhr.setRequestHeader(...entry))
}
// Butter (spread) over requestHeader.
const requestHeader = {
'Content-Type': 'application/json',
...params.requestHeader
}

xhr.responseType = params.responseType || 'json'
Object.entries(requestHeader).forEach(entry => xhr.setRequestHeader(...entry))
}

xhr.onload = e => {
xhr.responseType = params.responseType || 'json'

if (e.target.status >= 400) {
resolve(new Error(e.target.status))
return;
}
xhr.onload = e => {

// Cache the response in the requestMap
params.cache && requestMap.set(params, e.target.response)
if (e.target.status >= 400) {
resolve(new Error(e.target.status))
return;
}

resolve(params.resolveTarget ? e.target : e.target.response)
}
// Cache the response in the requestMap
params.cache && requestMap.set(params, e.target.response)

resolve(params.resolveTarget ? e.target : e.target.response)
}

xhr.onerror = (e) => resolve(new Error(e))
xhr.onerror = (e) => resolve(new Error(e))

xhr.send(params.body)
})
xhr.send(params.body)
})
}
137 changes: 87 additions & 50 deletions mod/user/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,92 +17,129 @@ const mailer = require('../utils/mailer')
@function update
@description
The update method will send a request to the ACL to set param.field = param.value for the param.email user record.
The update method will send a request to the ACL to update a user record in the ACL.
@param {Object} req HTTP request.
@param {Object} res HTTP response.
@param {Object} req.params
Request parameter.
@param {string} req.params.email
The user object to update can be provided as request body.
Property values to be updated must be provided as a substitutes array to prevent SQL injection.
The update_user keys must be validated to contain white listed characters only to prevent SQL injection.
@param {req} req HTTP request.
@param {req} res HTTP response.
@property {Object} [req.body]
HTTP Post request body containing the update information.
@property {Object} req.params
HTTP request parameter.
@property {string} params.email
User to update
@param {string} req.params.field
@property {string} params.field
User record field to update
@param {string} req.params.value
@property {string} params.value
Update value for user record field.
@param {Object} req.params.user
@property {Object} params.user
Requesting user.
@param {boolean} req.params.user.admin
@property {boolean} user.admin
Requesting user is admin.
*/

module.exports = async function update(req, res) {

// acl module will export an empty require object without the ACL being configured.
if (typeof acl !== 'function') {
return res.status(500).send('ACL unavailable.')
}

if (!req.params.user) {
if (!req.params.user?.admin) {

return new Error('login_required')
// The update request can only be made by an administrator.
return new Error('admin_user_login_required')
}

if (!req.params.user?.admin) {
// Create update_user from request body or create Object with email from params.
const update_user = req.body || {
email: req.params.email
}

if (req.params.field === 'roles') {

return new Error('admin_required')
// The value string must be split into an array for the roles field params.
req.params.value = req.params.value?.split(',') || [];

} else if (req.params.field) {

// Assign field property from request params.
update_user[req.params.field] = req.params.value
}

// Remove spaces from email.
const email = req.params.email.replace(/\s+/g, '')
// Create ISODate for administrator request log.
const ISODate = new Date().toISOString().replace(/\..*/, '');

if (req.params.field === 'roles') {
req.params.value = req.params.value?.split(',') || []
let password_reset = ''

if (update_user.verified) {

// Verifying a user will also approve the user, reset password, and failed login attempts.
Object.assign(update_user, {
password_reset: null,
failedattempts: 0,
verificationtoken: null,
approved: true,
approved_by: `${req.params.user.email}|${ISODate}`
})

password_reset = `password = password_reset,`
}

const ISODate = new Date().toISOString().replace(/\..*/, '')
if (update_user.approved) {

// Set approved_by field when updating the approved field in record.
const approved_by = req.params.field === 'approved'
? `, approved_by = '${req.params.user.email}|${ISODate}'`
: '';
// Log who and when approved a user.
update_user.approved_by = `${req.params.user.email}|${ISODate}`
}

let verification_by_admin = ''
if (req.params.field === 'verified' && req.params.value === true) {
// Validate update_user keys.
if (Object.keys(update_user).some(key => !/^[A-Za-z0-9.,_-\s]*$/.test(key))) {

verification_by_admin = `
, password = password_reset
, password_reset = NULL
, failedattempts = 0
, verificationtoken = NULL
, approved = true
, approved_by = '${req.params.user.email}|${ISODate}'
`
// Return bad request 400 if an update_user key contains not white listed characters.
return res.status(400).send('Invalid key in user object for SQL update.')
}

// Get user to update from ACL.
const rows = await acl(`
const properties = []
const substitutes = [update_user.email]

// Populate properties, substitutes array for update_query.
Object.keys(update_user)
.filter(key => key !== 'email')
.forEach(key => {
substitutes.push(update_user[key])
properties.push(`${key} = $${substitutes.length}`)
})

const update_query = `
UPDATE acl_schema.acl_table
SET
${req.params.field} = $2
${verification_by_admin}
${approved_by}
WHERE lower(email) = lower($1);`,
[email, req.params.value])
SET
${password_reset}
${properties.join(',')}
WHERE lower(email) = lower($1);`

const rows = await acl(update_query, substitutes);

if (rows instanceof Error) {
return res.status(500).send('Failed to access ACL.')
}

// Send email to the user account if an account has been approved.
if (req.params.field === 'approved' && req.params.value === true) {
await mailer({
if (update_user.approved) {

const approval_mail = {
template: 'approved_account',
language: req.params.user.language,
to: email,
language: update_user.language,
to: update_user.email,
host: req.params.host
})
}

await mailer(approval_mail);
}

return res.send('Update success')
}
}
3 changes: 3 additions & 0 deletions tests/browser/local.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { pluginsTest } from '../plugins/_plugins.test.mjs';
import { setView } from '../utils/view.js';
import { delayFunction } from '../utils/delay.js';
import { apiTest } from './_api.test.mjs';
import { userTest } from '../mod/user/_user.test.js';
// import { booleanTest } from '../lib/ui/locations/entries/boolean.test.mjs';

await userTest.updateTest();

const mapview = await base();
// Run the dictionary Tests
await dictionaryTest.baseDictionaryTest(mapview);
Expand Down
5 changes: 5 additions & 0 deletions tests/mod/user/_user.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { updateTest } from './update.test.mjs';

export const userTest = {
updateTest
}
Loading

0 comments on commit 1425188

Please sign in to comment.