Skip to content

Commit

Permalink
Remove 'accountId' from PlaceEntity to simplify place linkage management
Browse files Browse the repository at this point in the history
    Update API-Place documentation
Add middleware.placeFromParams to map :placeId to req.vPlace
    Add req.vPlace to ExpressJS.Request
    Centralize code to look up place with both id and name
    Lookup domain when fetching place by parameter id
Update /api/v1/places/* to use :placeId
    Use permissions.checkEntityAccess rather than checking fields for permissions
  • Loading branch information
Misterblue committed Oct 26, 2020
1 parent 48e048f commit 36c6a25
Show file tree
Hide file tree
Showing 11 changed files with 80 additions and 115 deletions.
3 changes: 0 additions & 3 deletions docs/API-Places.md
Expand Up @@ -48,7 +48,6 @@ This request return JSON formatted as:
"time_of_last_heartbeat": ISOStringDate,
"num_users": integer
},
"accountId": string,
"thumbnail": URL,
"images": [ URL, URL, ... ]
},
Expand Down Expand Up @@ -84,7 +83,6 @@ domains the requestor is the associated account of.
"time_of_last_heartbeat": ISOStringDate,
"num_users": integer
},
"accountId": string,
"thumbnail": URL,
"images": [ URL, URL, ... ]
},
Expand Down Expand Up @@ -180,7 +178,6 @@ The place fields that can be fetched:
| placeId | all | none | string |
| name | all | domain, owner, admin | string |
| description | all | domain, owner, admin | string |
| accountId | all | none | string |
| domainId | all | none | string |
| address | all | domain, owner, admin | addressString |
| thumbnail | all | domain, owner, admin | string |
Expand Down
14 changes: 2 additions & 12 deletions src/Entities/PlaceEntity.ts
Expand Up @@ -19,7 +19,7 @@ import { AuthToken } from '@Entities/AuthToken';
import { Domains } from '@Entities/Domains';
import { Places } from '@Entities/Places';

import { FieldDefn } from '@Route-Tools/Permissions';
import { checkAccessToEntity, FieldDefn, Perm } from '@Route-Tools/Permissions';
import { isStringValidator, isSArraySet, isPathValidator, isDateValidator } from '@Route-Tools/Permissions';
import { simpleGetter, simpleSetter, sArraySetter, dateStringGetter } from '@Route-Tools/Permissions';
import { getEntityField, setEntityField, getEntityUpdateForField } from '@Route-Tools/Permissions';
Expand All @@ -33,7 +33,6 @@ export class PlaceEntity implements Entity {
public id: string; // globally unique place identifier
public name: string; // Human friendly name of the place
public description: string; // Human friendly description of the place
public accountId: string; // the 'owner' of the place (should be sponsor of the domain)
public domainId: string; // domain the place is in
public address: string; // Address within the domain
public thumbnail: string; // thumbnail for place
Expand Down Expand Up @@ -114,15 +113,6 @@ export const placeFields: { [key: string]: FieldDefn } = {
setter: simpleSetter,
getter: simpleGetter
},
'accountId': {
entity_field: 'accountId',
request_field_name: 'accountId',
get_permissions: [ 'all' ],
set_permissions: [ 'none' ],
validate: isStringValidator,
setter: simpleSetter,
getter: simpleGetter
},
'domainId': {
entity_field: 'domainId',
request_field_name: 'domainId',
Expand All @@ -136,7 +126,7 @@ export const placeFields: { [key: string]: FieldDefn } = {
const maybeDomain = await Domains.getDomainWithId(pVal);
if (IsNotNullOrEmpty(maybeDomain)) {
if (IsNotNullOrEmpty(pAuth)) {
if (pAuth.accountId === maybeDomain.sponsorAccountId) {
if (checkAccessToEntity(pAuth, maybeDomain, [ Perm.SPONSOR, Perm.ADMIN ])) {
valid = true;
}
else {
Expand Down
2 changes: 2 additions & 0 deletions src/RequestAdditions.ts
Expand Up @@ -17,6 +17,7 @@
import { RESTResponse } from './route-tools/RESTResponse';
import { AccountEntity } from './Entities/AccountEntity';
import { DomainEntity } from './Entities/DomainEntity';
import { PlaceEntity } from './Entities/PlaceEntity';
import { SessionEntity } from './Entities/SessionEntity';
import { AuthToken } from '@Entities/AuthToken';

Expand All @@ -34,6 +35,7 @@ declare global {
vAccountError?: string; // if vAccount is not set, the reason why
vUsername?: string; // parameter ':username'
vDomain?: DomainEntity; // pointer to verified DomainEntity
vPlace?: PlaceEntity; // pointer to place entity
vDomainError?: string; // if vDomain is not set, the reason why
vDomainAPIKey?: string; // if domain APIkey supplied in the request, it is passed here
vTokenId?: string; // value from :tokenId
Expand Down
1 change: 0 additions & 1 deletion src/route-tools/Util.ts
Expand Up @@ -246,7 +246,6 @@ export async function buildPlaceInfoSmall(pPlace: PlaceEntity): Promise<any> {
'name': pPlace.name,
'address': pPlace.address,
'description': pPlace.description,
'accountId': pPlace.accountId,
'thumbnail': pPlace.thumbnail,
'images': pPlace.images
};
Expand Down
32 changes: 24 additions & 8 deletions src/route-tools/middleware.ts
Expand Up @@ -184,13 +184,6 @@ export const verifyDomainAccess: RequestHandler = async (req: Request, resp: Res
req.vDomain.sponsorAccountId = aAccount.id;
await Domains.updateEntityFields(req.vDomain, { 'sponsorAccountId': aAccount.id } );
};
// If associating a domain with an account, make sure the domain places also point to this account
for await (const place of Places.enumerateAsync(new GenericFilter({ 'domainId': req.vDomain.id }))) {
const updates: VKeyedCollection = {};
// Not using "setPlaceField" since this field should not ever be set except by this code
place.accountId = aAccount.id;
await Places.updateEntityFields(place, { 'accountId': aAccount.id });
};
};
if (req.vDomain.sponsorAccountId === aToken.accountId) {
verified = true;
Expand Down Expand Up @@ -220,7 +213,6 @@ export const usernameFromParams: RequestHandler = async (req: Request, resp: Res
next();
};

// MetaverseAPI middleware.
// The request has a :domainId label that needs to be looked up and verified.
// Decorate the passed Request with 'vDoamin' which points to a DomainEntity.
// If domain cannot be found or verified, 'vDomainError' is set with text explaining the error.
Expand All @@ -239,6 +231,30 @@ export const domainFromParams: RequestHandler = async (req: Request, resp: Respo
next();
};

// Look for :placeId label and lookup and set req.vPlace and req.vDomain.
// The accepts either the 'placeId' or the name of the place
export const placeFromParams: RequestHandler = async (req: Request, resp: Response, next: NextFunction) => {
if (req.params && req.params.placeId) {
if (typeof(req.params.placeId) === 'string') {
let aPlace = await Places.getPlaceWithId(req.params.placeId);
if (IsNullOrEmpty(aPlace)) {
aPlace = await Places.getPlaceWithName(decodeURIComponent(req.params.placeId));
};
if (aPlace) {
const aDomain = await Domains.getDomainWithId(aPlace.domainId);
if (aDomain) {
req.vPlace = aPlace;
req.vDomain = aDomain;
}
else {
Logger.error(`placeFromParams: lookup Place with bad domain. placeId=${req.params.placeId}, domainId=${aPlace.domainId}`);
};
};
};
};
next();
};

// Find domain apikey from JSON body and set as 'vDomainAPIKey'
export const domainAPIkeyFromBody: RequestHandler = async (req: Request, resp: Response, next: NextFunction) => {
if (req.body && req.body.domain && req.body.domain.api_key) {
Expand Down
1 change: 1 addition & 0 deletions src/routes/api/maint/testSarray.ts
Expand Up @@ -75,6 +75,7 @@ const proctestsarray: RequestHandler = async (req: Request, resp: Response, next
'good path 2': await isPathValidator(undefined, undefined, '/23.5,-44.6,22/0.5639,-0.3355,0.32,0.33'),
'good path 3': await isPathValidator(undefined, undefined, 'domainname/23.5,-44.6,22/0.5639,-0.3355,0.32,0.33'),
'good path 4': await isPathValidator(undefined, undefined, '/-33,20,44/-0.447,0.3232,0.031,1'),
'good path 5': await isPathValidator(undefined, undefined, '/4,5,6/0.343,0.444,-34,1.0'),
'bad path 1': await isPathValidator(undefined, undefined, '0,0,0/0,0,0,1'),
'bad path 2': await isPathValidator(undefined, undefined, '/X,0,0/0,0,0,1'),
'bad path 3': await isPathValidator(undefined, undefined, '/0,0,0/0,0,0,1/'),
Expand Down
3 changes: 2 additions & 1 deletion src/routes/api/v1/domains.ts
Expand Up @@ -56,6 +56,7 @@ const procGetDomains: RequestHandler = async (req: Request, resp: Response, next
next();
};

// Create a domain entry
const procPostDomains: RequestHandler = async (req: Request, resp: Response, next: NextFunction) => {
if (req.vAuthAccount) {
if (req.body && req.body.domain && req.body.domain.label) {
Expand All @@ -72,6 +73,7 @@ const procPostDomains: RequestHandler = async (req: Request, resp: Response, nex
};

// Creating a domain also creates a Place for that domain
// Note that place names are unique so we modify the place name if there is already one.
const newPlacename = await Places.uniqifyPlaceName(newDomain.name);
const newPlace = Places.createPlace();
newPlace.domainId = newDomain.id;
Expand All @@ -83,7 +85,6 @@ const procPostDomains: RequestHandler = async (req: Request, resp: Response, nex
if (req.vAuthToken) {
Logger.debug(`procPostDomains: associating account ${req.vAuthToken.accountId} with new domain ${newDomain.id}`)
newDomain.sponsorAccountId = req.vAuthToken.accountId;
newPlace.accountId = req.vAuthToken.accountId;
};

// Now that the local structures are updated, store the new entries
Expand Down
1 change: 0 additions & 1 deletion src/routes/api/v1/domains/temporary.ts
Expand Up @@ -62,7 +62,6 @@ const procPostDomainsTemporary: RequestHandler = async (req: Request, resp: Resp
if (req.vAuthToken) {
Logger.debug(`procPostDomainsTemporary: associating account ${req.vAuthToken.accountId} with new domain ${newDomain.id}`)
newDomain.sponsorAccountId = req.vAuthToken.accountId;
newPlace.accountId = req.vAuthToken.accountId;
};

// Now that the local structures are updated, store the new entries
Expand Down
1 change: 0 additions & 1 deletion src/routes/api/v1/places.ts
Expand Up @@ -86,7 +86,6 @@ export const procPostPlaces: RequestHandler = async (req: Request, resp: Respons
newPlace.name = requestedName;
newPlace.description = requestedDesc;
newPlace.address = requestedAddr;
newPlace.accountId = aDomain.sponsorAccountId;
newPlace.domainId = aDomain.id;
Places.addPlace(newPlace);

Expand Down
87 changes: 33 additions & 54 deletions src/routes/api/v1/places/placeId.ts
Expand Up @@ -15,7 +15,7 @@
'use strict';

import { Router, RequestHandler, Request, Response, NextFunction } from 'express';
import { setupMetaverseAPI, finishMetaverseAPI, param1FromParams } from '@Route-Tools/middleware';
import { setupMetaverseAPI, finishMetaverseAPI, param1FromParams, placeFromParams } from '@Route-Tools/middleware';
import { accountFromAuthToken } from '@Route-Tools/middleware';

import { checkAccessToEntity, Perm } from '@Route-Tools/Permissions';
Expand Down Expand Up @@ -56,19 +56,9 @@ const procGetPlaces: RequestHandler = async (req: Request, resp: Response, next:
};

export const procGetPlacesPlaceId: RequestHandler = async (req: Request, resp: Response, next: NextFunction) => {
let aPlace = await Places.getPlaceWithId(req.vParam1);
if (IsNullOrEmpty(aPlace)) {
// the request can be made for the ID or the placename
aPlace = await Places.getPlaceWithName(req.vParam1);
}
if (aPlace) {
if (checkAccessToEntity(req.vAuthToken, aPlace, [ Perm.OWNER, Perm.ADMIN ], req.vAuthAccount)) {
req.vRestResp.Data = {
'place': await buildPlaceInfo(aPlace)
};
}
else {
req.vRestResp.respondFailure('unauthorized');
if (req.vPlace) {
req.vRestResp.Data = {
'place': await buildPlaceInfo(req.vPlace)
};
}
else {
Expand All @@ -80,58 +70,47 @@ export const procGetPlacesPlaceId: RequestHandler = async (req: Request, resp: R
// Update place information
// This request happens when a domain is being assigned to another domain
export const procPutPlacesPlaceId: RequestHandler = async (req: Request, resp: Response, next: NextFunction) => {
if (req.vAuthAccount && req.vParam1) {
const aPlace = await Places.getPlaceWithId(req.vParam1);
if (aPlace) {
if (req.vAuthAccount.id === aPlace.accountId || Accounts.isAdmin(req.vAuthAccount)) {
if (req.body.place) {
const updates: VKeyedCollection = {};
if (req.body.place.pointee_query) {
// The caller specified a domain. Either the same domain or changing
if (req.body.place.pointee_query !== aPlace.domainId) {
Logger.info(`procPutPlacesPlaceId: domain changing from ${aPlace.domainId} to ${req.body.place.pointee_query}`)
aPlace.domainId = req.body.place.pointee_query;
updates.domainId = aPlace.domainId;
};
if (req.vAuthAccount && req.vPlace && req.vDomain) {
if (checkAccessToEntity(req.vAuthToken, req.vDomain, [ Perm.SPONSOR, Perm.ADMIN ], req.vAuthAccount)) {
if (req.body.place) {
const updates: VKeyedCollection = {};
if (req.body.place.pointee_query) {
// The caller specified a domain. Either the same domain or changing
if (req.body.place.pointee_query !== req.vPlace.domainId) {
Logger.info(`procPutPlacesPlaceId: domain changing from ${req.vPlace.domainId} to ${req.body.place.pointee_query}`)
req.vPlace.domainId = req.body.place.pointee_query;
updates.domainId = req.vPlace.domainId;
};
for (const field of [ 'path', 'address', 'description', 'thumbnail' ]) {
if (req.body.place.hasOwnProperty(field)) {
await setPlaceField(req.vAuthToken, aPlace, field, req.body.place[field], req.vAuthAccount, updates);
};
};
for (const field of [ 'path', 'address', 'description', 'thumbnail' ]) {
if (req.body.place.hasOwnProperty(field)) {
await setPlaceField(req.vAuthToken, req.vPlace, field, req.body.place[field], req.vAuthAccount, updates);
};
Places.updateEntityFields(aPlace, updates);
}
else {
req.vRestResp.respondFailure('badly formed data');
};
Places.updateEntityFields(req.vPlace, updates);
}
else {
req.vRestResp.respondFailure('unauthorized');
req.vRestResp.respondFailure('badly formed data');
};
}
else {
req.vRestResp.respondFailure('no such place');
req.vRestResp.respondFailure('unauthorized');
};
}
else {
req.vRestResp.respondFailure('no account');
req.vRestResp.respondFailure('no such place');
};
next();
};
// Delete a Place
export const procDeletePlacesPlaceId: RequestHandler = async (req: Request, resp: Response, next: NextFunction) => {
if (req.vAuthAccount && req.vParam1) {
const aPlace = await Places.getPlaceWithId(req.vParam1);
if (aPlace) {
if (req.vAuthAccount.id === aPlace.accountId || Accounts.isAdmin(req.vAuthAccount)) {
await Places.removePlace(aPlace);
}
else {
req.vRestResp.respondFailure('unauthorized');
};
if (req.vAuthAccount && req.vPlace && req.vDomain) {
if (checkAccessToEntity(req.vAuthToken, req.vDomain, [ Perm.SPONSOR, Perm.ADMIN ], req.vAuthAccount)) {
Logger.info(`procDeletePlacesPlaceId: deleting place "${req.vPlace.name}", id=${req.vPlace.id}`);
await Places.removePlace(req.vPlace);
}
else {
req.vRestResp.respondFailure('no such place');
req.vRestResp.respondFailure('unauthorized');
};
}
else {
Expand All @@ -144,21 +123,21 @@ export const name = '/api/v1/places/:placeId';

export const router = Router();

router.get( '/api/v1/places/:param1',
router.get( '/api/v1/places/:placeId',
[ setupMetaverseAPI, // req.vRESTResp
param1FromParams, // req.vParam1
placeFromParams, // req.vPlace, req.vDomain
procGetPlacesPlaceId,
finishMetaverseAPI ] );
router.put( '/api/v1/places/:param1',
router.put( '/api/v1/places/:placeId',
[ setupMetaverseAPI, // req.vRESTResp
accountFromAuthToken, // req.vAuthAccount
param1FromParams, // req.vParam1
placeFromParams, // req.vPlace, req.vDomain
procPutPlacesPlaceId,
finishMetaverseAPI ] );
router.delete( '/api/v1/places/:param1',
router.delete( '/api/v1/places/:placeId',
[ setupMetaverseAPI, // req.vRESTResp
accountFromAuthToken, // req.vAuthAccount
param1FromParams, // req.vParam1
placeFromParams, // req.vPlace, req.vDomain
procDeletePlacesPlaceId,
finishMetaverseAPI ] );

Expand Down

0 comments on commit 36c6a25

Please sign in to comment.