Skip to content

Commit

Permalink
Merge pull request #90 from developmentseed/join-public-teams-#80
Browse files Browse the repository at this point in the history
authenticated users can join public teams
  • Loading branch information
kamicut authored Aug 23, 2019
2 parents b09ab07 + b748be1 commit 4b02f06
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 14 deletions.
13 changes: 12 additions & 1 deletion app/manage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ const expressPino = require('express-pino-logger')

const { getClients, createClient, deleteClient } = require('./client')
const { login, loginAccept, logout } = require('./login')
const { listTeams, createTeam, getTeam, updateTeam, destroyTeam, addMember, removeMember, updateMembers } = require('./teams')
const { can } = require('./permissions')
const sessionMiddleware = require('./sessions')
const logger = require('../lib/logger')
const {
listTeams,
createTeam,
getTeam,
updateTeam,
destroyTeam,
addMember,
removeMember,
updateMembers,
joinTeam
} = require('./teams')

/**
* The manageRouter handles all routes related to the first party
Expand Down Expand Up @@ -55,6 +65,7 @@ function manageRouter (nextApp) {
router.put('/api/teams/add/:id/:osmId', can('team:update'), addMember)
router.put('/api/teams/remove/:id/:osmId', can('team:update'), removeMember)
router.patch('/api/teams/:id/members', can('team:update'), updateMembers)
router.put('/api/teams/:id/join', can('team:join'), joinTeam)

/**
* Page renders
Expand Down
3 changes: 2 additions & 1 deletion app/manage/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const teamPermissions = {
'team:create': require('./create-team'),
'team:update': require('./update-team'),
'team:view': require('./view-team'),
'team:delete': require('./delete-team')
'team:delete': require('./delete-team'),
'team:join': require('./join-team')
}

const clientPermissions = {
Expand Down
18 changes: 18 additions & 0 deletions app/manage/permissions/join-team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { isPublic, isMember } = require('../../lib/team')

/**
* team:join
*
* To join a team, the team must be public
*
* @param {string} uid user id
* @param {Object} params request parameters
* @returns {boolean} can the request go through?
*/
async function joinTeam (uid, { id }) {
const publicTeam = await isPublic(id)
const member = await isMember(id, uid)
return publicTeam && !member
}

module.exports = joinTeam
24 changes: 23 additions & 1 deletion app/manage/teams.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,27 @@ async function removeMember (req, reply) {
}
}

async function joinTeam (req, reply) {
const { id } = req.params
const osmId = reply.locals.user_id

if (!id) {
return reply.boom.badRequest('team id is required')
}

if (!osmId) {
return reply.boom.badRequest('osm id is required')
}

try {
await team.addMember(id, osmId)
return reply.sendStatus(200)
} catch (err) {
console.log(err)
return reply.boom.badRequest()
}
}

