Skip to content

Commit cfc34f6

Browse files
authored
Merge pull request #233 from developmentseed/feature/private-teams
Feature/private teams
2 parents 8a43037 + 47fcbfb commit cfc34f6

24 files changed

+434
-139
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
exports.up = async (knex) => {
3+
return knex.schema.alterTable('organization', table => {
4+
table.enum('privacy', ['public', 'private', 'unlisted']).defaultTo('public')
5+
table.boolean('teams_can_be_public').defaultTo(true)
6+
})
7+
}
8+
9+
exports.down = async (knex) => {
10+
return knex.schema.alterTable('organization', table => {
11+
table.dropColumn('privacy')
12+
table.dropColumn('teams_can_be_public')
13+
})
14+
}

app/lib/organization.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const orgAttributes = [
88
'id',
99
'name',
1010
'description',
11+
'privacy',
12+
'teams_can_be_public',
1113
'created_at',
1214
'updated_at'
1315
]
@@ -233,7 +235,7 @@ async function getMembers (organizationId, page) {
233235
}
234236

235237
/**
236-
* Checks if an osmId is part of an organization
238+
* Checks if an osmId is part of an organization members
237239
* @param {int} organizationId - organization id
238240
* @param {int} osmId - id of member we are testing
239241
*/
@@ -244,6 +246,23 @@ async function isMember (organizationId, osmId) {
244246
return includes(Number(osmId), map(Number, memberIds))
245247
}
246248

