diff --git a/app/src/api/api.ts b/app/src/api/api.ts new file mode 100644 index 00000000..2ab9ba3b --- /dev/null +++ b/app/src/api/api.ts @@ -0,0 +1,141 @@ +import express from 'express'; +import bodyParser from 'body-parser'; + +import { authUser, createUser, getUserById, getNewUserAPIKey } from './services/user'; +import { queryLocation } from './services/search'; + +import buildingsRouter from './routes/buildingsRouter'; + + +const server = express.Router() + +// parse POSTed json body +server.use(bodyParser.json()); + +server.use('/buildings', buildingsRouter); + +// POST new user +server.post('/users', function (req, res) { + const user = req.body; + if (req.session.user_id) { + res.send({ error: 'Already signed in' }); + return + } + + if (user.email) { + if (user.email != user.confirm_email) { + res.send({ error: 'Email did not match confirmation.' }); + return + } + } else { + user.email = null; + } + + createUser(user).then(function (result) { + if (result.user_id) { + req.session.user_id = result.user_id; + res.send({ user_id: result.user_id }); + } else { + req.session.user_id = undefined; + res.send({ error: result.error }); + } + }).catch(function (err) { + console.error(err); + res.send(err) + }); +}); + +// POST user auth +server.post('/login', function (req, res) { + authUser(req.body.username, req.body.password).then(function (user: any) { // TODO: remove any + if (user.user_id) { + req.session.user_id = user.user_id; + } else { + req.session.user_id = undefined; + } + res.send(user); + }).catch(function (error) { + res.send(error); + }) +}); + +// POST user logout +server.post('/logout', function (req, res) { + req.session.user_id = undefined; + req.session.destroy(function (err) { + if (err) { + console.error(err); + res.send({ error: 'Failed to end session' }) + } + res.send({ success: true }); + }); +}); + +// GET own user info +server.get('/users/me', function (req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + + getUserById(req.session.user_id).then(function (user) { + res.send(user); + }).catch(function (error) { + res.send(error); + }); +}); + +// POST generate API key +server.post('/api/key', function (req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + + getNewUserAPIKey(req.session.user_id).then(function (apiKey) { + res.send(apiKey); + }).catch(function (error) { + res.send(error); + }); +}) + +// GET search +server.get('/search', function (req, res) { + const searchTerm = req.query.q; + if (!searchTerm) { + res.send({ + error: 'Please provide a search term' + }) + return + } + queryLocation(searchTerm).then((results) => { + if (typeof (results) === 'undefined') { + res.send({ + error: 'Database error' + }) + return + } + res.send({ + results: results.map(item => { + // map from DB results to GeoJSON Feature objects + const geom = JSON.parse(item.st_asgeojson) + return { + type: 'Feature', + attributes: { + label: item.search_str, + zoom: item.zoom || 9 + }, + geometry: geom + } + }) + }) + }).catch(function (error) { + res.send(error); + }); +}) + +server.use((req, res) => { + res.status(404).json({ error: 'Resource not found'}); +}) + +export default server; diff --git a/app/src/api/controllers/buildingController.ts b/app/src/api/controllers/buildingController.ts new file mode 100644 index 00000000..7987293d --- /dev/null +++ b/app/src/api/controllers/buildingController.ts @@ -0,0 +1,154 @@ +import * as buildingService from '../services/building'; +import * as userService from '../services/user'; + + +// GET buildings +// not implemented - may be useful to GET all buildings, paginated + +// GET buildings at point +function getBuildingsByLocation(req, res) { + const { lng, lat } = req.query; + buildingService.queryBuildingsAtPoint(lng, lat).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +// GET buildings by reference (UPRN/TOID or other identifier) +function getBuildingsByReference(req, res) { + const { key, id } = req.query; + buildingService.queryBuildingsByReference(key, id).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +// GET individual building, POST building updates +function getBuildingById(req, res) { + const { building_id } = req.params; + buildingService.getBuildingById(building_id).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +function updateBuildingById(req, res) { + if (req.session.user_id) { + 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' }); + }); + } else { + res.send({ error: 'Must be logged in' }); + } +} + +function updateBuilding(req, res, userId) { + const { building_id } = req.params; + const building = req.body; + buildingService.saveBuilding(building_id, building, userId).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' }) + ) +} + +// GET building UPRNs +function getBuildingUPRNsById(req, res) { + const { building_id } = req.params; + buildingService.getBuildingUPRNsById(building_id).then(function (result) { + if (typeof (result) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send({ + uprns: result + }); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +// GET/POST like building +function getBuildingLikeById(req, res) { + if (!req.session.user_id) { + res.send({ like: false }); // not logged in, so cannot have liked + return + } + 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' }) + ) +} + +function updateBuildingLikeById(req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + 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' }) + ) + } +} + +export default { + getBuildingsByLocation, + getBuildingsByReference, + getBuildingById, + updateBuildingById, + getBuildingUPRNsById, + getBuildingLikeById, + updateBuildingLikeById +}; \ No newline at end of file diff --git a/app/src/api/routes/buildingsRouter.ts b/app/src/api/routes/buildingsRouter.ts new file mode 100644 index 00000000..a8921ab6 --- /dev/null +++ b/app/src/api/routes/buildingsRouter.ts @@ -0,0 +1,32 @@ +import express from 'express'; + +import buildingController from '../controllers/buildingController'; + +const router = express.Router(); + + +// GET buildings +// not implemented - may be useful to GET all buildings, paginated + +// GET buildings at point +router.get('/locate', buildingController.getBuildingsByLocation); + +// GET buildings by reference (UPRN/TOID or other identifier) +router.get('/reference', buildingController.getBuildingsByReference); + +router.route('/:building_id.json') + // GET individual building + .get(buildingController.getBuildingById) + // POST building updates + .post(buildingController.updateBuildingById); + + +// GET building UPRNs +router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById); + +// GET/POST like building +router.route('/:building_id/like.json') + .get(buildingController.getBuildingLikeById) + .post(buildingController.updateBuildingLikeById); + +export default router; \ No newline at end of file diff --git a/app/src/api/building.ts b/app/src/api/services/building.ts similarity index 99% rename from app/src/api/building.ts rename to app/src/api/services/building.ts index 39bf8d10..4d4befcc 100644 --- a/app/src/api/building.ts +++ b/app/src/api/services/building.ts @@ -2,8 +2,8 @@ * Building data access * */ -import db from '../db'; -import { removeAllAtBbox } from '../tiles/cache'; +import db from '../../db'; +import { removeAllAtBbox } from '../../tiles/cache'; // 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. diff --git a/app/src/api/search.ts b/app/src/api/services/search.ts similarity index 96% rename from app/src/api/search.ts rename to app/src/api/services/search.ts index 08fbebe3..0defa117 100644 --- a/app/src/api/search.ts +++ b/app/src/api/services/search.ts @@ -6,7 +6,7 @@ * - this DOES expose geometry, another reason to keep this clearly separated from building * data */ -import db from '../db'; +import db from '../../db'; function queryLocation(term) { const limit = 5; diff --git a/app/src/api/user.ts b/app/src/api/services/user.ts similarity index 99% rename from app/src/api/user.ts rename to app/src/api/services/user.ts index 7cb3b6c2..da7e2e5d 100644 --- a/app/src/api/user.ts +++ b/app/src/api/services/user.ts @@ -2,7 +2,7 @@ * User data access * */ -import db from '../db'; +import db from '../../db'; function createUser(user) { if (!user.password || user.password.length < 8) { diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 0c824689..c060313b 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -85,7 +85,7 @@ class App extends React.Component { // TODO: add proper types selectBuilding(building) { this.increaseRevision(building.revision_id); // get UPRNs and update - fetch(`/building/${building.building_id}/uprns.json`, { + fetch(`/api/buildings/${building.building_id}/uprns.json`, { method: 'GET', headers:{ 'Content-Type': 'application/json' @@ -106,7 +106,7 @@ class App extends React.Component { // TODO: add proper types }); // get if liked and update - fetch(`/building/${building.building_id}/like.json`, { + fetch(`/api/buildings/${building.building_id}/like.json`, { method: 'GET', headers:{ 'Content-Type': 'application/json' @@ -147,7 +147,7 @@ class App extends React.Component { // TODO: add proper types } likeBuilding(buildingId) { - fetch(`/building/${buildingId}/like.json`, { + fetch(`/api/buildings/${buildingId}/like.json`, { method: 'POST', headers:{ 'Content-Type': 'application/json' @@ -168,7 +168,7 @@ class App extends React.Component { // TODO: add proper types } updateBuilding(buildingId, data){ - fetch(`/building/${buildingId}.json`, { + fetch(`/api/buildings/${buildingId}.json`, { method: 'POST', body: JSON.stringify(data), headers:{ diff --git a/app/src/frontend/building-edit.tsx b/app/src/frontend/building-edit.tsx index 80da4816..e7cc9eb8 100644 --- a/app/src/frontend/building-edit.tsx +++ b/app/src/frontend/building-edit.tsx @@ -176,7 +176,7 @@ class EditForm extends Component { // TODO: add proper types event.preventDefault(); const like = event.target.checked; - fetch(`/building/${this.props.building_id}/like.json`, { + fetch(`/api/buildings/${this.props.building_id}/like.json`, { method: 'POST', headers:{ 'Content-Type': 'application/json' @@ -203,7 +203,7 @@ class EditForm extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}) - fetch(`/building/${this.props.building_id}.json`, { + fetch(`/api/buildings/${this.props.building_id}.json`, { method: 'POST', body: JSON.stringify(this.state), headers:{ diff --git a/app/src/frontend/login.tsx b/app/src/frontend/login.tsx index 569ec4c1..bd8c7302 100644 --- a/app/src/frontend/login.tsx +++ b/app/src/frontend/login.tsx @@ -39,7 +39,7 @@ class Login extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}) - fetch('/login', { + fetch('/api/login', { method: 'POST', body: JSON.stringify(this.state), headers:{ @@ -52,7 +52,7 @@ class Login extends Component { // TODO: add proper types if (res.error) { this.setState({error: res.error}) } else { - fetch('/users/me', { + fetch('/api/users/me', { credentials: 'same-origin' }).then( (res) => res.json() diff --git a/app/src/frontend/map.tsx b/app/src/frontend/map.tsx index 013b04e6..e32c52b2 100644 --- a/app/src/frontend/map.tsx +++ b/app/src/frontend/map.tsx @@ -61,7 +61,7 @@ class ColouringMap extends Component { // TODO: add proper types const newCat = parseCategoryURL(this.props.match.url); const mapCat = newCat || 'age'; fetch( - '/buildings/locate?lat='+lat+'&lng='+lng + '/api/buildings/locate?lat='+lat+'&lng='+lng ).then( (res) => res.json() ).then(function(data){ diff --git a/app/src/frontend/my-account.tsx b/app/src/frontend/my-account.tsx index 3094e5cc..81c77d30 100644 --- a/app/src/frontend/my-account.tsx +++ b/app/src/frontend/my-account.tsx @@ -30,7 +30,7 @@ class MyAccountPage extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}); - fetch('/logout', { + fetch('/api/logout', { method: 'POST', credentials: 'same-origin' }).then( @@ -50,7 +50,7 @@ class MyAccountPage extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}); - fetch('/api/key', { + fetch('/api/api/key', { method: 'POST', credentials: 'same-origin' }).then( @@ -81,7 +81,7 @@ class MyAccountPage extends Component { // TODO: add proper types

