From 3a17913d889b599b9a617ba63b4b81e2dc524fe2 Mon Sep 17 00:00:00 2001 From: sethvincent Date: Wed, 21 Aug 2019 18:06:07 -0700 Subject: [PATCH 1/4] authenticated users can join public teams --- app/manage/index.js | 13 ++++- app/manage/permissions/index.js | 3 +- app/manage/permissions/join-team.js | 18 +++++++ app/manage/teams.js | 24 ++++++++- app/tests/permissions/join-team.test.js | 72 +++++++++++++++++++++++++ lib/teams-api.js | 16 ++++++ pages/team.js | 38 +++++++++---- 7 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 app/manage/permissions/join-team.js create mode 100644 app/tests/permissions/join-team.test.js diff --git a/app/manage/index.js b/app/manage/index.js index 4ad15363..20523bc7 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -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 @@ -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 diff --git a/app/manage/permissions/index.js b/app/manage/permissions/index.js index 83857c0f..abc55840 100644 --- a/app/manage/permissions/index.js +++ b/app/manage/permissions/index.js @@ -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 = { diff --git a/app/manage/permissions/join-team.js b/app/manage/permissions/join-team.js new file mode 100644 index 00000000..6512cc81 --- /dev/null +++ b/app/manage/permissions/join-team.js @@ -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 diff --git a/app/manage/teams.js b/app/manage/teams.js index 1d0435ac..8e5ab2d1 100644 --- a/app/manage/teams.js +++ b/app/manage/teams.js @@ -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, @@ -159,5 +180,6 @@ module.exports = { destroyTeam, addMember, updateMembers, - removeMember + removeMember, + joinTeam } diff --git a/app/tests/permissions/join-team.test.js b/app/tests/permissions/join-team.test.js new file mode 100644 index 00000000..cdd4f906 --- /dev/null +++ b/app/tests/permissions/join-team.test.js @@ -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) +}) diff --git a/lib/teams-api.js b/lib/teams-api.js index d8d86a15..fc304a59 100644 --- a/lib/teams-api.js +++ b/lib/teams-api.js @@ -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' + } + }) +} diff --git a/pages/team.js b/pages/team.js index bc95b8b1..69c1acaa 100644 --- a/pages/team.js +++ b/pages/team.js @@ -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 }) @@ -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
No location specified
@@ -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 @@ -181,6 +197,8 @@ export default class Team extends Component {

{team.name}

{ isUserModerator ? :
} + { userId && !isMember ? :
} + { !userId ? :
}
@@ -211,7 +229,7 @@ export default class Team extends Component {
From bfe7de2dcc09b3340c5361bb145988e42e0f5ef9 Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Thu, 22 Aug 2019 09:22:38 -0700 Subject: [PATCH 2/4] Update pages/team.js Co-Authored-By: Lane Goodman --- pages/team.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/team.js b/pages/team.js index 69c1acaa..f64dcf3a 100644 --- a/pages/team.js +++ b/pages/team.js @@ -198,7 +198,7 @@ export default class Team extends Component {

{team.name}

{ isUserModerator ? :
} { userId && !isMember ? :
} - { !userId ? :
} + { !userId ? : '' }
From 3387f798647c84fa8824213ed6f6e5ec2a857095 Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Thu, 22 Aug 2019 09:22:50 -0700 Subject: [PATCH 3/4] Update pages/team.js Co-Authored-By: Lane Goodman --- pages/team.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/team.js b/pages/team.js index f64dcf3a..03c044ad 100644 --- a/pages/team.js +++ b/pages/team.js @@ -196,7 +196,7 @@ export default class Team extends Component {

{team.name}

- { isUserModerator ? :
} + { isUserModerator ? : ''} { userId && !isMember ? :
} { !userId ? : '' }
From 56681159f2c187768a158b928ec29d79a53ee88f Mon Sep 17 00:00:00 2001 From: Seth Vincent Date: Thu, 22 Aug 2019 09:25:02 -0700 Subject: [PATCH 4/4] Update pages/team.js --- pages/team.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/team.js b/pages/team.js index 03c044ad..96c2d9e9 100644 --- a/pages/team.js +++ b/pages/team.js @@ -197,7 +197,7 @@ export default class Team extends Component {

{team.name}

{ isUserModerator ? : ''} - { userId && !isMember ? :
} + { userId && !isMember ? : '' } { !userId ? : '' }