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

Add Team BOM api endpoint #4849

Merged
merged 4 commits into from
Dec 3, 2024
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
48 changes: 48 additions & 0 deletions forge/ee/routes/bom/team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const teamShared = require('../../../routes/api/shared/team.js')

module.exports = async function (app) {
app.addHook('preHandler', teamShared.defaultPreHandler.bind(null, app))

app.get('/:teamId/bom', {
preHandler: app.needsPermission('team:bom'),
schema: {
summary: 'Get team BOM',
tags: ['Teams'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'array',
items: {
$ref: 'ApplicationBom'
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const teamType = await request.team.getTeamType()
if (!teamType.getFeatureProperty('bom', false)) {
return reply.code(404).send({ code: 'unexpected_error', error: 'Feature not enabled.' })
}
const applications = await app.db.models.Application.byTeam(request.params.teamId)
const results = []
for (const application of applications) {
const dependants = await application.getChildren({ includeDependencies: true })
const childrenView = dependants.map(child => app.db.views.BOM.dependant(child.model, child.dependencies))
const result = {
id: application.hashid,
name: application.name,
children: childrenView
}
results.push(result)
}
reply.send(results)
})
}
1 change: 1 addition & 0 deletions forge/ee/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = async function (app) {
await app.register(require('./pipeline'), { prefix: '/api/v1', logLevel: app.config.logging.http })
await app.register(require('./deviceEditor'), { prefix: '/api/v1/devices/:deviceId/editor', logLevel: app.config.logging.http })
await app.register(require('./bom/application.js'), { prefix: '/api/v1/applications', logLevel: app.config.logging.http })
await app.register(require('./bom/team.js'), { prefix: '/api/v1/teams', logLevel: app.config.logging.http })

await app.register(require('./flowBlueprints'), { prefix: '/api/v1/flow-blueprints', logLevel: app.config.logging.http })

Expand Down
5 changes: 4 additions & 1 deletion forge/lib/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ const Permissions = {
'project:history': { description: 'View project history', role: Roles.Member },

// Application
'application:bom': { description: 'Get the Bill of Materials', role: Roles.Owner },
'application:bom': { description: 'Get the Application Bill of Materials', role: Roles.Owner },

// Team
'team:bom': { description: 'Get the Team Bill of Materials', role: Roles.Owner },

// Device Groups
'application:device-group:create': { description: 'Create a device group', role: Roles.Owner },
Expand Down
77 changes: 77 additions & 0 deletions forge/routes/api/shared/team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module.exports = {
defaultPreHandler: async (app, request, reply) => {
if (request.params.teamId !== undefined || request.params.teamSlug !== undefined) {
// The route may provide either :teamId or :teamSlug
if (request.params.teamId || request.params.teamSlug) {
let teamId = request.params.teamId
if (request.params.teamSlug) {
// If :teamSlug is provided, need to lookup the team to get
// its id for subsequent checks
request.team = await app.db.models.Team.bySlug(request.params.teamSlug)
if (!request.team) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
return
}
teamId = request.team.hashid
}

try {
if (!request.session.User) {
// If request.session.User is not defined, this request is being
// made with an access token. If it is a project access token,
// ensure that project is in this team
if (request.session.ownerType === 'project') {
// Want this to be as small a query as possible. Sequelize
// doesn't make it easy to just get `TeamId` without doing
// a join on Team table.
const project = await app.db.models.Project.findOne({
where: { id: request.session.ownerId },
include: {
model: app.db.models.Team,
attributes: ['hashid', 'id']
}
})
// Ensure the token's project is in the team being accessed
if (project && project.Team.hashid === teamId) {
return
}
} else if (request.session.ownerType === 'device') {
// Want this to be as small a query as possible. Sequelize
// doesn't make it easy to just get `TeamId` without doing
// a join on Team table.
const device = await app.db.models.Device.findOne({
where: { id: request.session.ownerId },
include: {
model: app.db.models.Team,
attributes: ['hashid', 'id']
}
})
// Ensure the device is in the team being accessed
if (device && device.Team.hashid === teamId) {
return
}
}
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
return
}
request.teamMembership = await request.session.User.getTeamMembership(teamId)
if (!request.teamMembership && !request.session.User?.admin) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
return
}
if (!request.team) {
// For a :teamId route, we can now lookup the full team object
request.team = await app.db.models.Team.byId(request.params.teamId)
if (!request.team) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
}
} catch (err) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
} else {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
}
}
}
77 changes: 2 additions & 75 deletions forge/routes/api/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { Op } = require('sequelize')

const { Roles } = require('../../lib/roles')

const teamShared = require('./shared/team.js')
const TeamDevices = require('./teamDevices.js')
const TeamInvitations = require('./teamInvitations.js')
const TeamMembers = require('./teamMembers.js')
Expand All @@ -19,81 +20,7 @@ const TeamMembers = require('./teamMembers.js')
*
*/
module.exports = async function (app) {
app.addHook('preHandler', async (request, reply) => {
if (request.params.teamId !== undefined || request.params.teamSlug !== undefined) {
// The route may provide either :teamId or :teamSlug
if (request.params.teamId || request.params.teamSlug) {
let teamId = request.params.teamId
if (request.params.teamSlug) {
// If :teamSlug is provided, need to lookup the team to get
// its id for subsequent checks
request.team = await app.db.models.Team.bySlug(request.params.teamSlug)
if (!request.team) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
return
}
teamId = request.team.hashid
}

try {
if (!request.session.User) {
// If request.session.User is not defined, this request is being
// made with an access token. If it is a project access token,
// ensure that project is in this team
if (request.session.ownerType === 'project') {
// Want this to be as small a query as possible. Sequelize
// doesn't make it easy to just get `TeamId` without doing
// a join on Team table.
const project = await app.db.models.Project.findOne({
where: { id: request.session.ownerId },
include: {
model: app.db.models.Team,
attributes: ['hashid', 'id']
}
})
// Ensure the token's project is in the team being accessed
if (project && project.Team.hashid === teamId) {
return
}
} else if (request.session.ownerType === 'device') {
// Want this to be as small a query as possible. Sequelize
// doesn't make it easy to just get `TeamId` without doing
// a join on Team table.
const device = await app.db.models.Device.findOne({
where: { id: request.session.ownerId },
include: {
model: app.db.models.Team,
attributes: ['hashid', 'id']
}
})
// Ensure the device is in the team being accessed
if (device && device.Team.hashid === teamId) {
return
}
}
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
return
}
request.teamMembership = await request.session.User.getTeamMembership(teamId)
if (!request.teamMembership && !request.session.User?.admin) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
return
}
if (!request.team) {
// For a :teamId route, we can now lookup the full team object
request.team = await app.db.models.Team.byId(request.params.teamId)
if (!request.team) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
}
} catch (err) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
} else {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
}
})
app.addHook('preHandler', teamShared.defaultPreHandler.bind(null, app))

app.post('/check-slug', {
preHandler: app.needsPermission('team:create'),
Expand Down
56 changes: 56 additions & 0 deletions test/unit/forge/ee/routes/api/team_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const sinon = require('sinon')
const should = require('should') // eslint-disable-line
const setup = require('../../setup')

const FF_UTIL = require('flowforge-test-utils')
const { Roles } = FF_UTIL.require('forge/lib/roles')

describe('Team API - with billing enabled', function () {
const sandbox = sinon.createSandbox()

Expand All @@ -29,7 +32,28 @@ describe('Team API - with billing enabled', function () {
sandbox.stub(app.billing, 'addProject')
sandbox.stub(app.billing, 'removeProject')

const userBob = await app.factory.createUser({
admin: false,
username: 'bob',
name: 'Bob Solo',
email: 'bob@example.com',
password: 'bbPassword'
})

const userChris = await app.factory.createUser({
admin: false,
username: 'chris',
name: 'Chris Kenobi',
email: 'chris@example.com',
password: 'ccPassword'
})

await app.team.addUser(userBob, { through: { role: Roles.Owner } })
await app.team.addUser(userChris, { through: { role: Roles.Member } })

await login('alice', 'aaPassword')
await login('bob', 'bbPassword')
await login('chris', 'ccPassword')
})

afterEach(async function () {
Expand Down Expand Up @@ -128,4 +152,36 @@ describe('Team API - with billing enabled', function () {
device.editor.should.have.property('connected', true)
})
})

describe('Team BOM', async function () {
beforeEach(async function () {
// enable BOM
const defaultTeamTypeProperties = app.defaultTeamType.properties
defaultTeamTypeProperties.features.bom = true
app.defaultTeamType.properties = defaultTeamTypeProperties
await app.defaultTeamType.save()
})

it('Owner can get BOM', async function () {
const response = await app.inject({
method: 'GET',
url: `/api/v1/teams/${app.team.hashid}/bom`,
cookies: { sid: TestObjects.tokens.bob }
})

response.statusCode.should.equal(200)
const result = response.json()
result.should.have.lengthOf(1)
result[0].name.should.equal('application-1')
})
it('Member can not get BOM', async function () {
const response = await app.inject({
method: 'GET',
url: `/api/v1/teams/${app.team.hashid}/bom`,
cookies: { sid: TestObjects.tokens.chris }
})

response.statusCode.should.equal(403)
})
})
})
Loading