Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

authenticated users can join public teams #90

Merged
merged 6 commits into from
Aug 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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