module.exports = {
listTeams,
getTeam,
Expand All @@ -159,5 +180,6 @@ module.exports = {
destroyTeam,
addMember,
updateMembers,
removeMember
removeMember,
joinTeam
}
72 changes: 72 additions & 0 deletions app/tests/permissions/join-team.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const test = require('ava')
const db = require('../../db')
const path = require('path')
const hydra = require('../../lib/hydra')
const sinon = require('sinon')

const team = require('../../lib/team')

const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations')

let agent
test.before(async (t) => {
const conn = await db()
await conn.migrate.latest({ directory: migrationsDirectory })

// seed
await conn('users').insert({ id: 100 })
await conn('users').insert({ id: 101 })

t.context.publicTeam = await team.create({ name: 'public team' }, 100)
t.context.privateTeam = await team.create({ name: 'private team', privacy: 'private' }, 100)

// stub hydra introspect
let introspectStub = sinon.stub(hydra, 'introspect')
introspectStub.withArgs('validToken').returns({
active: true,
sub: '100'
})
introspectStub.withArgs('differentUser').returns({
active: true,
sub: '101'
})
introspectStub.withArgs('invalidToken').returns({ active: false })

agent = require('supertest').agent(await require('../../index')())
})

test.after.always(async () => {
const conn = await db()
await conn.migrate.rollback({ directory: migrationsDirectory })
conn.destroy()
})

test('a user can join a public team', async t => {
const team = t.context.publicTeam
let res = await agent.put(`/api/teams/${team.id}/join`)
.set('Authorization', `Bearer differentUser`)
t.is(res.status, 200)
})

test('a user cannot join a private team', async t => {
const team = t.context.privateTeam
let res = await agent.put(`/api/teams/${team.id}/join`)
.set('Authorization', `Bearer differentUser`)
t.is(res.status, 401)
})

test('a user cannot join a team they are already in', async t => {
const team = t.context.publicTeam
let res = await agent.put(`/api/teams/${team.id}/join`)
.set('Authorization', `Bearer validToken`)
t.is(res.status, 401)
})

test('a user must be authenticated to join a team', async t => {
const team = t.context.publicTeam
let invalidToken = await agent.put(`/api/teams/${team.id}/join`)
.set('Authorization', `Bearer invalidToken`)
t.is(invalidToken.status, 401)
let unauthenticated = await agent.put(`/api/teams/${team.id}/join`)
t.is(unauthenticated.status, 401)
})
16 changes: 16 additions & 0 deletions lib/teams-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,19 @@ export async function addMember (id, osmId) {
export async function removeMember (id, osmId) {
return updateMembers(id, [], [osmId])
}

/**
* joinTeam
* Let current user join team
*
* @param id - Team id
* @returns {Response}
*/
export async function joinTeam (id) {
return fetch(join(URL, `${id}`, 'join'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
}
40 changes: 29 additions & 11 deletions pages/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Table from '../components/table'
import AddMemberForm from '../components/add-member-form'
import theme from '../styles/theme'

import { getTeam, addMember, removeMember } from '../lib/teams-api'
import { getTeam, addMember, removeMember, joinTeam } from '../lib/teams-api'

const Map = dynamic(() => import('../components/team-map'), { ssr: false })

Expand Down Expand Up @@ -54,6 +54,21 @@ export default class Team extends Component {
}
}

async joinTeam () {
const { id, user } = this.props
const osmId = user.uid

try {
await joinTeam(id, osmId)
await this.getTeam(id)
} catch (e) {
console.error(e)
this.setState({
error: e
})
}
}

renderMap (location) {
if (!location) {
return <div>No location specified</div>
Expand Down Expand Up @@ -151,25 +166,26 @@ export default class Team extends Component {

if (!team) return null

// Check if the user is a moderator for this team
const userId = this.props.user.uid
const members = map(prop('id'), team.members)
const moderators = map(prop('osm_id'), team.moderators)
const isUserModerator = contains(parseInt(this.props.user.uid), moderators)

let members = team.members
// TODO: moderators is an array of ints while members are an array of strings. fix this.
const isUserModerator = contains(parseInt(userId), moderators)
const isMember = contains(userId, members)

const columns = [
{ key: 'id' },
{ key: 'name' }
]

let memberRows = team.members
if (isUserModerator) {
columns.push({ key: 'actions' })

members = members.map((member) => {
if (isUserModerator) {
member.actions = (row, index, columns) => {
return this.renderActions(row, index, columns, isUserModerator)
}
memberRows = memberRows.map((member) => {
member.actions = (row, index, columns) => {
return this.renderActions(row, index, columns, isUserModerator)
}

return member
Expand All @@ -180,7 +196,9 @@ export default class Team extends Component {
<article className='inner page team'>
<div className='page__heading'>
<h2>{team.name}</h2>
{ isUserModerator ? <Button variant='primary' href={`/teams/${team.id}/edit`}>Edit Team</Button> : <div /> }
{ isUserModerator ? <Button variant='primary' href={`/teams/${team.id}/edit`}>Edit Team</Button> : ''}
{ userId && !isMember ? <Button variant='primary' onClick={() => this.joinTeam()}>Join Team</Button> : '' }
{ !userId ? <Button variant='primary' href={`/login`}>Sign in to join team</Button> : '' }
</div>
<div className='team__details'>
<Card>
Expand Down Expand Up @@ -211,7 +229,7 @@ export default class Team extends Component {
</div>
</div>
<Table
rows={members}
rows={memberRows}
columns={columns}
/>
</Section>
Expand Down

0 comments on commit 4b02f06

Please sign in to comment.