Add backend service to GET/POST user verifications, and include verification counts in building data
This commit is contained in:
parent
672a1efde4
commit
b30e882669
@ -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 getBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
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) => {
|
const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
let user_id;
|
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) => {
|
const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
if (!req.session.user_id) {
|
if (!req.session.user_id) {
|
||||||
return res.send({ like: false }); // not logged in, so cannot have liked
|
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 getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
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) => {
|
const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
if (!req.session.user_id) {
|
if (!req.session.user_id) {
|
||||||
return res.send({ error: 'Must be logged in' });
|
return res.send({ error: 'Must be logged in' });
|
||||||
@ -159,6 +162,48 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res:
|
|||||||
res.send(updatedBuilding);
|
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) => {
|
const getLatestRevisionId = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
const revisionId = await buildingService.getLatestRevisionId();
|
const revisionId = await buildingService.getLatestRevisionId();
|
||||||
@ -176,6 +221,8 @@ export default {
|
|||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
getBuildingLikeById,
|
getBuildingLikeById,
|
||||||
updateBuildingLikeById,
|
updateBuildingLikeById,
|
||||||
|
getUserVerifiedAttributes,
|
||||||
|
verifyBuildingAttributes,
|
||||||
getBuildingEditHistoryById,
|
getBuildingEditHistoryById,
|
||||||
getLatestRevisionId
|
getLatestRevisionId
|
||||||
};
|
};
|
||||||
|
88
app/src/api/dataAccess/verify.ts
Normal file
88
app/src/api/dataAccess/verify.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import db from '../../db';
|
||||||
|
import { DatabaseError, InvalidOperationError } from '../errors/general';
|
||||||
|
|
||||||
|
|
||||||
|
export async function getBuildingVerifiedAttributes(buildingId: number): Promise<any[]> {
|
||||||
|
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<any> {
|
||||||
|
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<void> {
|
||||||
|
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<null> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,11 @@ router.route('/:building_id/like.json')
|
|||||||
.get(buildingController.getBuildingLikeById)
|
.get(buildingController.getBuildingLikeById)
|
||||||
.post(buildingController.updateBuildingLikeById);
|
.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')
|
router.route('/:building_id/history.json')
|
||||||
.get(buildingController.getBuildingEditHistoryById);
|
.get(buildingController.getBuildingEditHistoryById);
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import { tileCache } from '../../tiles/rendererDefinition';
|
|||||||
import { BoundingBox } from '../../tiles/types';
|
import { BoundingBox } from '../../tiles/types';
|
||||||
import * as buildingDataAccess from '../dataAccess/building';
|
import * as buildingDataAccess from '../dataAccess/building';
|
||||||
import * as likeDataAccess from '../dataAccess/like';
|
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';
|
import { processBuildingUpdate } from './domainLogic/processBuildingUpdate';
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ async function getBuildingById(id: number) {
|
|||||||
const building = await getCurrentBuildingDataById(id);
|
const building = await getCurrentBuildingDataById(id);
|
||||||
|
|
||||||
building.edit_history = await getBuildingEditHistory(id);
|
building.edit_history = await getBuildingEditHistory(id);
|
||||||
|
building.verified = await getBuildingVerifications(building);
|
||||||
|
|
||||||
return building;
|
return building;
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
@ -159,6 +161,7 @@ async function getBuildingUPRNsById(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type
|
async function saveBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type
|
||||||
|
console.log("SAVE")
|
||||||
return await updateBuildingData(buildingId, userId, async () => {
|
return await updateBuildingData(buildingId, userId, async () => {
|
||||||
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
||||||
|
|
||||||
@ -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 ===
|
// === Utility functions ===
|
||||||
|
|
||||||
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
||||||
@ -378,5 +443,7 @@ export {
|
|||||||
saveBuilding,
|
saveBuilding,
|
||||||
likeBuilding,
|
likeBuilding,
|
||||||
unlikeBuilding,
|
unlikeBuilding,
|
||||||
getLatestRevisionId
|
getLatestRevisionId,
|
||||||
|
verifyBuildingAttributes,
|
||||||
|
getUserVerifiedAttributes
|
||||||
};
|
};
|
||||||
|
@ -89,7 +89,7 @@ async function getUserById(id: string) {
|
|||||||
try {
|
try {
|
||||||
return await db.one(
|
return await db.one(
|
||||||
`SELECT
|
`SELECT
|
||||||
username, email, registered, api_key
|
username, email, date_trunc('minute', registered) as registered, api_key
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
Loading…
Reference in New Issue
Block a user