diff --git a/app/src/api/controllers/buildingController.ts b/app/src/api/controllers/buildingController.ts index 6bc53d44..56916833 100644 --- a/app/src/api/controllers/buildingController.ts +++ b/app/src/api/controllers/buildingController.ts @@ -35,7 +35,7 @@ const getBuildingsByReference = asyncController(async (req: express.Request, res } }); -// GET individual building, POST building updates +// GET individual building const getBuildingById = asyncController(async (req: express.Request, res: express.Response) => { const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true); @@ -48,6 +48,7 @@ const getBuildingById = asyncController(async (req: express.Request, res: expres } }); +// POST building attribute updates const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => { let user_id; @@ -105,7 +106,7 @@ const getBuildingUPRNsById = asyncController(async (req: express.Request, res: e } }); -// GET/POST like building +// GET whether the user likes a building const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { if (!req.session.user_id) { return res.send({ like: false }); // not logged in, so cannot have liked @@ -123,6 +124,7 @@ const getBuildingLikeById = asyncController(async (req: express.Request, res: ex } }); +// GET building edit history const getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => { const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true); @@ -135,6 +137,7 @@ const getBuildingEditHistoryById = asyncController(async (req: express.Request, } }); +// POST update to like/unlike building const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { if (!req.session.user_id) { return res.send({ error: 'Must be logged in' }); @@ -159,6 +162,48 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res: res.send(updatedBuilding); }); +// GET building attributes (and values) as verified by user +const getUserVerifiedAttributes = asyncController(async (req: express.Request, res: express.Response) => { + if (!req.session.user_id) { + return res.send({error: "Not logged in"}); // not logged in, so send empty object as no attributes verified + } + + const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true); + + try { + const verifiedAttributes = await buildingService.getUserVerifiedAttributes(buildingId, req.session.user_id); + res.send(verifiedAttributes); + } catch (error) { + if(error instanceof UserError) { + throw new ApiUserError(error.message, error); + } + + throw error; + } +}); + +// POST update to verify building attribute +const verifyBuildingAttributes = asyncController(async (req: express.Request, res: express.Response) => { + if (!req.session.user_id) { + return res.send({ error: 'Must be logged in' }); + } + + const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true); + const verifiedAttributes = req.body; + + try { + const success = await buildingService.verifyBuildingAttributes(buildingId, req.session.user_id, verifiedAttributes); + res.send(success); + } catch (error) { + if(error instanceof UserError) { + throw new ApiUserError(error.message, error); + } + + throw error; + } +}); + +// GET latest revision id of any building const getLatestRevisionId = asyncController(async (req: express.Request, res: express.Response) => { try { const revisionId = await buildingService.getLatestRevisionId(); @@ -176,6 +221,8 @@ export default { getBuildingUPRNsById, getBuildingLikeById, updateBuildingLikeById, + getUserVerifiedAttributes, + verifyBuildingAttributes, getBuildingEditHistoryById, getLatestRevisionId }; diff --git a/app/src/api/dataAccess/verify.ts b/app/src/api/dataAccess/verify.ts new file mode 100644 index 00000000..e74a0c80 --- /dev/null +++ b/app/src/api/dataAccess/verify.ts @@ -0,0 +1,88 @@ +import db from '../../db'; +import { DatabaseError, InvalidOperationError } from '../errors/general'; + + +export async function getBuildingVerifiedAttributes(buildingId: number): Promise { + try { + return await (db).manyOrNone( + `SELECT + attribute, + verified_value + FROM + building_verification + WHERE + building_id = $1; + `, + [buildingId] + ); + } catch(error) { + throw new DatabaseError(error.detail); + } +} + +export async function getBuildingUserVerifiedAttributes(buildingId: number, userId: string): Promise { + try { + const verifications = await (db).manyOrNone( + `SELECT + attribute, + verified_value, + date_trunc('minute', verification_timestamp) as verification_timestamp + FROM + building_verification + WHERE + building_id = $1 + AND user_id = $2; + `, + [buildingId, userId] + ); + return asVerified(verifications) + } catch(error) { + throw new DatabaseError(error.detail); + } +} + +function asVerified(verifications){ + const user_verified = {}; + for (const item of verifications) { + user_verified[item.attribute] = item.verified_value + } + return user_verified; +} + +export async function updateBuildingUserVerifiedAttribute(buildingId: number, userId: string, attribute: string, value: any): Promise { + try { + await (db).none( + `INSERT INTO + building_verification + ( building_id, user_id, attribute, verified_value ) + VALUES + ($1, $2, $3, to_jsonb($4)); + `, + [buildingId, userId, attribute, value] + ); + } catch(error) { + if(error.detail?.includes('already exists')) { + const msg = 'User already verified that attribute for this building' + throw new InvalidOperationError(msg); + } else { + throw new DatabaseError(error.detail); + } + } +} + +export async function removeBuildingUserVerifiedAttribute(buildingId: number, userId: string, attribute: string) : Promise { + try { + return await (db).none( + `DELETE FROM + building_verification + WHERE + building_id = $1 + AND user_id = $2 + AND attribute = $3; + `, + [buildingId, userId, attribute] + ); + } catch(error) { + throw new DatabaseError(error.detail); + } +} \ No newline at end of file diff --git a/app/src/api/routes/buildingsRouter.ts b/app/src/api/routes/buildingsRouter.ts index 98347fe1..e3102f29 100644 --- a/app/src/api/routes/buildingsRouter.ts +++ b/app/src/api/routes/buildingsRouter.ts @@ -31,6 +31,11 @@ router.route('/:building_id/like.json') .get(buildingController.getBuildingLikeById) .post(buildingController.updateBuildingLikeById); +// POST verify building attribute +router.route('/:building_id/verify.json') + .get(buildingController.getUserVerifiedAttributes) + .post(buildingController.verifyBuildingAttributes); + router.route('/:building_id/history.json') .get(buildingController.getBuildingEditHistoryById); diff --git a/app/src/api/services/building.ts b/app/src/api/services/building.ts index f84eb0a7..4176efb6 100644 --- a/app/src/api/services/building.ts +++ b/app/src/api/services/building.ts @@ -10,7 +10,8 @@ import { tileCache } from '../../tiles/rendererDefinition'; import { BoundingBox } from '../../tiles/types'; import * as buildingDataAccess from '../dataAccess/building'; import * as likeDataAccess from '../dataAccess/like'; -import { UserError } from '../errors/general'; +import * as verifyDataAccess from '../dataAccess/verify'; +import { UserError, DatabaseError } from '../errors/general'; import { processBuildingUpdate } from './domainLogic/processBuildingUpdate'; @@ -110,6 +111,7 @@ async function getBuildingById(id: number) { const building = await getCurrentBuildingDataById(id); building.edit_history = await getBuildingEditHistory(id); + building.verified = await getBuildingVerifications(building); return building; } catch(error) { @@ -159,9 +161,10 @@ async function getBuildingUPRNsById(id: number) { } async function saveBuilding(buildingId: number, building: any, userId: string): Promise { // TODO add proper building type + console.log("SAVE") return await updateBuildingData(buildingId, userId, async () => { const processedBuilding = await processBuildingUpdate(buildingId, building); - + // remove read-only fields from consideration delete processedBuilding.building_id; delete processedBuilding.revision_id; @@ -204,6 +207,68 @@ async function unlikeBuilding(buildingId: number, userId: string) { ); } +async function verifyBuildingAttributes(buildingId: number, userId: string, patch: object) { + // get current building attribute values for comparison + const building = await getCurrentBuildingDataById(buildingId); + // keep track of attributes and values verified + const verified = {} + + // loop through attribute => value pairs to mark as verified + for (let [key, value] of Object.entries(patch)) { + // check key in whitelist + if(BUILDING_FIELD_WHITELIST.has(key)) { + // check value against current from database + if (value == building[key]) { + try { + await verifyDataAccess.updateBuildingUserVerifiedAttribute(buildingId, userId, key, building[key]); + verified[key] = building[key]; + } catch (error) { + // possible reasons: + // - not a building + // - not a user + // - user already verified this attribute for this building + throw new DatabaseError(error); + } + } else { + if (value === null) { + try { + await verifyDataAccess.removeBuildingUserVerifiedAttribute(buildingId, userId, key); + } + } else { + // not verifying current value + const msg = `Attribute "${key}" with value "${value}" did not match latest saved value "${building[key]}"`; + throw new DatabaseError(msg); + } + } + } else { + // not a valid attribute + const msg = `Attribute ${key} not recognised.`; + throw new DatabaseError(msg); + } + } + return verified; +} + +async function getUserVerifiedAttributes(buildingId: number, userId: string) { + return await verifyDataAccess.getBuildingUserVerifiedAttributes(buildingId, userId); +} + +async function getBuildingVerifications(building) { + const verifications = await verifyDataAccess.getBuildingVerifiedAttributes(building.building_id); + + const verified = {}; + for (const element of BUILDING_FIELD_WHITELIST) { + verified[element] = 0; + } + + for (const item of verifications) { + if (building[item.attribute] == item.verified_value) { + verified[item.attribute] += 1 + } + } + return verified; +} + // === Utility functions === function pickAttributesToUpdate(obj: any, fieldWhitelist: Set) { @@ -378,5 +443,7 @@ export { saveBuilding, likeBuilding, unlikeBuilding, - getLatestRevisionId + getLatestRevisionId, + verifyBuildingAttributes, + getUserVerifiedAttributes }; diff --git a/app/src/api/services/user.ts b/app/src/api/services/user.ts index 3ba78a69..da1aae5b 100644 --- a/app/src/api/services/user.ts +++ b/app/src/api/services/user.ts @@ -18,7 +18,7 @@ async function createUser(user) { throw { error: err.message }; } else throw err; } - + try { return await db.one( `INSERT @@ -89,7 +89,7 @@ async function getUserById(id: string) { try { return await db.one( `SELECT - username, email, registered, api_key + username, email, date_trunc('minute', registered) as registered, api_key FROM users WHERE