-
+
Start colouring @@ -104,7 +104,7 @@ class MyAccountPage extends Component { // TODO: add proper types

Are you a software developer? If so, you might be interested in these.

API key

{this.props.user.api_key? this.props.user.api_key : '-'}

- + diff --git a/app/src/frontend/search-box.tsx b/app/src/frontend/search-box.tsx index 52b5a988..cbc270b1 100644 --- a/app/src/frontend/search-box.tsx +++ b/app/src/frontend/search-box.tsx @@ -78,7 +78,7 @@ class SearchBox extends Component { // TODO: add proper types }) fetch( - '/search?q='+this.state.q + '/api/search?q='+this.state.q ).then( (res) => res.json() ).then((data) => { @@ -160,7 +160,7 @@ class SearchBox extends Component { // TODO: add proper types : null; return (
-
+
diff --git a/app/src/frontend/signup.tsx b/app/src/frontend/signup.tsx index d78263fb..1163d933 100644 --- a/app/src/frontend/signup.tsx +++ b/app/src/frontend/signup.tsx @@ -42,7 +42,7 @@ class SignUp extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}) - fetch('/users', { + fetch('/api/users', { method: 'POST', body: JSON.stringify(this.state), headers:{ @@ -55,7 +55,7 @@ class SignUp extends Component { // TODO: add proper types if (res.error) { this.setState({error: res.error}) } else { - fetch('/users/me', { + fetch('/api/users/me', { credentials: 'same-origin' }).then( (res) => res.json() diff --git a/app/src/server.tsx b/app/src/server.tsx index fb80bbe9..3d46d6ab 100644 --- a/app/src/server.tsx +++ b/app/src/server.tsx @@ -10,25 +10,19 @@ import express from 'express'; import { renderToString } from 'react-dom/server'; import serialize from 'serialize-javascript'; -import bodyParser from 'body-parser'; import session from 'express-session'; import pgConnect from 'connect-pg-simple'; import App from './frontend/app'; import db from './db'; -import { authUser, createUser, getUserById, authAPIUser, getNewUserAPIKey } from './api/user'; +import { getUserById } from './api/services/user'; import { - queryBuildingsAtPoint, - queryBuildingsByReference, getBuildingById, getBuildingLikeById, - getBuildingUPRNsById, - saveBuilding, - likeBuilding, - unlikeBuilding -} from './api/building'; -import { queryLocation } from './api/search'; + getBuildingUPRNsById +} from './api/services/building'; import tileserver from './tiles/tileserver'; +import apiServer from './api/api'; import { parseBuildingURL } from './parse'; // create server @@ -43,8 +37,6 @@ server.disable('x-powered-by'); // serve static files server.use(express.static(process.env.RAZZLE_PUBLIC_DIR)); -// parse POSTed json body -server.use(bodyParser.json()); // handle user sessions const pgSession = pgConnect(session); @@ -162,268 +154,11 @@ function renderHTML(context, data, req, res) { } } -// GET tiles server.use('/tiles', tileserver); -// GET buildings -// not implemented - may be useful to GET all buildings, paginated +server.use('/api', apiServer); -// GET buildings at point -server.get('/buildings/locate', function (req, res) { - const { lng, lat } = req.query; - queryBuildingsAtPoint(lng, lat).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}); - -// GET buildings by reference (UPRN/TOID or other identifier) -server.get('/buildings/reference', function (req, res) { - const { key, id } = req.query; - queryBuildingsByReference(key, id).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}); - -// GET individual building, POST building updates -server.route('/building/:building_id.json') - .get(function (req, res) { - const { building_id } = req.params; - getBuildingById(building_id).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) - }) - .post(function (req, res) { - if (req.session.user_id) { - updateBuilding(req, res, req.session.user_id); - } else if (req.query.api_key) { - 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' }); - }); - } else { - res.send({ error: 'Must be logged in' }); - } - }) - -function updateBuilding(req, res, userId) { - const { building_id } = req.params; - const building = req.body; - saveBuilding(building_id, building, userId).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' }) - ) -} - -// GET building UPRNs -server.get('/building/:building_id/uprns.json', function (req, res) { - const { building_id } = req.params; - getBuildingUPRNsById(building_id).then(function (result) { - if (typeof (result) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send({ - uprns: result - }); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}) - -// GET/POST like building -server.route('/building/:building_id/like.json') - .get(function (req, res) { - if (!req.session.user_id) { - res.send({ like: false }); // not logged in, so cannot have liked - return - } - const { building_id } = req.params; - getBuildingLikeById(building_id, req.session.user_id).then(like => { - // any value returned means like - res.send({ like: like }) - }).catch( - () => res.send({ error: 'Database error' }) - ) - }) - .post(function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - const { building_id } = req.params; - const { like } = req.body; - if (like) { - 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 { - 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' }) - ) - } - }) - -// POST new user -server.post('/users', function (req, res) { - const user = req.body; - if (req.session.user_id) { - res.send({ error: 'Already signed in' }); - return - } - - if (user.email) { - if (user.email != user.confirm_email) { - res.send({ error: 'Email did not match confirmation.' }); - return - } - } else { - user.email = null; - } - - createUser(user).then(function (result) { - if (result.user_id) { - req.session.user_id = result.user_id; - res.send({ user_id: result.user_id }); - } else { - req.session.user_id = undefined; - res.send({ error: result.error }); - } - }).catch(function (err) { - console.error(err); - res.send(err) - }); -}); - -// POST user auth -server.post('/login', function (req, res) { - authUser(req.body.username, req.body.password).then(function (user: any) { // TODO: remove any - if (user.user_id) { - req.session.user_id = user.user_id; - } else { - req.session.user_id = undefined; - } - res.send(user); - }).catch(function (error) { - res.send(error); - }) -}); - -// POST user logout -server.post('/logout', function (req, res) { - req.session.user_id = undefined; - req.session.destroy(function (err) { - if (err) { - console.error(err); - res.send({ error: 'Failed to end session' }) - } - res.send({ success: true }); - }); -}); - -// GET own user info -server.get('/users/me', function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - - getUserById(req.session.user_id).then(function (user) { - res.send(user); - }).catch(function (error) { - res.send(error); - }); -}); - -// POST generate API key -server.post('/api/key', function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - - getNewUserAPIKey(req.session.user_id).then(function (apiKey) { - res.send(apiKey); - }).catch(function (error) { - res.send(error); - }); -}) - -// GET search -server.get('/search', function (req, res) { - const searchTerm = req.query.q; - if (!searchTerm) { - res.send({ - error: 'Please provide a search term' - }) - return - } - queryLocation(searchTerm).then((results) => { - if (typeof (results) === 'undefined') { - res.send({ - error: 'Database error' - }) - return - } - res.send({ - results: results.map(item => { - // map from DB results to GeoJSON Feature objects - const geom = JSON.parse(item.st_asgeojson) - return { - type: 'Feature', - attributes: { - label: item.search_str, - zoom: item.zoom || 9 - }, - geometry: geom - } - }) - }) - }).catch(function (error) { - res.send(error); - }); -}) +// use the frontend route for anything else - will presumably show the 404 page +server.use(frontendRoute); export default server; diff --git a/etl/join_building_data/load_conservation_areas.py b/etl/join_building_data/load_conservation_areas.py index cd84c595..48832423 100644 --- a/etl/join_building_data/load_conservation_areas.py +++ b/etl/join_building_data/load_conservation_areas.py @@ -59,7 +59,7 @@ def save_data(building_id, data, api_key, base_url): """Save data to a building """ r = requests.post( - "{}/building/{}.json?api_key={}".format(base_url, building_id, api_key), + "{}/buildings/{}.json?api_key={}".format(base_url, building_id, api_key), json=data ) diff --git a/etl/join_building_data/load_data.py b/etl/join_building_data/load_data.py index 28ac7eca..4b805844 100644 --- a/etl/join_building_data/load_data.py +++ b/etl/join_building_data/load_data.py @@ -89,7 +89,7 @@ def save_data(building_id, data, api_key, base_url): """Save data to a building """ r = requests.post( - "{}/building/{}.json?api_key={}".format(base_url, building_id, api_key), + "{}/buildings/{}.json?api_key={}".format(base_url, building_id, api_key), json=data )