From c63f42f921e80f0e5fff9c25d922a38a8652b9a4 Mon Sep 17 00:00:00 2001 From: mz8i Date: Mon, 21 Oct 2019 15:19:35 +0100 Subject: [PATCH 1/5] Refactor types and await for user/building backend (#476) * Refactor buildings API for async/await, types * Return building data after update * Refactor users API for await, TS types * Refactor building service to remove repetition As part of this refactor, these changes in functionality were made: - tx isolation lvl for save/like/unlike building is always serializable - both reverse and forward patch updated for like/unlike - comparing old and new data uses == instead of === (this is because the new data even for numbers comes in as string) - the checking of no data change in case of building unlike was fixed (didn't work because it re-used code for like which is different) * Improve param order, docs for updateBuildingData --- app/src/api/controllers/buildingController.ts | 184 +++--- app/src/api/controllers/userController.ts | 46 +- app/src/api/services/building.ts | 529 +++++++++--------- app/src/api/services/user.ts | 248 ++++---- 4 files changed, 509 insertions(+), 498 deletions(-) diff --git a/app/src/api/controllers/buildingController.ts b/app/src/api/controllers/buildingController.ts index 7987293d..b2a9eb33 100644 --- a/app/src/api/controllers/buildingController.ts +++ b/app/src/api/controllers/buildingController.ts @@ -1,147 +1,139 @@ +import express from 'express'; import * as buildingService from '../services/building'; import * as userService from '../services/user'; +import asyncController from '../routes/asyncController'; // GET buildings // not implemented - may be useful to GET all buildings, paginated // GET buildings at point -function getBuildingsByLocation(req, res) { +const getBuildingsByLocation = asyncController(async (req: express.Request, res: express.Response) => { const { lng, lat } = req.query; - buildingService.queryBuildingsAtPoint(lng, lat).then(function (result) { + try { + const result = await buildingService.queryBuildingsAtPoint(lng, lat); res.send(result); - }).catch(function (error) { + } catch (error) { console.error(error); - res.send({ error: 'Database error' }) - }) -} + res.send({ error: 'Database error' }); + } +}); // GET buildings by reference (UPRN/TOID or other identifier) -function getBuildingsByReference(req, res) { +const getBuildingsByReference = asyncController(async (req: express.Request, res: express.Response) => { const { key, id } = req.query; - buildingService.queryBuildingsByReference(key, id).then(function (result) { + try { + const result = await buildingService.queryBuildingsByReference(key, id); res.send(result); - }).catch(function (error) { + } catch (error) { console.error(error); - res.send({ error: 'Database error' }) - }) -} + res.send({ error: 'Database error' }); + } +}); // GET individual building, POST building updates -function getBuildingById(req, res) { +const getBuildingById = asyncController(async (req: express.Request, res: express.Response) => { const { building_id } = req.params; - buildingService.getBuildingById(building_id).then(function (result) { + try { + const result = await buildingService.getBuildingById(building_id); res.send(result); - }).catch(function (error) { + } catch(error) { console.error(error); - res.send({ error: 'Database error' }) - }) -} + res.send({ error: 'Database error' }); + } +}); -function updateBuildingById(req, res) { +const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => { if (req.session.user_id) { - updateBuilding(req, res, req.session.user_id); + await updateBuilding(req, res, req.session.user_id); } else if (req.query.api_key) { - userService.authAPIUser(req.query.api_key) - .then(function (user) { - updateBuilding(req, res, user.user_id) - }) - .catch(function (err) { - console.error(err); - res.send({ error: 'Must be logged in' }); - }); + try { + const user = await userService.authAPIUser(req.query.api_key); + await updateBuilding(req, res, user.user_id); + } catch(err) { + console.error(err); + res.send({ error: 'Must be logged in' }); + } } else { res.send({ error: 'Must be logged in' }); } -} +}); -function updateBuilding(req, res, userId) { +async function updateBuilding(req: express.Request, res: express.Response, userId: string) { const { building_id } = req.params; - const building = req.body; - buildingService.saveBuilding(building_id, building, userId).then(building => { - if (building.error) { - res.send(building) - return - } + const buildingUpdate = req.body; + + try { + const building = await buildingService.saveBuilding(building_id, buildingUpdate, userId); + if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return + return res.send({ error: 'Database error' }); } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) + if (building.error) { + return res.send(building); + } + res.send(building); + } catch(err) { + res.send({ error: 'Database error' }); + } } // GET building UPRNs -function getBuildingUPRNsById(req, res) { +const getBuildingUPRNsById = asyncController(async (req: express.Request, res: express.Response) => { const { building_id } = req.params; - buildingService.getBuildingUPRNsById(building_id).then(function (result) { + try { + const result = await buildingService.getBuildingUPRNsById(building_id); + if (typeof (result) === 'undefined') { - res.send({ error: 'Database error' }) - return + return res.send({ error: 'Database error' }); } - res.send({ - uprns: result - }); - }).catch(function (error) { + res.send({uprns: result}); + } catch(error) { console.error(error); - res.send({ error: 'Database error' }) - }) -} + res.send({ error: 'Database error' }); + } +}); // GET/POST like building -function getBuildingLikeById(req, res) { +const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { if (!req.session.user_id) { - res.send({ like: false }); // not logged in, so cannot have liked - return + return res.send({ like: false }); // not logged in, so cannot have liked } const { building_id } = req.params; - buildingService.getBuildingLikeById(building_id, req.session.user_id).then(like => { - // any value returned means like - res.send({ like: like }) - }).catch( - () => res.send({ error: 'Database error' }) - ) -} + try { + const like = await buildingService.getBuildingLikeById(building_id, req.session.user_id); -function updateBuildingLikeById(req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return + // any value returned means like + res.send({ like: like }); + } catch(error) { + res.send({ error: 'Database error' }) } +}); + +const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { + if (!req.session.user_id) { + return res.send({ error: 'Must be logged in' }); + } + const { building_id } = req.params; const { like } = req.body; - if (like) { - buildingService.likeBuilding(building_id, req.session.user_id).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) - } else { - buildingService.unlikeBuilding(building_id, req.session.user_id).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) + + try { + const building = like ? + await buildingService.likeBuilding(building_id, req.session.user_id) : + await buildingService.unlikeBuilding(building_id, req.session.user_id); + + if (building.error) { + return res.send(building); + } + if (typeof (building) === 'undefined') { + return res.send({ error: 'Database error' }); + } + res.send(building); + } catch(error) { + res.send({ error: 'Database error' }); } -} +}); export default { getBuildingsByLocation, diff --git a/app/src/api/controllers/userController.ts b/app/src/api/controllers/userController.ts index c5c4f593..b29291a2 100644 --- a/app/src/api/controllers/userController.ts +++ b/app/src/api/controllers/userController.ts @@ -8,23 +8,22 @@ import { TokenVerificationError } from '../services/passwordReset'; import asyncController from '../routes/asyncController'; import { ValidationError } from '../validation'; -function createUser(req, res) { +const createUser = asyncController(async (req: express.Request, res: express.Response) => { const user = req.body; if (req.session.user_id) { - res.send({ error: 'Already signed in' }); - return; + return res.send({ error: 'Already signed in' }); } if (user.email) { if (user.email != user.confirm_email) { - res.send({ error: 'Email did not match confirmation.' }); - return; + return res.send({ error: 'Email did not match confirmation.' }); } } else { user.email = null; } - userService.createUser(user).then(function (result) { + try { + const result = await userService.createUser(user); if (result.user_id) { req.session.user_id = result.user_id; res.send({ user_id: result.user_id }); @@ -32,39 +31,40 @@ function createUser(req, res) { req.session.user_id = undefined; res.send({ error: result.error }); } - }).catch(function (err) { + } catch(err) { console.error(err); res.send(err); - }); -} + } +}); -function getCurrentUser(req, res) { +const getCurrentUser = asyncController(async (req: express.Request, res: express.Response) => { if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return; + return res.send({ error: 'Must be logged in' }); } - userService.getUserById(req.session.user_id).then(function (user) { + try { + const user = await userService.getUserById(req.session.user_id); res.send(user); - }).catch(function (error) { + } catch(error) { res.send(error); - }); -} + } +}); -function deleteCurrentUser(req, res) { +const deleteCurrentUser = asyncController(async (req: express.Request, res: express.Response) => { if (!req.session.user_id) { return res.send({ error: 'Must be logged in' }); } console.log(`Deleting user ${req.session.user_id}`); - userService.deleteUser(req.session.user_id).then( - () => userService.logout(req.session) - ).then(() => { + try { + await userService.deleteUser(req.session.user_id); + await userService.logout(req.session); + res.send({ success: true }); - }).catch(err => { + } catch(err) { res.send({ error: err }); - }); -} + } +}); const resetPassword = asyncController(async function(req: express.Request, res: express.Response) { if(req.body == undefined || (req.body.email == undefined && req.body.token == undefined)) { diff --git a/app/src/api/services/building.ts b/app/src/api/services/building.ts index 36ff4748..e97e2cf3 100644 --- a/app/src/api/services/building.ts +++ b/app/src/api/services/building.ts @@ -5,6 +5,7 @@ import db from '../../db'; import { tileCache } from '../../tiles/rendererDefinition'; import { BoundingBox } from '../../tiles/types'; +import { ITask } from 'pg-promise'; // data type note: PostgreSQL bigint (64-bit) is handled as string in JavaScript, because of // JavaScript numerics are 64-bit double, giving only partial coverage. @@ -18,282 +19,289 @@ const serializable = new TransactionMode({ readOnly: false }); -function queryBuildingsAtPoint(lng, lat) { - return db.manyOrNone( - `SELECT b.* - FROM buildings as b, geometries as g - WHERE - b.geometry_id = g.geometry_id - AND - ST_Intersects( - ST_Transform( - ST_SetSRID(ST_Point($1, $2), 4326), - 3857 - ), - geometry_geom - ) - `, - [lng, lat] - ).catch(function (error) { - console.error(error); - return undefined; - }); -} - -function queryBuildingsByReference(key, id) { - if (key === 'toid') { - return db.manyOrNone( - `SELECT - * - FROM - buildings +async function queryBuildingsAtPoint(lng: number, lat: number) { + try { + return await db.manyOrNone( + `SELECT b.* + FROM buildings as b, geometries as g WHERE - ref_toid = $1 - `, - [id] - ).catch(function (error) { - console.error(error); - return undefined; - }); - } - if (key === 'uprn') { - return db.manyOrNone( - `SELECT - b.* - FROM - buildings as b, building_properties as p - WHERE - b.building_id = p.building_id + b.geometry_id = g.geometry_id AND - p.uprn = $1 + ST_Intersects( + ST_Transform( + ST_SetSRID(ST_Point($1, $2), 4326), + 3857 + ), + geometry_geom + ) `, - [id] - ).catch(function (error) { - console.error(error); - return undefined; - }); + [lng, lat] + ); + } catch(error) { + console.error(error); + return undefined; } - return Promise.resolve({ error: 'Key must be UPRN or TOID' }); } -function getBuildingById(id) { - return db.one( - 'SELECT * FROM buildings WHERE building_id = $1', - [id] - ).then((building) => { - return getBuildingEditHistory(id).then((edit_history) => { - building.edit_history = edit_history - return building - }) - }).catch(function (error) { - console.error(error); - return undefined; - }); -} - -function getBuildingEditHistory(id) { - return db.manyOrNone( - `SELECT log_id as revision_id, forward_patch, reverse_patch, date_trunc('minute', log_timestamp), username - FROM logs, users - WHERE building_id = $1 AND logs.user_id = users.user_id`, - [id] - ).then((data) => { - return data - }).catch(function (error) { - console.error(error); - return [] - }); -} - -function getBuildingLikeById(buildingId, userId) { - return db.oneOrNone( - 'SELECT true as like FROM building_user_likes WHERE building_id = $1 and user_id = $2 LIMIT 1', - [buildingId, userId] - ).then(res => { - return res && res.like - }).catch(function (error) { - console.error(error); - return undefined; - }); -} - -function getBuildingUPRNsById(id) { - return db.any( - 'SELECT uprn, parent_uprn FROM building_properties WHERE building_id = $1', - [id] - ).catch(function (error) { - console.error(error); - return undefined; - }); -} - -function saveBuilding(buildingId, building, userId) { - // remove read-only fields from consideration - delete building.building_id; - delete building.revision_id; - delete building.geometry_id; - - // start transaction around save operation - // - select and compare to identify changeset - // - insert changeset - // - update to latest state - // commit or rollback (repeated-read sufficient? or serializable?) - return db.tx(t => { - return t.one( - 'SELECT * FROM buildings WHERE building_id = $1 FOR UPDATE;', - [buildingId] - ).then(oldBuilding => { - const patches = compare(oldBuilding, building, BUILDING_FIELD_WHITELIST); - console.log('Patching', buildingId, patches) - const forward = patches[0]; - const reverse = patches[1]; - if (Object.keys(forward).length === 0) { - return Promise.reject('No change provided') - } - return t.one( - `INSERT INTO logs ( - forward_patch, reverse_patch, building_id, user_id - ) VALUES ( - $1:json, $2:json, $3, $4 - ) RETURNING log_id +async function queryBuildingsByReference(key: string, ref: string) { + try { + if (key === 'toid') { + return await db.manyOrNone( + `SELECT + * + FROM + buildings + WHERE + ref_toid = $1 `, - [forward, reverse, buildingId, userId] - ).then(revision => { - const sets = db.$config.pgp.helpers.sets(forward); - console.log('Setting', buildingId, sets) - return t.one( - `UPDATE - buildings - SET - revision_id = $1, - $2:raw - WHERE - building_id = $3 - RETURNING - * - `, - [revision.log_id, sets, buildingId] - ).then((data) => { - expireBuildingTileCache(buildingId) - return data - }) - }); + [ref] + ); + } else if (key === 'uprn') { + return await db.manyOrNone( + `SELECT + b.* + FROM + buildings as b, building_properties as p + WHERE + b.building_id = p.building_id + AND + p.uprn = $1 + `, + [ref] + ); + } else { + return { error: 'Key must be UPRN or TOID' }; + } + } catch(err) { + console.error(err); + return undefined; + } +} + +async function getBuildingById(id: number) { + try { + const building = await db.one( + 'SELECT * FROM buildings WHERE building_id = $1', + [id] + ); + + building.edit_history = await getBuildingEditHistory(id); + + return building; + } catch(error) { + console.error(error); + return undefined; + } +} + +async function getBuildingEditHistory(id: number) { + try { + return await db.manyOrNone( + `SELECT log_id as revision_id, forward_patch, reverse_patch, date_trunc('minute', log_timestamp), username + FROM logs, users + WHERE building_id = $1 AND logs.user_id = users.user_id`, + [id] + ); + } catch(error) { + console.error(error); + return []; + } +} + +async function getBuildingLikeById(buildingId: number, userId: string) { + try { + const res = await db.oneOrNone( + 'SELECT true as like FROM building_user_likes WHERE building_id = $1 and user_id = $2 LIMIT 1', + [buildingId, userId] + ); + return res && res.like; + } catch(error) { + console.error(error); + return undefined; + } +} + +async function getBuildingUPRNsById(id: number) { + try { + return await db.any( + 'SELECT uprn, parent_uprn FROM building_properties WHERE building_id = $1', + [id] + ); + } catch(error) { + console.error(error); + return undefined; + } +} + +async function saveBuilding(buildingId: number, building: any, userId: string) { // TODO add proper building type + try { + return await updateBuildingData(buildingId, userId, async () => { + // remove read-only fields from consideration + delete building.building_id; + delete building.revision_id; + delete building.geometry_id; + + // return whitelisted fields to update + return pickAttributesToUpdate(building, BUILDING_FIELD_WHITELIST); }); - }).catch(function (error) { + } catch(error) { console.error(error); return { error: error }; - }); + } } -function likeBuilding(buildingId, userId) { - // start transaction around save operation - // - insert building-user like - // - count total likes - // - insert changeset - // - update building to latest state - // commit or rollback (serializable - could be more compact?) - return db.tx({mode: serializable}, t => { - return t.none( - 'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);', - [buildingId, userId] - ).then(() => { - return t.one( - 'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;', - [buildingId] - ).then(building => { - return t.one( - `INSERT INTO logs ( - forward_patch, building_id, user_id - ) VALUES ( - $1:json, $2, $3 - ) RETURNING log_id - `, - [{ likes_total: building.likes }, buildingId, userId] - ).then(revision => { - return t.one( - `UPDATE buildings - SET - revision_id = $1, - likes_total = $2 - WHERE - building_id = $3 - RETURNING - * - `, - [revision.log_id, building.likes, buildingId] - ).then((data) => { - expireBuildingTileCache(buildingId) - return data - }) - }) - }); - }); - }).catch(function (error) { +async function likeBuilding(buildingId: number, userId: string) { + try { + return await updateBuildingData( + buildingId, + userId, + async (t) => { + // return total like count after update + return getBuildingLikeCount(buildingId, t); + }, + async (t) => { + // insert building-user like + await t.none( + 'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);', + [buildingId, userId] + ); + }, + ); + } catch (error) { console.error(error); if (error.detail && error.detail.includes('already exists')) { // 'already exists' is thrown if user already liked it return { error: 'It looks like you already like that building!' }; } else { - return undefined + return undefined; } - }); + } } -function unlikeBuilding(buildingId, userId) { - // start transaction around save operation - // - insert building-user like - // - count total likes - // - insert changeset - // - update building to latest state - // commit or rollback (serializable - could be more compact?) - return db.tx({mode: serializable}, t => { - return t.none( - 'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;', - [buildingId, userId] - ).then(() => { - return t.one( - 'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;', - [buildingId] - ).then(building => { - return t.one( - `INSERT INTO logs ( - forward_patch, building_id, user_id +async function unlikeBuilding(buildingId: number, userId: string) { + try { + return await updateBuildingData( + buildingId, + userId, + async (t) => { + // return total like count after update + return getBuildingLikeCount(buildingId, t); + }, + async (t) => { + // remove building-user like + const result = await t.result( + 'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;', + [buildingId, userId] + ); + + if (result.rowCount === 0) { + throw new Error('No change'); + } + }, + ); + } catch(error) { + console.error(error); + if (error.message === 'No change') { + // 'No change' is thrown if user doesn't like this building + return { error: 'It looks like you have already revoked your like for that building!' }; + } else { + return undefined; + } + } +} + +// === Utility functions === + +function pickAttributesToUpdate(obj: any, fieldWhitelist: Set) { + const subObject = {}; + for (let [key, value] of Object.entries(obj)) { + if(fieldWhitelist.has(key)) { + subObject[key] = value; + } + } + return subObject; +} + +/** + * + * @param buildingId ID of the building to count likes for + * @param t The database context inside which the count should happen + */ +function getBuildingLikeCount(buildingId: number, t: ITask) { + return t.one( + 'SELECT count(*) as likes_total FROM building_user_likes WHERE building_id = $1;', + [buildingId] + ); +} + +/** + * Carry out an update of the buildings data. Allows for running any custom database operations before the main update. + * All db hooks get passed a transaction. + * @param buildingId The ID of the building to update + * @param userId The ID of the user updating the data + * @param getUpdateValue Function returning the set of attribute to update for the building + * @param preUpdateDbAction Any db operations to carry out before updating the buildings table (mostly intended for updating the user likes table) + */ +async function updateBuildingData( + buildingId: number, + userId: string, + getUpdateValue: (t: ITask) => Promise, + preUpdateDbAction?: (t: ITask) => Promise, +) { + return await db.tx({mode: serializable}, async t => { + if (preUpdateDbAction != undefined) { + await preUpdateDbAction(t); + } + + const update = await getUpdateValue(t); + + const oldBuilding = await t.one( + 'SELECT * FROM buildings WHERE building_id = $1 FOR UPDATE;', + [buildingId] + ); + + console.log(update); + const patches = compare(oldBuilding, update); + console.log('Patching', buildingId, patches) + const [forward, reverse] = patches; + if (Object.keys(forward).length === 0) { + throw 'No change provided'; + } + + const revision = await t.one( + `INSERT INTO logs ( + forward_patch, reverse_patch, building_id, user_id ) VALUES ( - $1:json, $2, $3 + $1:json, $2:json, $3, $4 ) RETURNING log_id `, - [{ likes_total: building.likes }, buildingId, userId] - ).then(revision => { - return t.one( - `UPDATE buildings - SET - revision_id = $1, - likes_total = $2 - WHERE - building_id = $3 - RETURNING - * - `, - [revision.log_id, building.likes, buildingId] - ).then((data) => { - expireBuildingTileCache(buildingId) - return data - }) - }) - }); - }); - }).catch(function (error) { - console.error(error); - if (error.detail && error.detail.includes('already exists')) { - // 'already exists' is thrown if user already liked it - return { error: 'It looks like you already like that building!' }; - } else { - return undefined - } + [forward, reverse, buildingId, userId] + ); + + const sets = db.$config.pgp.helpers.sets(forward); + console.log('Setting', buildingId, sets); + + const data = await t.one( + `UPDATE + buildings + SET + revision_id = $1, + $2:raw + WHERE + building_id = $3 + RETURNING + * + `, + [revision.log_id, sets, buildingId] + ); + + expireBuildingTileCache(buildingId); + + return data; }); } -function privateQueryBuildingBBOX(buildingId){ +function privateQueryBuildingBBOX(buildingId: number){ return db.one( `SELECT ST_XMin(envelope) as xmin, @@ -310,14 +318,13 @@ function privateQueryBuildingBBOX(buildingId){ b.building_id = $1 ) as envelope`, [buildingId] - ) + ); } -function expireBuildingTileCache(buildingId) { - privateQueryBuildingBBOX(buildingId).then((bbox) => { - const buildingBbox: BoundingBox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin]; - tileCache.removeAllAtBbox(buildingBbox); - }) +async function expireBuildingTileCache(buildingId: number) { + const bbox = await privateQueryBuildingBBOX(buildingId) + const buildingBbox: BoundingBox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin]; + tileCache.removeAllAtBbox(buildingBbox); } const BUILDING_FIELD_WHITELIST = new Set([ @@ -385,16 +392,16 @@ const BUILDING_FIELD_WHITELIST = new Set([ * @param {Set} whitelist * @returns {[object, object]} */ -function compare(oldObj, newObj, whitelist) { - const reverse = {} - const forward = {} +function compare(oldObj: object, newObj: object): [object, object] { + const reverse = {}; + const forward = {}; for (const [key, value] of Object.entries(newObj)) { - if (oldObj[key] !== value && whitelist.has(key)) { + if (oldObj[key] != value) { reverse[key] = oldObj[key]; forward[key] = value; } } - return [forward, reverse] + return [forward, reverse]; } export { diff --git a/app/src/api/services/user.ts b/app/src/api/services/user.ts index 5594e3bb..c91c8e60 100644 --- a/app/src/api/services/user.ts +++ b/app/src/api/services/user.ts @@ -6,173 +6,185 @@ import { errors } from 'pg-promise'; import db from '../../db'; import { validateUsername, ValidationError, validatePassword } from '../validation'; +import { promisify } from 'util'; -function createUser(user) { + +async function createUser(user) { try { validateUsername(user.username); validatePassword(user.password); } catch(err) { if (err instanceof ValidationError) { - return Promise.reject({ error: err.message }); + throw { error: err.message }; } else throw err; } - return db.one( - `INSERT - INTO users ( - user_id, - username, - email, - pass - ) VALUES ( - gen_random_uuid(), - $1, - $2, - crypt($3, gen_salt('bf')) - ) RETURNING user_id - `, [ - user.username, - user.email, - user.password - ] - ).catch(function (error) { - console.error('Error:', error) + try { + return await db.one( + `INSERT + INTO users ( + user_id, + username, + email, + pass + ) VALUES ( + gen_random_uuid(), + $1, + $2, + crypt($3, gen_salt('bf')) + ) RETURNING user_id + `, [ + user.username, + user.email, + user.password + ] + ); + } catch(error) { + console.error('Error:', error); - if (error.detail.indexOf('already exists') !== -1) { - if (error.detail.indexOf('username') !== -1) { + if (error.detail.includes('already exists')) { + if (error.detail.includes('username')) { return { error: 'Username already registered' }; - } else if (error.detail.indexOf('email') !== -1) { + } else if (error.detail.includes('email')) { return { error: 'Email already registered' }; } } - return { error: 'Database error' } - }); + return { error: 'Database error' }; + } } -function authUser(username, password) { - return db.one( - `SELECT - user_id, - ( - pass = crypt($2, pass) - ) AS auth_ok - FROM users - WHERE - username = $1 - `, [ - username, - password - ] - ).then(function (user) { +async function authUser(username: string, password: string) { + try { + const user = await db.one( + `SELECT + user_id, + ( + pass = crypt($2, pass) + ) AS auth_ok + FROM users + WHERE + username = $1 + `, [ + username, + password + ] + ); + if (user && user.auth_ok) { return { user_id: user.user_id } } else { return { error: 'Username or password not recognised' } } - }).catch(function (err) { + } catch(err) { if (err instanceof errors.QueryResultError) { console.error(`Authentication failed for user ${username}`); return { error: 'Username or password not recognised' }; } console.error('Error:', err); return { error: 'Database error' }; - }) + } } -function getUserById(id) { - return db.one( - `SELECT - username, email, registered, api_key - FROM - users - WHERE - user_id = $1 - `, [ - id - ] - ).catch(function (error) { +async function getUserById(id: string) { + try { + return await db.one( + `SELECT + username, email, registered, api_key + FROM + users + WHERE + user_id = $1 + `, [ + id + ] + ); + } catch(error) { console.error('Error:', error) return undefined; - }); + } } -function getUserByEmail(email: string) { - return db.one( - `SELECT - user_id, username, email - FROM - users - WHERE - email = $1 - `, [email] - ).catch(function(error) { +async function getUserByEmail(email: string) { + try { + return db.one( + `SELECT + user_id, username, email + FROM + users + WHERE + email = $1 + `, [email] + ); + } catch(error) { console.error('Error:', error); return undefined; - }); + } } -function getNewUserAPIKey(id) { - return db.one( - `UPDATE - users - SET - api_key = gen_random_uuid() - WHERE - user_id = $1 - RETURNING - api_key - `, [ - id - ] - ).catch(function (error) { +async function getNewUserAPIKey(id: string) { + try{ + return db.one( + `UPDATE + users + SET + api_key = gen_random_uuid() + WHERE + user_id = $1 + RETURNING + api_key + `, [ + id + ] + ); + } catch(error) { console.error('Error:', error) return { error: 'Failed to generate new API key.' }; - }); + } } -function authAPIUser(key) { - return db.one( - `SELECT - user_id - FROM - users - WHERE - api_key = $1 - `, [ - key - ] - ).catch(function (error) { +async function authAPIUser(key: string) { + try { + return await db.one( + `SELECT + user_id + FROM + users + WHERE + api_key = $1 + `, [ + key + ] + ); + } catch(error) { console.error('Error:', error) return undefined; - }); + } } -function deleteUser(id) { - return db.none( - `UPDATE users - SET - email = null, - pass = null, - api_key = null, - username = concat('deleted_', cast(user_id as char(13))), - is_deleted = true, - deleted_on = now() at time zone 'utc' - WHERE user_id = $1 - `, [id] - ).catch((error) => { +async function deleteUser(id: string) { + try { + return await db.none( + `UPDATE users + SET + email = null, + pass = null, + api_key = null, + username = concat('deleted_', cast(user_id as char(13))), + is_deleted = true, + deleted_on = now() at time zone 'utc' + WHERE user_id = $1 + `, [id] + ); + } catch(error) { console.error('Error:', error); return {error: 'Database error'}; - }); + } } -function logout(session: Express.Session) { - return new Promise((resolve, reject) => { - session.user_id = undefined; - session.destroy(err => { - if (err) return reject(err); - return resolve(); - }); - }); +function logout(session: Express.Session): Promise { + session.user_id = undefined; + + return promisify(session.destroy.bind(session))(); } export { From 5d3eff480075b391698e9d6614e44a4b2ba153c7 Mon Sep 17 00:00:00 2001 From: mz8i Date: Mon, 28 Oct 2019 16:46:22 +0000 Subject: [PATCH 2/5] Branding adjustments (#471) * Synchronise category colours with WIX page version * Display static colourful logo on map pages --- app/src/frontend/app.tsx | 13 +++++++++++-- app/src/frontend/components/logo.tsx | 24 ++++++++++++------------ app/src/frontend/header.tsx | 17 ++++++++++++++--- app/src/frontend/styles/colours.css | 24 ++++++++++++------------ 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 5e681574..f6952671 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -49,6 +49,8 @@ class App extends React.Component { // TODO: add proper types building_like: PropTypes.bool }; + static mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?']; + constructor(props: Readonly) { super(props); @@ -79,7 +81,14 @@ class App extends React.Component { // TODO: add proper types render() { return ( -
+ + +
+ + +
+ +
@@ -105,7 +114,7 @@ class App extends React.Component { // TODO: add proper types - ( + ( = (props) => { const LogoGrid: React.FunctionComponent = () => (
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
) diff --git a/app/src/frontend/header.tsx b/app/src/frontend/header.tsx index 41c3cea4..861dbb4a 100644 --- a/app/src/frontend/header.tsx +++ b/app/src/frontend/header.tsx @@ -5,14 +5,25 @@ import PropTypes from 'prop-types'; import { Logo } from './components/logo'; import './header.css'; + +interface HeaderProps { + user: any; + animateLogo: boolean; +} + +interface HeaderState { + collapseMenu: boolean; +} + /** * Render the main header using a responsive design */ -class Header extends React.Component { // TODO: add proper types +class Header extends React.Component { // TODO: add proper types static propTypes = { // TODO: generate propTypes from TS user: PropTypes.shape({ username: PropTypes.string - }) + }), + animateLogo: PropTypes.bool }; constructor(props) { @@ -40,7 +51,7 @@ class Header extends React.Component { // TODO: add proper types