249+
/**
250+
* Checks if an osmId is part of an organization members or staff
251+
* @param {int} organizationId - organization id
252+
* @param {int} osmId - id of member we are testing
253+
*/
254+
async function isMemberOrStaff (organizationId, osmId) {
255+
if (!organizationId) throw new PropertyRequiredError('organization id')
256+
if (!osmId) throw new PropertyRequiredError('osm id')
257+
const conn = await db()
258+
const subquery = conn('organization_team').select('team_id').where('organization_id', organizationId)
259+
const memberQuery = conn('member').select('osm_id').where('team_id', 'in', subquery).andWhere('osm_id', osmId)
260+
const ownerQuery = conn('organization_owner').select('osm_id').where({ organization_id: organizationId, osm_id: osmId })
261+
const managerQuery = conn('organization_manager').select('osm_id').where({ organization_id: organizationId, osm_id: osmId })
262+
const result = await memberQuery.union(ownerQuery).union(managerQuery)
263+
return result.length > 0
264+
}
265+
247266
/**
248267
* Checks if the osm user is an owner of a team
249268
* @param {int} organizationId - organization id
@@ -290,15 +309,29 @@ async function getOrgStaff (options) {
290309

291310
if (options.organizationId) {
292311
ownerQuery = ownerQuery.where('organization.id', options.organizationId)
293-
managerQuery = ownerQuery.where('organization.id', options.organizationId)
312+
managerQuery = managerQuery.where('organization.id', options.organizationId)
294313
}
295314
if (options.osmId) {
296315
ownerQuery = ownerQuery.where('organization_owner.osm_id', options.osmId)
297-
managerQuery = ownerQuery.where('organization_manager.osm_id', options.osmId)
316+
managerQuery = managerQuery.where('organization_manager.osm_id', options.osmId)
298317
}
299318
return ownerQuery.unionAll(managerQuery)
300319
}
301320

321+
/**
322+
* isPublic
323+
* Checks if org privacy is public
324+
*
325+
* @param orgId - ord id
326+
* @returns {Boolean} is the org public?
327+
*/
328+
async function isPublic (orgId) {
329+
if (!orgId) throw new PropertyRequiredError('organization id')
330+
const conn = await db()
331+
const { privacy } = await unpack(conn('organization').where({ id: orgId }))
332+
return (privacy === 'public')
333+
}
334+
302335
module.exports = {
303336
get,
304337
create,
@@ -311,10 +344,12 @@ module.exports = {
311344
getOwners,
312345
getManagers,
313346
getMembers,
347+
isMemberOrStaff,
314348
isOwner,
315349
isManager,
316350
isMember,
317351
createOrgTeam,
318352
listMyOrganizations,
319-
getOrgStaff
353+
getOrgStaff,
354+
isPublic
320355
}

app/lib/team.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,19 @@ async function isMember (teamId, osmId) {
379379
async function isPublic (teamId) {
380380
if (!teamId) throw new Error('team id is required as first argument')
381381
const conn = await db()
382-
const { privacy } = await unpack(conn('team').where({ id: teamId }))
382+
const { privacy } = await unpack(
383+
conn('team')
384+
.select('id', conn.raw(`
385+
case
386+
when (
387+
select teams_can_be_public from organization join organization_team on organization.id = organization_id where team_id = team.id
388+
) = false then 'private'
389+
when privacy = 'private' then 'private'
390+
when privacy = 'public' then 'public'
391+
end privacy
392+
`))
393+
.where({ id: teamId })
394+
)
383395
return (privacy === 'public')
384396
}
385397

app/manage/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const router = require('express-promise-router')()
22
const expressPino = require('express-pino-logger')
3+
const { path } = require('ramda')
34

45
const { getClients, createClient, deleteClient } = require('./client')
56
const { login, loginAccept, logout } = require('./login')
@@ -12,6 +13,7 @@ const {
1213
createTeam,
1314
destroyTeam,
1415
getTeam,
16+
getTeamMembers,
1517
joinTeam,
1618
listTeams,
1719
listMyTeams,
@@ -33,7 +35,8 @@ const {
3335
createOrgTeam,
3436
getOrgTeams,
3537
getOrgMembers,
36-
listMyOrgs
38+
listMyOrgs,
39+
getOrgStaff
3740
} = require('./organizations')
3841

3942
const {
@@ -49,8 +52,8 @@ const {
4952
getTeamProfile
5053
} = require('./profiles')
5154

52-
const { getOrgStaff } = require('../lib/organization')
5355
const { getUserManageToken } = require('../lib/profile')
56+
const organization = require('../lib/organization')
5457

5558
/**
5659
* The manageRouter handles all routes related to the first party
@@ -71,7 +74,7 @@ function manageRouter (nextApp) {
7174
* Home page
7275
*/
7376
router.get('/', (req, res) => {
74-
return nextApp.render(req, res, '/', { user: req.session.user })
77+
return nextApp.render(req, res, '/', { user: path(['session', 'user'], req) })
7578
})
7679

7780
/**
@@ -95,6 +98,7 @@ function manageRouter (nextApp) {
9598
router.get('/api/my/teams', can('public:authenticated'), listMyTeams)
9699
router.post('/api/teams', can('public:authenticated'), createTeam)
97100
router.get('/api/teams/:id', can('team:view'), getTeam)
101+
router.get('/api/teams/:id/members', can('team:view-members'), getTeamMembers)
98102
router.put('/api/teams/:id', can('team:edit'), updateTeam)
99103
router.delete('/api/teams/:id', can('team:edit'), destroyTeam)
100104
router.put('/api/teams/add/:id/:osmId', can('team:edit'), addMember)
@@ -109,10 +113,11 @@ function manageRouter (nextApp) {
109113
*/
110114
router.get('/api/my/organizations', can('public:authenticated'), listMyOrgs)
111115
router.post('/api/organizations', can('public:authenticated'), createOrg)
112-
router.get('/api/organizations/:id', can('public:authenticated'), getOrg) // TODO handle private organizations
116+
router.get('/api/organizations/:id', can('public:authenticated'), getOrg)
113117
router.put('/api/organizations/:id', can('organization:edit'), updateOrg)
114118
router.delete('/api/organizations/:id', can('organization:edit'), destroyOrg)
115-
router.get('/api/organizations/:id/members', can('public:authenticated'), getOrgMembers)
119+
router.get('/api/organizations/:id/staff', can('organization:view-members'), getOrgStaff)
120+
router.get('/api/organizations/:id/members', can('organization:view-members'), getOrgMembers)
116121

117122
router.put('/api/organizations/:id/addOwner/:osmId', can('organization:edit'), addOwner)
118123
router.put('/api/organizations/:id/removeOwner/:osmId', can('organization:edit'), removeOwner)
@@ -166,7 +171,7 @@ function manageRouter (nextApp) {
166171
})
167172

168173
router.get('/teams/create', can('public:authenticated'), async (req, res) => {
169-
const staff = await getOrgStaff(res.locals.user_id)
174+
const staff = await organization.getOrgStaff({ osmId: Number(res.locals.user_id) })
170175
return nextApp.render(req, res, '/team-create', { staff })
171176
})
172177

app/manage/organizations.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function createOrg (req, reply) {
3636
}
3737

3838
/**
39-
* Get an organization
39+
* Get an organization's metadata
4040
* Requires id of organization
4141
*/
4242
async function getOrg (req, reply) {
@@ -48,12 +48,33 @@ async function getOrg (req, reply) {
4848
}
4949

5050
try {
51-
let [data, owners, managers, isMemberOfOrg] = await Promise.all([
51+
let [data, isMemberOfOrg] = await Promise.all([
5252
organization.get(id),
53-
organization.getOwners(id),
54-
organization.getManagers(id),
5553
organization.isMember(id, user_id)
5654
])
55+
reply.send({ ...data, isMemberOfOrg })
56+
} catch (err) {
57+
console.log(err)
58+
return reply.boom.badRequest(err.message)
59+
}
60+
}
61+
62+
/**
63+
* Get an organization's staff
64+
* Requires id of organization
65+
*/
66+
async function getOrgStaff (req, reply) {
67+
const { id } = req.params
68+
69+
if (!id) {
70+
return reply.boom.badRequest('organization id is required')
71+
}
72+
73+
try {
74+
let [owners, managers] = await Promise.all([
75+
organization.getOwners(id),
76+
organization.getManagers(id)
77+
])
5778
const ownerIds = map(prop('osm_id'), owners)
5879
const managerIds = map(prop('osm_id'), managers)
5980
if (ownerIds.length > 0) {
@@ -63,7 +84,7 @@ async function getOrg (req, reply) {
6384
managers = await team.resolveMemberNames(managerIds)
6485
}
6586

66-
reply.send({ ...data, owners, managers, isMemberOfOrg })
87+
reply.send({ owners, managers })
6788
} catch (err) {
6889
console.log(err)
6990
return reply.boom.badRequest(err.message)
@@ -269,5 +290,6 @@ module.exports = {
269290
createOrgTeam,
270291
getOrgTeams,
271292
listMyOrgs,
293+
getOrgStaff,
272294
getOrgMembers
273295
}

app/manage/permissions/edit-key.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ async function editKey (uid, { id }) {
3232
owners = await organization.getOwners(key.owner_org)
3333
}
3434
let osmIds = owners.map(owner => (R.prop('osm_id', owner)).toString())
35-
console.log(osmIds, uid, osmIds.includes(uid))
3635
return osmIds.includes(uid.toString())
3736
}
3837

app/manage/permissions/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ const keyPermissions = {
1717
const teamPermissions = {
1818
'team:edit': require('./edit-team'),
1919
'team:view': require('./view-team'),
20+
'team:view-members': require('./view-team-members'),
2021
'team:join': require('./join-team'),
2122
'team:member': require('./member-team')
2223
}
2324

2425
const organizationPermissions = {
2526
'organization:edit': require('./edit-org'),
2627
'organization:create-team': require('./create-org-team'),
27-
'organization:member': require('./member-org')
28+
'organization:member': require('./member-org'),
29+
'organization:view-members': require('./view-org-members')
2830
}
2931

3032
const clientPermissions = {

app/manage/permissions/member-org.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { isMember, isOwner, isManager } = require('../../lib/organization')
1+
const { isMemberOrStaff } = require('../../lib/organization')
22

33
/**
44
* org:member
@@ -11,12 +11,7 @@ const { isMember, isOwner, isManager } = require('../../lib/organization')
1111
*/
1212
async function memberOrg (uid, { id }) {
1313
try {
14-
const [member, owner, manager] = await Promise.all([
15-
isMember(id, uid),
16-
isOwner(id, uid),
17-
isManager(id, uid)
18-
])
19-
return member || owner || manager
14+
return await isMemberOrStaff(id, uid)
2015
} catch (e) {
2116
return false
2217
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { isPublic, isMemberOrStaff } = require('../../lib/organization')
2+
3+
/**
4+
* org:view-members
5+
*
6+
* To view an org's members, the org needs to be either public
7+
* or the user should be a member of the org
8+
*
9+
* @param {string} uid user id
10+
* @param {Object} params request parameters
11+
* @returns {boolean} can the request go through?
12+
*/
13+
async function viewOrgMembers (uid, { id }) {
14+
try {
15+
const publicOrg = await isPublic(id)
16+
if (publicOrg) return publicOrg
17+
return await isMemberOrStaff(id, uid)
18+
} catch (e) {
19+
console.error(e)
20+
return false
21+
}
22+
}
23+
24+
module.exports = viewOrgMembers
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { isPublic, isMember } = require('../../lib/team')
2+
3+
/**
4+
* team:view-members
5+
*
6+
* To view a team's members, the team needs to be either public
7+
* or the user should be a member of the team
8+
*
9+
* @param {string} uid user id
10+
* @param {Object} params request parameters
11+
* @returns {boolean} can the request go through?
12+
*/
13+
async function viewTeamMembers (uid, { id }) {
14+
const publicTeam = await isPublic(id)
15+
if (publicTeam) return publicTeam
16+
17+
try {
18+
return await isMember(id, uid)
19+
} catch (e) {
20+
return false
21+
}
22+
}
23+
24+
module.exports = viewTeamMembers

0 commit comments

Comments
 (0)