1
0
Fork 0
mirror of https://github.com/openstf/stf synced 2025-10-04 10:19:30 +02:00

Merge branch 'master' into feature/separate-device-logs

This commit is contained in:
Karol Wrótniak 2020-02-08 01:40:43 +01:00 committed by GitHub
commit c3a51cf867
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
155 changed files with 17373 additions and 1235 deletions

View file

@ -1,5 +1,6 @@
Copyright © 2013 CyberAgent, Inc.
Copyright © 2016 The OpenSTF Project
Copyright © 2019 Orange SA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -5,20 +5,48 @@
## E2E Frontend
### On first run
## On first run
- `gulp webdriver-update`
### Chrome Local STF
- Connect a device
- Run stf
- `gulp protractor`
### Multiple Browsers Local STF with a specific suite
## Protractor&Jasmine - Local STF tests
---
#### Preconditions
Test configuration point to Google Chrome browser. Test works on Google Chrome v.77.0.3865.75 together with chromedriver with ver. 77.0.3865.40.
---
- Connect a device or start android emulator
- Run RethinkDb
```
rethinkdb
```
- Run stf
```
./bin/stf local
```
Wait till STF will be fully functional and devices will be discovered
- Run tests
```
gulp protractor
```
---
#### Info
Test results can be found in:
test-results/reports-protractor/dashboardReport-protractor/index.html
---
## Multiple Browsers Local STF with a specific suite
- Connect a device
- Run stf
- `gulp protractor --multi --suite devices`
### Chrome Remote STF
## Chrome Remote STF
- `export STF_URL='http://stf-url/#!/'`
- `export STF_USERNAME='user'`
- `export STF_PASSWORD='pass'`

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports.command = 'api'
module.exports.describe = 'Start an API unit.'
@ -18,6 +22,18 @@ module.exports.builder = function(yargs) {
, array: true
, demand: true
})
.option('connect-push-dev', {
alias: 'pd'
, describe: 'Device-side ZeroMQ PULL endpoint to connect to.'
, array: true
, demand: true
})
.option('connect-sub-dev', {
alias: 'sd'
, describe: 'Device-side ZeroMQ PUB endpoint to connect to.'
, array: true
, demand: true
})
.option('port', {
alias: 'p'
, describe: 'The port to bind to.'
@ -53,6 +69,8 @@ module.exports.handler = function(argv) {
, endpoints: {
push: argv.connectPush
, sub: argv.connectSub
, pushdev: argv.connectPushDev
, subdev: argv.connectSubDev
}
})
}

View file

@ -30,16 +30,13 @@ module.exports.handler = function() {
var proc = cp.spawn(command, args, options)
var stdout = []
proc.stdout.on('readable', function() {
var chunk
while ((chunk = proc.stdout.read())) {
stdout.push(chunk)
}
proc.stdout.on('data', function(data) {
stdout.push(data)
})
proc.on('error', reject)
proc.on('exit', function(code, signal) {
proc.on('close', function(code, signal) {
if (signal) {
reject(new CheckError('Exited with signal %s', signal))
}

View file

@ -0,0 +1,39 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports.command = 'generate-fake-group'
module.exports.builder = function(yargs) {
return yargs
.strict()
.option('n', {
alias: 'number'
, describe: 'How many groups to create.'
, type: 'number'
, default: 1
})
}
module.exports.handler = function(argv) {
var logger = require('../../util/logger')
var log = logger.createLogger('cli:generate-fake-group')
var fake = require('../../util/fakegroup')
var n = argv.number
function next() {
return fake.generate().then(function(email) {
log.info('Created fake group "%s"', email)
return --n ? next() : null
})
}
return next()
.then(function() {
process.exit(0)
})
.catch(function(err) {
log.fatal('Fake group creation had an error:', err.stack)
process.exit(1)
})
}

View file

@ -0,0 +1,39 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports.command = 'generate-fake-user'
module.exports.builder = function(yargs) {
return yargs
.strict()
.option('n', {
alias: 'number'
, describe: 'How many users to create.'
, type: 'number'
, default: 1
})
}
module.exports.handler = function(argv) {
var logger = require('../../util/logger')
var log = logger.createLogger('cli:generate-fake-user')
var fake = require('../../util/fakeuser')
var n = argv.number
function next() {
return fake.generate().then(function(email) {
log.info('Created fake user "%s"', email)
return --n ? next() : null
})
}
return next()
.then(function() {
process.exit(0)
})
.catch(function(err) {
log.fatal('Fake user creation had an error:', err.stack)
process.exit(1)
})
}

View file

@ -0,0 +1,51 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports.command = 'groups-engine'
module.exports.describe = 'Start the groups engine unit.'
module.exports.builder = function(yargs) {
return yargs
.env('STF_GROUPS_ENGINE')
.strict()
.option('connect-push', {
alias: 'c'
, describe: 'App-side ZeroMQ PULL endpoint to connect to.'
, array: true
, demand: true
})
.option('connect-sub', {
alias: 'u'
, describe: 'App-side ZeroMQ PUB endpoint to connect to.'
, array: true
, demand: true
})
.option('connect-push-dev', {
alias: 'pd'
, describe: 'Device-side ZeroMQ PULL endpoint to connect to.'
, array: true
, demand: true
})
.option('connect-sub-dev', {
alias: 'sd'
, describe: 'Device-side ZeroMQ PUB endpoint to connect to.'
, array: true
, demand: true
})
.epilog('Each option can be be overwritten with an environment variable ' +
'by converting the option to uppercase, replacing dashes with ' +
'underscores and prefixing it with `STF_GROUPS_ENGINE_` .)')
}
module.exports.handler = function(argv) {
return require('../../units/groups-engine')({
endpoints: {
push: argv.connectPush
, sub: argv.connectSub
, pushdev: argv.connectPushDev
, subdev: argv.connectSubDev
}
})
}

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var yargs = require('yargs')
var Promise = require('bluebird')
@ -12,9 +16,12 @@ var _argv = yargs.usage('Usage: $0 <command> [options]')
.command(require('./auth-oauth2'))
.command(require('./auth-openid'))
.command(require('./auth-saml2'))
.command(require('./groups-engine'))
.command(require('./device'))
.command(require('./doctor'))
.command(require('./generate-fake-device'))
.command(require('./generate-fake-user'))
.command(require('./generate-fake-group'))
.command(require('./local'))
.command(require('./log-rethinkdb'))
.command(require('./migrate'))

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports.command = 'local [serial..]'
module.exports.describe = 'Start a complete local development environment.'
@ -337,6 +341,17 @@ module.exports.handler = function(argv) {
, '--secret', argv.authSecret
, '--connect-push', argv.bindAppPull
, '--connect-sub', argv.bindAppPub
, '--connect-push-dev', argv.bindDevPull
, '--connect-sub-dev', argv.bindDevPub
])
// groups engine
, procutil.fork(path.resolve(__dirname, '..'), [
'groups-engine'
, '--connect-push', argv.bindAppPull
, '--connect-sub', argv.bindAppPub
, '--connect-push-dev', argv.bindDevPull
, '--connect-sub-dev', argv.bindDevPub
])
// websocket

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports.command = 'migrate'
module.exports.describe = 'Migrates the database to the latest version.'
@ -10,13 +14,44 @@ module.exports.handler = function() {
var logger = require('../../util/logger')
var log = logger.createLogger('cli:migrate')
var db = require('../../db')
var dbapi = require('../../db/api')
const apiutil = require('../../util/apiutil')
const Promise = require('bluebird')
return db.setup()
.then(function() {
process.exit(0)
return new Promise(function(resolve, reject) {
setTimeout(function() {
return dbapi.getGroupByIndex(apiutil.ROOT, 'privilege').then(function(group) {
if (!group) {
const env = {
STF_ROOT_GROUP_NAME: 'Common'
, STF_ADMIN_NAME: 'administrator'
, STF_ADMIN_EMAIL: 'administrator@fakedomain.com'
}
for (const i in env) {
if (process.env[i]) {
env[i] = process.env[i]
}
}
return dbapi.createBootStrap(env)
}
return group
})
.then(function() {
resolve(true)
})
.catch(function(err) {
reject(err)
})
}, 1000)
})
})
.catch(function(err) {
log.fatal('Migration had an error:', err.stack)
process.exit(1)
})
.finally(function() {
process.exit(0)
})
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var r = require('rethinkdb')
module.exports = {
@ -50,9 +54,30 @@ module.exports = {
return device('provider')('channel')
}
}
, group: {
indexFunction: function(device) {
return device('group')('id')
}
}
}
}
, logs: {
primaryKey: 'id'
}
, groups: {
primaryKey: 'id'
, indexes: {
privilege: null
, owner: {
indexFunction: function(group) {
return group('owner')('email')
}
}
, startTime: {
indexFunction: function(group) {
return group('dates').nth(0)('start')
}
}
}
}
}

View file

@ -1,79 +1,527 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var _ = require('lodash')
var Promise = require('bluebird')
var dbapi = require('../../../db/api')
var logger = require('../../../util/logger')
var datautil = require('../../../util/datautil')
var log = logger.createLogger('api:controllers:devices')
module.exports = {
getDevices: getDevices
, getDeviceBySerial: getDeviceBySerial
const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const util = require('util')
const uuid = require('uuid')
const wire = require('../../../wire')
const wireutil = require('../../../wire/util')
const wirerouter = require('../../../wire/router')
/* ------------------------------------ PRIVATE FUNCTIONS ------------------------------- */
function filterGenericDevices(req, res, devices) {
apiutil.respond(res, 200, 'Devices Information', {
devices: devices.map(function(device) {
return apiutil.filterDevice(req, device)
})
})
}
function getDevices(req, res) {
var fields = req.swagger.params.fields.value
dbapi.loadDevices()
.then(function(cursor) {
return Promise.promisify(cursor.toArray, cursor)()
.then(function(list) {
var deviceList = []
list.forEach(function(device) {
datautil.normalize(device, req.user)
var responseDevice = device
function getGenericDevices(req, res, loadDevices) {
loadDevices(req.user.groups.subscribed).then(function(devices) {
filterGenericDevices(req, res, devices)
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to load device list: ', err.stack)
})
}
function getDeviceFilteredGroups(serial, fields, bookingOnly) {
return dbapi.getDeviceGroups(serial).then(function(groups) {
return Promise.map(groups, function(group) {
return !bookingOnly || !apiutil.isOriginGroup(group.class) ?
group :
'filtered'
})
.then(function(groups) {
return _.without(groups, 'filtered').map(function(group) {
if (fields) {
responseDevice = _.pick(device, fields.split(','))
return _.pick(apiutil.publishGroup(group), fields.split(','))
}
deviceList.push(responseDevice)
return apiutil.publishGroup(group)
})
})
})
}
res.json({
success: true
, devices: deviceList
function extractStandardizableDevices(devices) {
return dbapi.getTransientGroups().then(function(groups) {
return Promise.map(devices, function(device) {
return Promise.map(groups, function(group) {
if (group.devices.indexOf(device.serial) > -1) {
return Promise.reject('booked')
}
return true
})
.then(function() {
return device
})
.catch(function(err) {
if (err !== 'booked') {
throw err
}
return err
})
})
.then(function(devices) {
return _.without(devices, 'booked')
})
})
}
function getStandardizableDevices(req, res) {
dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
extractStandardizableDevices(devices).then(function(devices) {
filterGenericDevices(req, res, devices)
})
})
.catch(function(err) {
log.error('Failed to load device list: ', err.stack)
res.status(500).json({
success: false
apiutil.internalError(res, 'Failed to load device list: ', err.stack)
})
}
function removeDevice(serial, req, res) {
const presentState = req.swagger.params.present.value
const bookingState = req.swagger.params.booked.value
const notesState = req.swagger.params.annotated.value
const controllingState = req.swagger.params.controlled.value
const anyPresentState = typeof presentState === 'undefined'
const anyBookingState = typeof bookingState === 'undefined'
const anyNotesState = typeof notesState === 'undefined'
const anyControllingState = typeof controllingState === 'undefined'
const lock = {}
function deleteGroupDevice(email, id) {
const lock = {}
return dbapi.lockGroupByOwner(email, id).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const group = lock.group = stats.changes[0].new_val
if (group.devices.indexOf(serial) > -1) {
return apiutil.isOriginGroup(group.class) ?
dbapi.removeOriginGroupDevice(group, serial) :
dbapi.removeGroupDevices(group, [serial])
}
return group
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function deleteDeviceInDatabase() {
function wrappedDeleteDeviceInDatabase() {
const result = {
status: false
, data: 'not deleted'
}
return dbapi.loadDeviceBySerial(serial).then(function(device) {
if (device && device.group.id === device.group.origin) {
return deleteGroupDevice(device.group.owner.email, device.group.id)
.then(function(group) {
if (group !== 'not found') {
return dbapi.deleteDevice(serial).then(function() {
result.status = true
result.data = 'deleted'
})
}
return false
})
}
return false
})
.then(function() {
return result
})
}
return apiutil.setIntervalWrapper(
wrappedDeleteDeviceInDatabase
, 10
, Math.random() * 500 + 50)
}
return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const device = lock.device = stats.changes[0].new_val
if (!anyPresentState && device.present !== presentState ||
!anyControllingState && (device.owner === null) === controllingState ||
!anyNotesState &&
(typeof device.notes !== 'undefined' && device.notes !== '') !== notesState ||
!anyBookingState && (device.group.id !== device.group.origin && !bookingState ||
device.group.class === apiutil.STANDARD && bookingState)) {
return 'unchanged'
}
if (device.group.class === apiutil.STANDARD) {
return deleteDeviceInDatabase()
}
return dbapi.getDeviceTransientGroups(serial).then(function(groups) {
if (groups.length && !anyBookingState && !bookingState) {
return 'unchanged'
}
return Promise.each(groups, function(group) {
return deleteGroupDevice(group.owner.email, group.id)
})
.then(function() {
if (!groups.length && !anyBookingState && bookingState) {
return 'unchanged'
}
return deleteDeviceInDatabase()
})
})
})
.finally(function() {
lockutil.unlockDevice(lock)
})
}
/* ------------------------------------ PUBLIC FUNCTIONS ------------------------------- */
function getDevices(req, res) {
const target = req.swagger.params.target.value
switch(target) {
case apiutil.BOOKABLE:
getGenericDevices(req, res, dbapi.loadBookableDevices)
break
case apiutil.ORIGIN:
getGenericDevices(req, res, dbapi.loadDevicesByOrigin)
break
case apiutil.STANDARD:
getGenericDevices(req, res, dbapi.loadStandardDevices)
break
case apiutil.STANDARDIZABLE:
getStandardizableDevices(req, res)
break
default:
getGenericDevices(req, res, dbapi.loadDevices)
}
}
function getDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
var fields = req.swagger.params.fields.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
})
}
datautil.normalize(device, req.user)
var responseDevice = device
let responseDevice = apiutil.publishDevice(device, req.user)
if (fields) {
responseDevice = _.pick(device, fields.split(','))
}
res.json({
success: true
, device: responseDevice
})
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
log.error('Failed to load device "%s": ', serial, err.stack)
res.status(500).json({
success: false
})
})
}
function getDeviceGroups(req, res) {
const serial = req.swagger.params.serial.value
const fields = req.swagger.params.fields.value
dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) {
return cursor.toArray()
})
.then(function(devices) {
if (!devices.length) {
apiutil.respond(res, 404, 'Not Found (device)')
}
else {
getDeviceFilteredGroups(serial, fields, false)
.then(function(groups) {
return apiutil.respond(res, 200, 'Groups Information', {groups: groups})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get device groups: ', err.stack)
})
}
function getDeviceBookings(req, res) {
const serial = req.swagger.params.serial.value
const fields = req.swagger.params.fields.value
dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) {
return cursor.toArray()
})
.then(function(devices) {
if (!devices.length) {
apiutil.respond(res, 404, 'Not Found (device)')
}
else {
getDeviceFilteredGroups(serial, fields, true)
.then(function(bookings) {
apiutil.respond(res, 200, 'Bookings Information', {bookings: bookings})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get device bookings: ', err.stack)
})
}
function addOriginGroupDevices(req, res) {
const serials = apiutil.getBodyParameter(req.body, 'serials')
const fields = apiutil.getQueryParameter(req.swagger.params.fields)
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
const lock = {}
function askUpdateDeviceOriginGroup(group, serial) {
return new Promise(function(resolve, reject) {
const signature = util.format('%s', uuid.v4()).replace(/-/g, '')
let messageListener
const responseTimer = setTimeout(function() {
req.options.channelRouter.removeListener(wireutil.global, messageListener)
apiutil.respond(res, 504, 'Gateway Time-out')
reject('timeout')
}, 5000)
messageListener = wirerouter()
.on(wire.DeviceOriginGroupMessage, function(channel, message) {
if (message.signature === signature) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
dbapi.loadDeviceBySerial(serial).then(function(device) {
if (fields) {
resolve(_.pick(apiutil.publishDevice(device, req.user), fields.split(',')))
}
else {
resolve(apiutil.publishDevice(device, req.user))
}
})
}
})
.handler()
req.options.channelRouter.on(wireutil.global, messageListener)
return dbapi.askUpdateDeviceOriginGroup(serial, group, signature)
})
}
function updateDeviceOriginGroup(group, serial) {
const lock = {}
return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.device = stats.changes[0].new_val
return dbapi.isUpdateDeviceOriginGroupAllowed(serial, group)
.then(function(updatingAllowed) {
if (!updatingAllowed) {
apiutil.respond(res, 403, 'Forbidden (device is currently booked)')
return Promise.reject('booked')
}
return askUpdateDeviceOriginGroup(group, serial)
})
})
.finally(function() {
lockutil.unlockDevice(lock)
})
}
function updateDevicesOriginGroup(group, serials) {
let results = []
return Promise.each(serials, function(serial) {
return updateDeviceOriginGroup(group, serial).then(function(result) {
results.push(result)
})
})
.then(function() {
const result = target === 'device' ? {device: {}} : {devices: []}
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`, result)
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
if (target === 'device') {
result.device = results[0]
}
else {
result.devices = results
}
return apiutil.respond(res, 200, `Updated (${target})`, result)
})
.catch(function(err) {
if (err !== 'booked' && err !== 'timeout' && err !== 'busy') {
throw err
}
})
}
return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
if (!apiutil.isOriginGroup(group.class)) {
return apiutil.respond(res, 400, 'Bad Request (this group cannot act as an origin one)')
}
if (typeof serials !== 'undefined') {
return updateDevicesOriginGroup(
group
, _.without(serials.split(','), '').filter(function(serial) {
return group.devices.indexOf(serial) < 0
})
)
}
return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
if (group.class === apiutil.BOOKABLE) {
return devices
}
return extractStandardizableDevices(devices)
})
.then(function(devices) {
const serials = []
devices.forEach(function(device) {
if (group.devices.indexOf(device.serial) < 0) {
serials.push(device.serial)
}
})
return updateDevicesOriginGroup(group, serials)
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to update ${target} origin group: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function addOriginGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', addOriginGroupDevices, req, res)
}
function removeOriginGroupDevices(req, res) {
const lock = {}
return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
if (!apiutil.checkBodyParameter(req.body, 'serials')) {
req.body = {serials: group.devices.join()}
}
return dbapi.getRootGroup().then(function(group) {
req.swagger.params.id = {value: group.id}
return addOriginGroupDevices(req, res)
})
}
return false
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeOriginGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', removeOriginGroupDevices, req, res)
}
function deleteDevices(req, res) {
const serials = apiutil.getBodyParameter(req.body, 'serials')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
function removeDevices(serials) {
let results = []
return Promise.each(serials, function(serial) {
return removeDevice(serial, req, res).then(function(result) {
if (result === 'not deleted') {
apiutil.respond(res, 503, 'Server too busy [code: 2], please try again later')
return Promise.reject('busy')
}
return results.push(result)
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`)
}
if (!_.without(results, 'not found').length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
return apiutil.respond(res, 200, `Deleted (${target})`)
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
(function() {
if (typeof serials === 'undefined') {
return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
return removeDevices(devices.map(function(device) {
return device.serial
}))
})
}
else {
return removeDevices(_.without(serials.split(','), ''))
}
})()
.catch(function(err) {
apiutil.internalError(res, `Failed to delete ${target}: `, err.stack)
})
}
function deleteDevice(req, res) {
apiutil.redirectApiWrapper('serial', deleteDevices, req, res)
}
module.exports = {
getDevices: getDevices
, getDeviceBySerial: getDeviceBySerial
, getDeviceGroups: getDeviceGroups
, getDeviceBookings: getDeviceBookings
, addOriginGroupDevice: addOriginGroupDevice
, addOriginGroupDevices: addOriginGroupDevices
, removeOriginGroupDevice: removeOriginGroupDevice
, removeOriginGroupDevices: removeOriginGroupDevices
, deleteDevice: deleteDevice
, deleteDevices: deleteDevices
}

View file

@ -0,0 +1,931 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const _ = require('lodash')
const dbapi = require('../../../db/api')
const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const util = require('util')
const uuid = require('uuid')
const Promise = require('bluebird')
const usersapi = require('./users')
/* ---------------------------------- PRIVATE FUNCTIONS --------------------------------- */
function groupApiWrapper(email, fn, req, res) {
dbapi.loadUser(email).then(function(user) {
if (!user) {
apiutil.respond(res, 404, 'Not Found (user)')
}
else {
req.user = user
fn(req, res)
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to wrap "%s": ', fn.name, err.stack)
})
}
function getDevice(req, serial) {
return dbapi.loadDeviceBySerial(serial).then(function(device) {
if (!device) {
throw new Error(`Device not found: ${serial}`)
}
return apiutil.filterDevice(req, device)
})
}
function checkConflicts(id, devices, dates) {
function computeConflicts(conflicts, liteGroup, otherGroup) {
if (otherGroup.id !== liteGroup.id) {
const devices = _.intersection(liteGroup.devices, otherGroup.devices)
if (devices.length) {
for (let liteGroupDate of liteGroup.dates) {
for (let otherGroupDate of otherGroup.dates) {
if (liteGroupDate.start < otherGroupDate.stop &&
liteGroupDate.stop > otherGroupDate.start) {
conflicts.push({
devices: devices
, date: {
start: new Date(
Math.max(liteGroupDate.start.getTime()
, otherGroupDate.start.getTime()))
, stop: new Date(
Math.min(liteGroupDate.stop.getTime()
, otherGroupDate.stop.getTime()))
}
, group: otherGroup.name
, owner: otherGroup.owner
})
}
}
}
}
}
}
return dbapi.getTransientGroups().then(function(groups) {
const conflicts = []
groups.forEach(function(otherGroup) {
computeConflicts(
conflicts
, {id: id, devices: devices, dates: dates}
, otherGroup)
})
return conflicts
})
}
function checkSchedule(res, oldGroup, _class, email, repetitions, privilege, start, stop) {
if (oldGroup && oldGroup.devices.length &&
(apiutil.isOriginGroup(_class) && !apiutil.isOriginGroup(oldGroup.class) ||
apiutil.isOriginGroup(oldGroup.class) && !apiutil.isOriginGroup(_class))) {
return Promise.resolve(apiutil.respond(res, 403,
'Forbidden (unauthorized class while device list is not empty)'))
}
if (apiutil.isAdminGroup(_class) && privilege === apiutil.USER) {
return Promise.resolve(apiutil.respond(res, 403, 'Forbidden (unauthorized class)'))
}
if (isNaN(start.getTime())) {
return Promise.resolve(apiutil.respond(res, 400, 'Bad Request (Invalid startTime format)'))
}
if (isNaN(stop.getTime())) {
return Promise.resolve(apiutil.respond(res, 400, 'Bad Request (Invalid stopTime format)'))
}
if (start >= stop) {
return Promise.resolve(
apiutil.respond(res, 400, 'Bad Request (Invalid life time: startTime >= stopTime)'))
}
if ((stop - start) > apiutil.CLASS_DURATION[_class]) {
return Promise.resolve(apiutil.respond(res, 400,
'Bad Request (Invalid Life time & class combination: life time > class duration)'
))
}
switch(_class) {
case apiutil.BOOKABLE:
case apiutil.STANDARD:
case apiutil.ONCE:
if (repetitions !== 0) {
return Promise.resolve(
apiutil.respond(res, 400, 'Bad Request (Invalid class & repetitions combination)'))
}
break
default:
if (repetitions === 0) {
return Promise.resolve(
apiutil.respond(res, 400, 'Bad Request (Invalid class & repetitions combination)'))
}
break
}
return dbapi.loadUser(email).then(function(owner) {
if (repetitions > owner.groups.quotas.repetitions) {
return apiutil.respond(res, 400, 'Bad Request (Invalid repetitions value)')
}
return true
})
}
/* ---------------------------------- PUBLIC FUNCTIONS ------------------------------------- */
function addGroupDevices(req, res) {
const id = req.swagger.params.id.value
const serials = apiutil.getBodyParameter(req.body, 'serials')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
const lock = {}
let email = null
function addGroupDevice(group, serial) {
const lock = {}
return dbapi.lockBookableDevice(req.user.groups.subscribed, serial).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.device = stats.changes[0].new_val
return dbapi.lockGroup(lock.device.group.origin).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.group = {id: lock.device.group.origin}
return checkConflicts(id, [serial], group.dates).then(function(conflicts) {
return conflicts.length ?
Promise.reject(conflicts) :
dbapi.addGroupDevices(group, [serial])
})
})
.finally(function() {
lockutil.unlockGroup(lock)
})
})
.finally(function() {
lockutil.unlockDevice(lock)
})
}
function _addGroupDevices(lockedGroup, serials) {
let results = []
let group = lockedGroup
return Promise.each(serials, function(serial) {
return addGroupDevice(group, serial).then(function(result) {
results.push(result)
if (result.hasOwnProperty('id')) {
group = result
}
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
else {
results = _.without(results, 'not found')
if (!results.length) {
apiutil.respond(res, 404, `Not Found (group ${target})`)
}
else {
apiutil.respond(res, 200, `Added (group ${target})`
, {group: apiutil.publishGroup(results[results.length - 1])})
}
}
})
.catch(function(err) {
if (err === 'quota is reached') {
apiutil.respond(res, 403, 'Forbidden (groups duration quota is reached)')
}
else if (Array.isArray(err)) {
apiutil.respond(res, 409, 'Conflicts Information', {conflicts: err})
}
else if (err !== 'busy') {
throw err
}
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
let group = lock.group
if (req.user.privilege === apiutil.ADMIN && req.user.email !== group.owner.email) {
email = group.owner.email
return false
}
if (apiutil.isOriginGroup(group.class)) {
return apiutil.respond(res, 400, 'Bad Request (use admin API for bookable/standard groups)')
}
return (function() {
if (typeof serials === 'undefined') {
return dbapi.loadBookableDevices(req.user.groups.subscribed).then(function(devices) {
const serials = []
devices.forEach(function(device) {
if (group.devices.indexOf(device.serial) < 0) {
serials.push(device.serial)
}
})
return _addGroupDevices(group, serials)
})
}
else {
return _addGroupDevices(
group
, _.difference(
_.without(serials.split(','), '')
, group.devices)
)
}
})()
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to add group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
if (email) {
groupApiWrapper(email, addGroupDevices, req, res)
}
})
}
function addGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', addGroupDevices, req, res)
}
function removeGroupDevices(req, res) {
const serials = apiutil.getBodyParameter(req.body, 'serials')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
const lock = {}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
if (apiutil.isOriginGroup(group.class)) {
return apiutil.respond(res, 400, 'Bad Request (use admin API for bookable/standard groups)')
}
let serialsToRemove = group.devices
if (typeof serials !== 'undefined') {
serialsToRemove = _.without(serials.split(','), '')
}
if (!serialsToRemove.length) {
return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
serialsToRemove = _.intersection(serialsToRemove, group.devices)
if (!serialsToRemove.length) {
return apiutil.respond(res, 404, `Not Found (group ${target})`)
}
return dbapi.removeGroupDevices(group, serialsToRemove).then(function(group) {
apiutil.respond(res, 200, `Removed (group ${target})`, {group: apiutil.publishGroup(group)})
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to remove group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeGroupDevice(req, res) {
apiutil.redirectApiWrapper('serial', removeGroupDevices, req, res)
}
function getGroupDevice(req, res) {
const id = req.swagger.params.id.value
const serial = req.swagger.params.serial.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
}
else if (group.devices.indexOf(serial) < 0) {
apiutil.respond(res, 404, 'Not Found (device)')
}
else {
getDevice(req, serial).then(function(device) {
apiutil.respond(res, 200, 'Device Information', {device: device})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group device: ', err.stack)
})
}
function getGroupUser(req, res) {
const id = req.swagger.params.id.value
const email = req.swagger.params.email.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
}
else if (group.users.indexOf(email) < 0) {
apiutil.respond(res, 404, 'Not Found (user)')
}
else {
usersapi.getUserByEmail(req, res)
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group user: ', err.stack)
})
}
function getGroupUsers(req, res) {
const id = req.swagger.params.id.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
}
else {
Promise.map(group.users, function(email) {
return usersapi.getUserInfo(req, email).then(function(user) {
return user || Promise.reject(`Group user not found: ${email}`)
})
})
.then(function(users) {
apiutil.respond(res, 200, 'Users Information', {users: users})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group users: ', err.stack)
})
}
function removeGroupUsers(req, res) {
const id = req.swagger.params.id.value
const emails = apiutil.getBodyParameter(req.body, 'emails')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users'
const lock = {}
function removeGroupUser(email, group, rootGroup) {
if (group.users.indexOf(email) < 0) {
return Promise.resolve('not found')
}
if (email === rootGroup.owner.email || email === group.owner.email) {
return Promise.resolve('forbidden')
}
const lock = {}
return dbapi.lockUser(email).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.user = stats.changes[0].new_val
return dbapi.isRemoveGroupUserAllowed(email, group)
.then(function(isAllowed) {
return isAllowed ? dbapi.removeGroupUser(id, email) : 'forbidden'
})
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const group = lock.group
return dbapi.getRootGroup().then(function(rootGroup) {
let emailsToRemove = group.users
let results = []
if (typeof emails !== 'undefined') {
emailsToRemove = _.without(emails.split(','), '')
}
return Promise.each(emailsToRemove, function(email) {
return removeGroupUser(email, group, rootGroup).then(function(result) {
results.push(result)
})
})
.then(function() {
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (group ${target})`)
}
if (!_.without(results, 'forbidden').length) {
return apiutil.respond(res, 403, `Forbidden (group ${target})`)
}
return dbapi.getGroup(id).then(function(group) {
apiutil.respond(res, 200, `Removed (group ${target})`, {
group: apiutil.publishGroup(group)})
})
})
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, `Failed to remove group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeGroupUser(req, res) {
apiutil.redirectApiWrapper('email', removeGroupUsers, req, res)
}
function addGroupUsers(req, res) {
const id = req.swagger.params.id.value
const emails = apiutil.getBodyParameter(req.body, 'emails')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users'
const lock = {}
function addGroupUser(email) {
const lock = {}
return dbapi.lockUser(email).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.user = stats.changes[0].new_val
return dbapi.addGroupUser(id, email)
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
function _addGroupUsers(emails) {
let results = []
return Promise.each(emails, function(email) {
return addGroupUser(email).then(function(result) {
results.push(result)
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}})
}
if (!_.without(results, 'not found').length) {
return apiutil.respond(res, 404, `Not Found (group ${target})`)
}
return dbapi.getGroup(id).then(function(group) {
apiutil.respond(res, 200, `Added (group ${target})`, {group: apiutil.publishGroup(group)})
})
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (!lockingSuccessed) {
return false
}
const group = lock.group
return (function() {
if (typeof emails === 'undefined') {
return dbapi.getUsers().then(function(users) {
const emails = []
users.forEach(function(user) {
if (group.users.indexOf(user.email) < 0) {
emails.push(user.email)
}
})
return _addGroupUsers(emails)
})
}
else {
return _addGroupUsers(
_.difference(
_.without(emails.split(','), '')
, group.users)
)
}
})()
})
.catch(function(err) {
apiutil.internalError(res, `Failed to add group ${target}: `, err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function addGroupUser(req, res) {
apiutil.redirectApiWrapper('email', addGroupUsers, req, res)
}
function getGroup(req, res) {
const id = req.swagger.params.id.value
const fields = req.swagger.params.fields.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
return
}
let publishedGroup = apiutil.publishGroup(group)
if (fields) {
publishedGroup = _.pick(publishedGroup, fields.split(','))
}
apiutil.respond(res, 200, 'Group Information', {group: publishedGroup})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group: ', err.stack)
})
}
function getGroups(req, res) {
const fields = req.swagger.params.fields.value
const owner = req.swagger.params.owner.value
let getGenericGroups
switch(owner) {
case true:
getGenericGroups = dbapi.getOwnerGroups
break
case false:
getGenericGroups = dbapi.getOnlyUserGroups
break
default:
getGenericGroups = dbapi.getUserGroups
}
getGenericGroups(req.user.email).then(function(groups) {
return apiutil.respond(res, 200, 'Groups Information', {
groups: groups.map(function(group) {
if (fields) {
return _.pick(apiutil.publishGroup(group), fields.split(','))
}
return apiutil.publishGroup(group)
})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get groups: ', err.stack)
})
}
function createGroup(req, res) {
const _class = typeof req.body.class === 'undefined' ? apiutil.ONCE : req.body.class
const repetitions =
apiutil.isOriginGroup(_class) || typeof req.body.repetitions === 'undefined' ?
0 :
req.body.repetitions
const now = Date.now()
const start =
apiutil.isOriginGroup(_class) ?
new Date(now) :
new Date(req.body.startTime || now)
const stop =
apiutil.isOriginGroup(_class) ?
new Date(now + apiutil.ONE_YEAR) :
new Date(req.body.stopTime || now + apiutil.ONE_HOUR)
checkSchedule(res, null, _class, req.user.email, repetitions, req.user.privilege,
start, stop).then(function(checkingSuccessed) {
if (!checkingSuccessed) {
return
}
const name =
typeof req.body.name === 'undefined' ?
'New_' + util.format('%s', uuid.v4()).replace(/-/g, '') :
req.body.name
const state =
apiutil.isOriginGroup(_class) || typeof req.body.state === 'undefined' ?
apiutil.READY :
req.body.state
const isActive = state === apiutil.READY && apiutil.isOriginGroup(_class)
const duration = 0
const dates = apiutil.computeGroupDates({start: start, stop: stop}, _class, repetitions)
dbapi.createUserGroup({
name: name
, owner: {
email: req.user.email
, name: req.user.name
}
, privilege: req.user.privilege
, class: _class
, repetitions: repetitions
, isActive: isActive
, dates: dates
, duration: duration
, state: state
})
.then(function(group) {
if (group) {
apiutil.respond(res, 201, 'Created', {group: apiutil.publishGroup(group)})
}
else {
apiutil.respond(res, 403, 'Forbidden (groups number quota is reached)')
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to create group: ', err.stack)
})
})
}
function deleteGroups(req, res) {
const ids = apiutil.getBodyParameter(req.body, 'ids')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'group' : 'groups'
function removeGroup(id) {
const lock = {}
return dbapi.lockGroupByOwner(req.user.email, id).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const group = lock.group = stats.changes[0].new_val
if (group.privilege === apiutil.ROOT) {
return 'forbidden'
}
if (group.class === apiutil.BOOKABLE) {
return Promise.each(group.devices, function(serial) {
return dbapi.isDeviceBooked(serial)
.then(function(isBooked) {
return isBooked ? Promise.reject('booked') : true
})
})
.then(function() {
return dbapi.deleteUserGroup(id)
})
.catch(function(err) {
if (err !== 'booked') {
throw err
}
return 'forbidden'
})
}
else {
return dbapi.deleteUserGroup(id)
}
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function removeGroups(ids) {
let results = []
return Promise.each(ids, function(id) {
return removeGroup(id).then(function(result) {
results.push(result)
})
})
.then(function() {
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`)
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
results = _.without(results, 'forbidden')
if (!results.length) {
return apiutil.respond(res, 403, `Forbidden (${target})`)
}
return apiutil.respond(res, 200, `Deleted (${target})`)
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
(function() {
if (typeof ids === 'undefined') {
return dbapi.getOwnerGroups(req.user.email).then(function(groups) {
const ids = []
groups.forEach(function(group) {
if (group.privilege !== apiutil.ROOT) {
ids.push(group.id)
}
})
return removeGroups(ids)
})
}
else {
return removeGroups(_.without(ids.split(','), ''))
}
})()
.catch(function(err) {
apiutil.internalError(res, `Failed to delete ${target}: `, err.stack)
})
}
function deleteGroup(req, res) {
apiutil.redirectApiWrapper('id', deleteGroups, req, res)
}
function updateGroup(req, res) {
const id = req.swagger.params.id.value
const lock = {}
function updateUserGroup(group, data) {
return dbapi.updateUserGroup(group, data)
.then(function(group) {
if (group) {
apiutil.respond(res, 200, 'Updated (group)', {group: apiutil.publishGroup(group)})
}
else {
apiutil.respond(res, 403, 'Forbidden (groups duration quota is reached)')
}
})
}
lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
if (!lockingSuccessed) {
return false
}
const group = lock.group
const _class = typeof req.body.class === 'undefined' ? group.class : req.body.class
const name = typeof req.body.name === 'undefined' ? group.name : req.body.name
const repetitions =
typeof req.body.repetitions === 'undefined' ?
group.repetitions :
req.body.repetitions
const start = new Date(req.body.startTime || group.dates[0].start)
const stop = new Date(req.body.stopTime || group.dates[0].stop)
let state, isActive
if (apiutil.isOriginGroup(_class)) {
state = apiutil.READY
isActive = true
}
else {
state = typeof req.body.state === 'undefined' ? apiutil.PENDING : req.body.state
isActive = false
}
if (group.state === apiutil.READY && state === apiutil.PENDING) {
return apiutil.respond(res, 403, 'Forbidden (group is ready)')
}
return checkSchedule(res, group, _class, group.owner.email, repetitions, group.privilege,
start, stop).then(function(checkingSuccessed) {
if (!checkingSuccessed) {
return false
}
if (name === group.name &&
start.toISOString() === group.dates[0].start.toISOString() &&
stop.toISOString() === group.dates[0].stop.toISOString() &&
state === group.state &&
_class === group.class &&
repetitions === group.repetitions) {
return apiutil.respond(res, 200, 'Unchanged (group)', {group: {}})
}
const duration = group.devices.length * (stop - start) * (repetitions + 1)
const dates = apiutil.computeGroupDates({start: start, stop: stop}, _class, repetitions)
if (start < group.dates[0].start ||
stop > group.dates[0].stop ||
repetitions > group.repetitions ||
_class !== group.class) {
return checkConflicts(id, group.devices, dates)
.then(function(conflicts) {
if (!conflicts.length) {
return updateUserGroup(group, {
name: name
, state: state
, class: _class
, isActive: isActive
, repetitions: repetitions
, dates: dates
, duration: duration
})
}
return apiutil.respond(res, 409, 'Conflicts Information', {conflicts: conflicts})
})
}
return updateUserGroup(group, {
name: name
, state: state
, class: _class
, isActive: isActive
, repetitions: repetitions
, dates: dates
, duration: duration
})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to update group: ', err.stack)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function getGroupDevices(req, res) {
const id = req.swagger.params.id.value
const bookable = req.swagger.params.bookable.value
dbapi.getUserGroup(req.user.email, id).then(function(group) {
if (!group) {
apiutil.respond(res, 404, 'Not Found (group)')
return
}
if (bookable) {
if (apiutil.isOriginGroup(group.class)) {
apiutil.respond(res, 400, 'Bad Request (group is not transient)')
return
}
if (req.user.privilege === apiutil.ADMIN && req.user.email !== group.owner.email) {
groupApiWrapper(group.owner.email, getGroupDevices, req, res)
return
}
dbapi.loadBookableDevices(req.user.groups.subscribed).then(function(devices) {
Promise.map(devices, function(device) {
return device.serial
})
.then(function(serials) {
return checkConflicts(group.id, serials, group.dates)
.then(function(conflicts) {
let bookableSerials = serials
conflicts.forEach(function(conflict) {
bookableSerials = _.difference(bookableSerials, conflict.devices)
})
return bookableSerials
})
})
.then(function(bookableSerials) {
const deviceList = []
devices.forEach(function(device) {
if (bookableSerials.indexOf(device.serial) > -1) {
deviceList.push(apiutil.filterDevice(req, device))
}
})
apiutil.respond(res, 200, 'Devices Information', {devices: deviceList})
})
})
}
else {
Promise.map(group.devices, function(serial) {
return getDevice(req, serial)
})
.then(function(devices) {
apiutil.respond(res, 200, 'Devices Information', {devices: devices})
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get group devices: ', err.stack)
})
}
module.exports = {
createGroup: createGroup
, updateGroup: updateGroup
, deleteGroup: deleteGroup
, deleteGroups: deleteGroups
, getGroup: getGroup
, getGroups: getGroups
, getGroupUser: getGroupUser
, getGroupUsers: getGroupUsers
, addGroupUser: addGroupUser
, addGroupUsers: addGroupUsers
, removeGroupUser: removeGroupUser
, removeGroupUsers: removeGroupUsers
, getGroupDevice: getGroupDevice
, getGroupDevices: getGroupDevices
, addGroupDevice: addGroupDevice
, addGroupDevices: addGroupDevices
, removeGroupDevice: removeGroupDevice
, removeGroupDevices: removeGroupDevices
}

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var util = require('util')
var _ = require('lodash')
@ -12,6 +16,9 @@ var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var wirerouter = require('../../../wire/router')
const apiutil = require('../../../util/apiutil')
const jwtutil = require('../../../util/jwtutil')
var log = logger.createLogger('api:controllers:user')
module.exports = {
@ -24,9 +31,16 @@ module.exports = {
, remoteDisconnectUserDeviceBySerial: remoteDisconnectUserDeviceBySerial
, getUserAccessTokens: getUserAccessTokens
, addAdbPublicKey: addAdbPublicKey
, addUserDeviceV2: addUserDevice
, getAccessTokens: getAccessTokens
, getAccessToken: getAccessToken
, createAccessToken: createAccessToken
, deleteAccessToken: deleteAccessToken
, deleteAccessTokens: deleteAccessTokens
}
function getUser(req, res) {
// delete req.user.groups.lock
res.json({
success: true
, user: req.user
@ -53,6 +67,7 @@ function getUserDevices(req, res) {
res.json({
success: true
, description: 'Controlled devices information'
, devices: deviceList
})
})
@ -61,6 +76,7 @@ function getUserDevices(req, res) {
log.error('Failed to load device list: ', err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@ -69,9 +85,10 @@ function getUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
var fields = req.swagger.params.fields.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
@ -93,24 +110,29 @@ function getUserDeviceBySerial(req, res) {
res.json({
success: true
, description: 'Controlled device information'
, device: responseDevice
})
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
function addUserDevice(req, res) {
var serial = req.body.serial
var timeout = req.body.timeout || null
var serial = req.hasOwnProperty('body') ? req.body.serial : req.swagger.params.serial.value
var timeout = req.hasOwnProperty('body') ? req.body.timeout ||
null : req.swagger.params.timeout.value || null
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
@ -172,10 +194,12 @@ function addUserDevice(req, res) {
)
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@ -183,9 +207,10 @@ function addUserDevice(req, res) {
function deleteUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
@ -211,7 +236,8 @@ function deleteUserDeviceBySerial(req, res) {
var messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === serial && message.owner.email === req.user.email) {
if (message.serial === serial &&
(message.owner.email === req.user.email || req.user.privilege === 'admin')) {
clearTimeout(responseTimer)
req.options.channelRouter.removeListener(wireutil.global, messageListener)
@ -239,10 +265,12 @@ function deleteUserDeviceBySerial(req, res) {
)
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@ -250,9 +278,10 @@ function deleteUserDeviceBySerial(req, res) {
function remoteConnectUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
@ -286,9 +315,9 @@ function remoteConnectUserDeviceBySerial(req, res) {
clearTimeout(timer)
req.options.sub.unsubscribe(responseChannel)
req.options.channelRouter.removeListener(responseChannel, messageListener)
return res.json({
success: true
, description: 'Remote connection is enabled'
, remoteConnectUrl: message.url
})
}
@ -305,10 +334,12 @@ function remoteConnectUserDeviceBySerial(req, res) {
)
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@ -316,9 +347,10 @@ function remoteConnectUserDeviceBySerial(req, res) {
function remoteDisconnectUserDeviceBySerial(req, res) {
var serial = req.swagger.params.serial.value
dbapi.loadDevice(serial)
.then(function(device) {
if (!device) {
dbapi.loadDevice(req.user.groups.subscribed, serial)
.then(function(cursor) {
cursor.next(function(err, device) {
if (err) {
return res.status(404).json({
success: false
, description: 'Device not found'
@ -352,7 +384,6 @@ function remoteDisconnectUserDeviceBySerial(req, res) {
clearTimeout(timer)
req.options.sub.unsubscribe(responseChannel)
req.options.channelRouter.removeListener(responseChannel, messageListener)
return res.json({
success: true
, description: 'Device remote disconnected successfully'
@ -371,10 +402,12 @@ function remoteDisconnectUserDeviceBySerial(req, res) {
)
])
})
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.status(500).json({
success: false
, description: 'Internal Server Error'
})
})
}
@ -454,3 +487,111 @@ function addAdbPublicKey(req, res) {
})
})
}
function getAccessToken(req, res) {
const id = req.swagger.params.id.value
dbapi.loadAccessToken(id).then(function(token) {
if (!token || token.email !== req.user.email) {
apiutil.respond(res, 404, 'Not Found (access token)')
}
else {
apiutil.respond(res, 200, 'Access Token Information', {
token: apiutil.publishAccessToken(token)
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete access token "%s": ', id, err.stack)
})
}
function getAccessTokens(req, res) {
dbapi.loadAccessTokens(req.user.email).then(function(cursor) {
Promise.promisify(cursor.toArray, cursor)().then(function(tokens) {
const tokenList = []
tokens.forEach(function(token) {
tokenList.push(apiutil.publishAccessToken(token))
})
apiutil.respond(res, 200, 'Access Tokens Information', {tokens: tokenList})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get access tokens: ', err.stack)
})
}
function createAccessToken(req, res) {
const title = req.swagger.params.title.value
const jwt = jwtutil.encode({
payload: {
email: req.user.email
, name: req.user.name
}
, secret: req.options.secret
})
const id = util.format('%s-%s', uuid.v4(), uuid.v4()).replace(/-/g, '')
dbapi.saveUserAccessToken(req.user.email, {
title: title
, id: id
, jwt: jwt
})
.then(function(stats) {
req.options.pushdev.send([
req.user.group
, wireutil.envelope(new wire.UpdateAccessTokenMessage())
])
apiutil.respond(res, 201, 'Created (access token)',
{token: apiutil.publishAccessToken(stats.changes[0].new_val)})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to create access token "%s": ', title, err.stack)
})
}
function deleteAccessTokens(req, res) {
dbapi.removeUserAccessTokens(req.user.email).then(function(stats) {
if (!stats.deleted) {
apiutil.respond(res, 200, 'Unchanged (access tokens)')
}
else {
req.options.pushdev.send([
req.user.group
, wireutil.envelope(new wire.UpdateAccessTokenMessage())
])
apiutil.respond(res, 200, 'Deleted (access tokens)')
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete access tokens: ', err.stack)
})
}
function deleteAccessToken(req, res) {
const id = req.swagger.params.id.value
dbapi.loadAccessToken(id).then(function(token) {
if (!token || token.email !== req.user.email) {
apiutil.respond(res, 404, 'Not Found (access token)')
}
else {
dbapi.removeAccessToken(id).then(function(stats) {
if (!stats.deleted) {
apiutil.respond(res, 404, 'Not Found (access token)')
}
else {
req.options.pushdev.send([
req.user.group
, wireutil.envelope(new wire.UpdateAccessTokenMessage())
])
apiutil.respond(res, 200, 'Deleted (access token)')
}
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete access token "%s": ', id, err.stack)
})
}

View file

@ -0,0 +1,398 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const dbapi = require('../../../db/api')
const _ = require('lodash')
const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const Promise = require('bluebird')
const wire = require('../../../wire')
const wireutil = require('../../../wire/util')
const userapi = require('./user')
/* --------------------------------- PRIVATE FUNCTIONS --------------------------------------- */
function userApiWrapper(fn, req, res) {
const email = req.swagger.params.email.value
dbapi.loadUser(email).then(function(user) {
if (!user) {
apiutil.respond(res, 404, 'Not Found (user)')
}
else {
req.user = user
fn(req, res)
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to wrap "%s": ', fn.name, err.stack)
})
}
function getPublishedUser(user, userEmail, adminEmail, fields) {
let publishedUser = apiutil.publishUser(user)
if (userEmail !== adminEmail) {
publishedUser = _.pick(user, 'email', 'name', 'privilege')
}
if (fields) {
publishedUser = _.pick(publishedUser, fields.split(','))
}
return publishedUser
}
function removeUser(email, req, res) {
const groupOwnerState = req.swagger.params.groupOwner.value
const anyGroupOwnerState = typeof groupOwnerState === 'undefined'
const lock = {}
function removeGroupUser(owner, id) {
const lock = {}
return dbapi.lockGroupByOwner(owner, id).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
lock.group = stats.changes[0].new_val
return owner === email ?
dbapi.deleteUserGroup(id) :
dbapi.removeGroupUser(id, email)
})
.finally(function() {
lockutil.unlockGroup(lock)
})
}
function deleteUserInDatabase(channel) {
return dbapi.removeUserAccessTokens(email).then(function() {
return dbapi.deleteUser(email).then(function() {
req.options.pushdev.send([
channel
, wireutil.envelope(new wire.DeleteUserMessage(
email
))
])
return 'deleted'
})
})
}
function computeUserGroupOwnership(groups) {
if (anyGroupOwnerState) {
return Promise.resolve(true)
}
return Promise.map(groups, function(group) {
if (!groupOwnerState && group.owner.email === email) {
return Promise.reject('filtered')
}
return !groupOwnerState || group.owner.email === email
})
.then(function(results) {
return _.without(results, false).length > 0
})
.catch(function(err) {
if (err === 'filtered') {
return false
}
throw err
})
}
if (req.user.email === email) {
return Promise.resolve('forbidden')
}
return dbapi.lockUser(email).then(function(stats) {
if (!stats.replaced) {
return apiutil.lightComputeStats(res, stats)
}
const user = lock.user = stats.changes[0].new_val
return dbapi.getGroupsByUser(user.email).then(function(groups) {
return computeUserGroupOwnership(groups).then(function(doContinue) {
if (!doContinue) {
return 'unchanged'
}
return Promise.each(groups, function(group) {
return removeGroupUser(group.owner.email, group.id)
})
.then(function() {
return deleteUserInDatabase(user.group)
})
})
})
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
/* --------------------------------- PUBLIC FUNCTIONS --------------------------------------- */
function getUserInfo(req, email) {
const fields = req.swagger.params.fields.value
return dbapi.loadUser(email).then(function(user) {
if (user) {
return dbapi.getRootGroup().then(function(group) {
return getPublishedUser(user, req.user.email, group.owner.email, fields)
})
}
return false
})
}
function updateUserGroupsQuotas(req, res) {
const email = req.swagger.params.email.value
const duration =
typeof req.swagger.params.duration.value !== 'undefined' ?
req.swagger.params.duration.value :
null
const number =
typeof req.swagger.params.number.value !== 'undefined' ?
req.swagger.params.number.value :
null
const repetitions =
typeof req.swagger.params.repetitions.value !== 'undefined' ?
req.swagger.params.repetitions.value :
null
const lock = {}
lockutil.lockUser(email, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return dbapi.updateUserGroupsQuotas(email, duration, number, repetitions)
.then(function(stats) {
if (stats.replaced) {
return apiutil.respond(res, 200, 'Updated (user quotas)', {
user: apiutil.publishUser(stats.changes[0].new_val)
})
}
if ((duration === null || duration === lock.user.groups.quotas.allocated.duration) &&
(number === null || number === lock.user.groups.quotas.allocated.number) &&
(repetitions === null || repetitions === lock.user.groups.quotas.repetitions)
) {
return apiutil.respond(res, 200, 'Unchanged (user quotas)', {user: {}})
}
return apiutil.respond(
res
, 400
, 'Bad Request (quotas must be >= actual consumed resources)')
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to update user groups quotas: ', err.stack)
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
function updateDefaultUserGroupsQuotas(req, res) {
const duration =
typeof req.swagger.params.duration.value !== 'undefined' ?
req.swagger.params.duration.value :
null
const number =
typeof req.swagger.params.number.value !== 'undefined' ?
req.swagger.params.number.value :
null
const repetitions =
typeof req.swagger.params.repetitions.value !== 'undefined' ?
req.swagger.params.repetitions.value :
null
const lock = {}
lockutil.lockUser(req.user.email, res, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return dbapi.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions)
.then(function(stats) {
if (stats.replaced) {
return apiutil.respond(res, 200, 'Updated (user default quotas)', {
user: apiutil.publishUser(stats.changes[0].new_val)
})
}
return apiutil.respond(res, 200, 'Unchanged (user default quotas)', {user: {}})
})
}
return false
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to update default user groups quotas: ', err.stack)
})
.finally(function() {
lockutil.unlockUser(lock)
})
}
function getUserByEmail(req, res) {
const email = req.swagger.params.email.value
getUserInfo(req, email).then(function(user) {
if (user) {
apiutil.respond(res, 200, 'User Information', {user: user})
}
else {
apiutil.respond(res, 404, 'Not Found (user)')
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get user: ', err.stack)
})
}
function getUsers(req, res) {
const fields = req.swagger.params.fields.value
dbapi.getUsers().then(function(users) {
return dbapi.getRootGroup().then(function(group) {
apiutil.respond(res, 200, 'Users Information', {
users: users.map(function(user) {
return getPublishedUser(user, req.user.email, group.owner.email, fields)
})
})
})
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to get users: ', err.stack)
})
}
function createUser(req, res) {
const email = req.swagger.params.email.value
const name = req.swagger.params.name.value
dbapi.createUser(email, name, req.user.ip).then(function(stats) {
if (!stats.inserted) {
apiutil.respond(res, 403, 'Forbidden (user already exists)')
}
else {
apiutil.respond(res, 201, 'Created (user)', {
user: apiutil.publishUser(stats.changes[0].new_val)
})
}
})
.catch(function(err) {
apiutil.internalError(res, 'Failed to create user: ', err.stack)
})
}
function deleteUsers(req, res) {
const emails = apiutil.getBodyParameter(req.body, 'emails')
const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users'
function removeUsers(emails) {
let results = []
return Promise.each(emails, function(email) {
return removeUser(email, req, res).then(function(result) {
results.push(result)
})
})
.then(function() {
results = _.without(results, 'unchanged')
if (!results.length) {
return apiutil.respond(res, 200, `Unchanged (${target})`)
}
results = _.without(results, 'not found')
if (!results.length) {
return apiutil.respond(res, 404, `Not Found (${target})`)
}
results = _.without(results, 'forbidden')
if (!results.length) {
apiutil.respond(res, 403, `Forbidden (${target})`)
}
return apiutil.respond(res, 200, `Deleted (${target})`)
})
.catch(function(err) {
if (err !== 'busy') {
throw err
}
})
}
(function() {
if (typeof emails === 'undefined') {
return dbapi.getEmails().then(function(emails) {
return removeUsers(emails)
})
}
else {
return removeUsers(_.without(emails.split(','), ''))
}
})()
.catch(function(err) {
apiutil.internalError(res, 'Failed to delete ${target}: ', err.stack)
})
}
function deleteUser(req, res) {
apiutil.redirectApiWrapper('email', deleteUsers, req, res)
}
function createUserAccessToken(req, res) {
userApiWrapper(userapi.createAccessToken, req, res)
}
function deleteUserAccessToken(req, res) {
userApiWrapper(userapi.deleteAccessToken, req, res)
}
function deleteUserAccessTokens(req, res) {
userApiWrapper(userapi.deleteAccessTokens, req, res)
}
function getUserAccessToken(req, res) {
userApiWrapper(userapi.getAccessToken, req, res)
}
function getUserAccessTokens(req, res) {
userApiWrapper(userapi.getAccessTokens, req, res)
}
function getUserDevices(req, res) {
userApiWrapper(userapi.getUserDevices, req, res)
}
function getUserDevice(req, res) {
userApiWrapper(userapi.getUserDeviceBySerial, req, res)
}
function addUserDevice(req, res) {
userApiWrapper(userapi.addUserDevice, req, res)
}
function deleteUserDevice(req, res) {
userApiWrapper(userapi.deleteUserDeviceBySerial, req, res)
}
function remoteConnectUserDevice(req, res) {
userApiWrapper(userapi.remoteConnectUserDeviceBySerial, req, res)
}
function remoteDisconnectUserDevice(req, res) {
userApiWrapper(userapi.remoteDisconnectUserDeviceBySerial, req, res)
}
module.exports = {
updateUserGroupsQuotas: updateUserGroupsQuotas
, updateDefaultUserGroupsQuotas: updateDefaultUserGroupsQuotas
, getUsers: getUsers
, getUserByEmail: getUserByEmail
, getUserInfo: getUserInfo
, createUser: createUser
, deleteUser: deleteUser
, deleteUsers: deleteUsers
, createUserAccessToken: createUserAccessToken
, deleteUserAccessToken: deleteUserAccessToken
, deleteUserAccessTokens: deleteUserAccessTokens
, getUserAccessTokensV2: getUserAccessTokens
, getUserAccessToken: getUserAccessToken
, getUserDevicesV2: getUserDevices
, getUserDevice: getUserDevice
, addUserDeviceV3: addUserDevice
, deleteUserDevice: deleteUserDevice
, remoteConnectUserDevice: remoteConnectUserDevice
, remoteDisconnectUserDevice: remoteDisconnectUserDevice
}

View file

@ -1,7 +1,12 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var dbapi = require('../../../db/api')
var jwtutil = require('../../../util/jwtutil')
var urlutil = require('../../../util/urlutil')
var logger = require('../../../util/logger')
const apiutil = require('../../../util/apiutil')
var log = logger.createLogger('api:helpers:securityHandlers')
@ -47,17 +52,27 @@ function accessTokenAuth(req, res, next) {
if (!data) {
return res.status(500).json({
success: false
, description: 'Internal Server Error'
})
}
dbapi.loadUser(data.email)
.then(function(user) {
if (user) {
if (user.privilege === apiutil.USER &&
req.swagger.operation.definition.tags.indexOf('admin') > -1) {
return res.status(403).json({
success: false
, description: 'Forbidden: privileged operation (admin)'
})
}
req.user = user
next()
}
else {
return res.status(500).json({
success: false
, description: 'Internal Server Error'
})
}
})
@ -86,6 +101,7 @@ function accessTokenAuth(req, res, next) {
else {
return res.status(500).json({
success: false
, description: 'Internal Server Error'
})
}
})

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var path = require('path')
var events = require('events')
@ -52,16 +56,51 @@ module.exports = function(options) {
lifecycle.fatal()
})
var pushdev = zmqutil.socket('push')
Promise.map(options.endpoints.pushdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
pushdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to pushdev endpoint', err)
lifecycle.fatal()
})
var subdev = zmqutil.socket('sub')
Promise.map(options.endpoints.subdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Receiving input from "%s"', record.url)
subdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to subdev endpoint', err)
lifecycle.fatal()
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
subdev.subscribe(channel)
})
sub.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
subdev.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
// Swagger Express Config
var config = {
appRoot: __dirname
@ -83,6 +122,8 @@ module.exports = function(options) {
push: push
, sub: sub
, channelRouter: channelRouter
, pushdev: pushdev
, subdev: subdev
})
req.options = reqOptions
@ -96,7 +137,7 @@ module.exports = function(options) {
}))
lifecycle.observe(function() {
[push, sub].forEach(function(sock) {
[push, sub, pushdev, subdev].forEach(function(sock) {
try {
sock.close()
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var jwtutil = require('../../../util/jwtutil')
var urlutil = require('../../../util/urlutil')
@ -18,6 +22,7 @@ module.exports = function(options) {
})
.then(function() {
req.session.jwt = data
req.sessionOptions.httpOnly = false
res.redirect(redir)
})
.catch(next)

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var express = require('express')
@ -16,6 +20,8 @@ var pathutil = require('../../util/pathutil')
var urlutil = require('../../util/urlutil')
var lifecycle = require('../../util/lifecycle')
const dbapi = require('../../db/api')
module.exports = function(options) {
var log = logger.createLogger('auth-ldap')
var app = express()
@ -54,6 +60,24 @@ module.exports = function(options) {
res.redirect('/auth/ldap/')
})
app.get('/auth/contact', function(req, res) {
dbapi.getRootGroup().then(function(group) {
res.status(200)
.json({
success: true
, contact: group.owner
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.get('/auth/ldap/', function(req, res) {
res.render('index')
})

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var express = require('express')
@ -16,6 +20,8 @@ var pathutil = require('../../util/pathutil')
var urlutil = require('../../util/urlutil')
var lifecycle = require('../../util/lifecycle')
const dbapi = require('../../db/api')
module.exports = function(options) {
var log = logger.createLogger('auth-mock')
var app = express()
@ -80,6 +86,24 @@ module.exports = function(options) {
res.redirect('/auth/mock/')
})
app.get('/auth/contact', function(req, res) {
dbapi.getRootGroup().then(function(group) {
res.status(200)
.json({
success: true
, contact: group.owner
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.get('/auth/mock/', function(req, res) {
res.render('index')
})

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var util = require('util')
var syrup = require('stf-syrup')
@ -117,6 +121,7 @@ module.exports = syrup.serial()
if (plugin.isRunning()) {
activeServer.close()
activeServer.end()
activeServer = null
}
})
@ -131,7 +136,7 @@ module.exports = syrup.serial()
}
lifecycle.observe(plugin.stop)
group.on('leave', plugin.end)
group.on('leave', plugin.stop)
router
.on(wire.ConnectStartMessage, function(channel) {
@ -163,7 +168,7 @@ module.exports = syrup.serial()
})
.on(wire.ConnectStopMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.end()
plugin.stop()
.then(function() {
push.send([
channel
@ -187,6 +192,5 @@ module.exports = syrup.serial()
})
})
return plugin.start()
.return(plugin)
return(plugin)
})

View file

@ -33,8 +33,9 @@ module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/sdk'))
.dependency(require('../resources/service'))
.define(function(options, adb, router, push, apk) {
.define(function(options, adb, router, push, sdk, apk) {
var log = logger.createLogger('device:plugins:service')
var messageResolver = new MessageResolver()
var plugin = new events.EventEmitter()
@ -62,9 +63,11 @@ module.exports = syrup.serial()
}
function callService(intent) {
var startServiceCmd = (sdk.level < 26) ? 'startservice' : 'start-foreground-service'
log.info('using \'%s\' command for API %s', startServiceCmd, sdk.level)
return adb.shell(options.serial, util.format(
'am startservice --user 0 %s'
, intent
'am %s --user 0 %s'
, startServiceCmd, intent
))
.timeout(15000)
.then(function(out) {
@ -76,8 +79,8 @@ module.exports = syrup.serial()
.then(function(line) {
if (line.indexOf('--user') !== -1) {
return adb.shell(options.serial, util.format(
'am startservice %s'
, intent
'am %s %s'
, startServiceCmd, intent
))
.timeout(15000)
.then(function() {

View file

@ -44,6 +44,7 @@ module.exports = syrup.serial()
, identity.product
, identity.cpuPlatform
, identity.openGLESVersion
, identity.marketName
))
])
})

View file

@ -62,7 +62,7 @@ module.exports = syrup.serial()
}
function removeResource(res) {
return adb.shell(options.serial, ['rm', res.dest])
return adb.shell(options.serial, ['rm', '-f', res.dest])
.timeout(10000)
.then(function(out) {
return streamutil.readAll(out)

View file

@ -36,7 +36,7 @@ module.exports = syrup.serial()
}
function removeResource(res) {
return adb.shell(options.serial, ['rm', res.dest])
return adb.shell(options.serial, ['rm', '-f', res.dest])
.timeout(10000)
.then(function(out) {
return streamutil.readAll(out)

View file

@ -35,7 +35,7 @@ module.exports = syrup.serial()
}
function removeResource(res) {
return adb.shell(options.serial, ['rm', res.dest])
return adb.shell(options.serial, ['rm', '-f', res.dest])
.timeout(10000)
.then(function(out) {
return streamutil.readAll(out)

View file

@ -0,0 +1,115 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const events = require('events')
const Promise = require('bluebird')
const logger = require('../../util/logger')
const zmqutil = require('../../util/zmqutil')
const srv = require('../../util/srv')
const lifecycle = require('../../util/lifecycle')
const wireutil = require('../../wire/util')
const groupsScheduler = require('./scheduler')
const groupsWatcher = require('./watchers/groups')
const devicesWatcher = require('./watchers/devices')
const usersWatcher = require('./watchers/users')
module.exports = function(options) {
const log = logger.createLogger('groups-engine')
const channelRouter = new events.EventEmitter()
const push = zmqutil.socket('push')
Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
push.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to push endpoint', err)
lifecycle.fatal()
})
// Input
const sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Receiving input from "%s"', record.url)
sub.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to sub endpoint', err)
lifecycle.fatal()
})
const pushdev = zmqutil.socket('push')
Promise.map(options.endpoints.pushdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Sending output to "%s"', record.url)
pushdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to pushdev endpoint', err)
lifecycle.fatal()
})
const subdev = zmqutil.socket('sub')
Promise.map(options.endpoints.subdev, function(endpoint) {
return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) {
log.info('Receiving input from "%s"', record.url)
subdev.connect(record.url)
return Promise.resolve(true)
})
})
})
.catch(function(err) {
log.fatal('Unable to connect to subdev endpoint', err)
lifecycle.fatal()
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
subdev.subscribe(channel)
})
sub.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
subdev.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
groupsScheduler()
groupsWatcher(push, pushdev, channelRouter)
devicesWatcher(push, pushdev, channelRouter)
usersWatcher(pushdev)
lifecycle.observe(function() {
[push, sub, pushdev, subdev].forEach(function(sock) {
try {
sock.close()
}
catch (err) {
// No-op
}
})
})
log.info('Groups engine started')
}

View file

@ -0,0 +1,156 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const Promise = require('bluebird')
const logger = require('../../../util/logger')
const apiutil = require('../../../util/apiutil')
const db = require('../../../db')
const dbapi = require('../../../db/api')
const r = require('rethinkdb')
module.exports = function() {
const log = logger.createLogger('groups-scheduler')
function updateOriginGroupLifetime(group) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
const now = Date.now()
return db.run(r.table('groups').get(group.id).update({
dates: [{
start: new Date(now)
, stop: new Date(now + (group.dates[0].stop - group.dates[0].start))
}]
}))
}
return false
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function deleteUserGroup(group) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return dbapi.deleteUserGroup(group.id)
}
else {
return db.run(r.table('groups').get(group.id).update({
isActive: false
, state: apiutil.WAITING
}))
}
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function updateGroupDates(group, incr, isActive) {
const repetitions = group.repetitions - incr
const dates = group.dates.slice(incr)
const duration = group.devices.length * (dates[0].stop - dates[0].start) * (repetitions + 1)
return db.run(r.table('groups').get(group.id).update({
dates: dates
, repetitions: repetitions
, duration: duration
, isActive: isActive
, state: apiutil.READY
}))
.then(function() {
return dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration)
})
}
function doBecomeUnactiveGroup(group) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
if (lockingSuccessed) {
return updateGroupDates(group, 1, false)
}
else {
return db.run(r.table('groups').get(group.id).update({
isActive: false
, state: apiutil.WAITING
}))
}
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function doCleanElapsedGroupDates(group, incr) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
return lockingSuccessed ? updateGroupDates(group, incr, false) : false
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
function doBecomeActiveGroup(group, incr) {
const lock = {}
return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) {
return lockingSuccessed ? updateGroupDates(group, incr, true) : false
})
.finally(function() {
return dbapi.adminUnlockGroup(lock)
})
}
dbapi.unlockBookingObjects().then(function() {
setInterval(function() {
const now = Date.now()
dbapi.getReadyGroupsOrderByIndex('startTime').then(function(groups) {
Promise.each(groups, (function(group) {
if (apiutil.isOriginGroup(group.class)) {
if (now >= group.dates[0].stop.getTime()) {
return updateOriginGroupLifetime(group)
}
}
else if ((group.isActive || group.state === apiutil.WAITING) &&
now >= group.dates[0].stop.getTime()) {
if (group.dates.length === 1) {
return deleteUserGroup(group)
}
else {
return doBecomeUnactiveGroup(group)
}
}
else if (!group.isActive) {
for(const i in group.dates) {
if (now >= group.dates[i].stop.getTime()) {
if (group.dates[i].stop === group.dates[group.dates.length - 1].stop) {
return deleteUserGroup(group)
}
}
else if (now < group.dates[i].start.getTime()) {
return i > 0 ? doCleanElapsedGroupDates(group, i) : false
}
else {
return doBecomeActiveGroup(group, i)
}
}
}
return false
}))
})
.catch(function(err) {
log.error('An error occured during groups scheduling', err.stack)
})
}, 1000)
})
}

View file

@ -0,0 +1,255 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const wirerouter = require('../../../wire/router')
const _ = require('lodash')
const r = require('rethinkdb')
const util = require('util')
const uuid = require('uuid')
const logger = require('../../../util/logger')
const timeutil = require('../../../util/timeutil')
const wireutil = require('../../../wire/util')
const wire = require('../../../wire')
const dbapi = require('../../../db/api')
const db = require('../../../db')
module.exports = function(push, pushdev, channelRouter) {
const log = logger.createLogger('watcher-devices')
function sendReleaseDeviceControl(serial, channel) {
push.send([
channel
, wireutil.envelope(
new wire.UngroupMessage(
wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
)
)
])
}
function sendDeviceGroupChange(id, group, serial, originName) {
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.DeviceGroupChangeMessage(
id
, new wire.DeviceGroupMessage(
group.id
, group.name
, new wire.DeviceGroupOwnerMessage(
group.owner.email
, group.owner.name
)
, new wire.DeviceGroupLifetimeMessage(
group.dates[0].start.getTime()
, group.dates[0].stop.getTime()
)
, group.class
, group.repetitions
, originName
)
, serial
)
)
])
}
function sendDeviceChange(device1, device2, action) {
function publishDevice() {
const device = _.cloneDeep(device1)
delete device.channel
delete device.owner
delete device.group.id
delete device.group.lifeTime
return device
}
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.DeviceChangeMessage(
publishDevice()
, action
, device2.group.origin
, timeutil.now('nano')
)
)
])
}
function sendReleaseDeviceControlAndDeviceGroupChange(
device
, sendDeviceGroupChangeWrapper) {
let messageListener
const responseTimer = setTimeout(function() {
channelRouter.removeListener(wireutil.global, messageListener)
sendDeviceGroupChangeWrapper()
}, 5000)
messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === device.serial &&
message.owner.email === device.owner.email) {
clearTimeout(responseTimer)
channelRouter.removeListener(wireutil.global, messageListener)
sendDeviceGroupChangeWrapper()
}
})
.handler()
channelRouter.on(wireutil.global, messageListener)
sendReleaseDeviceControl(device.serial, device.channel)
}
db.run(r
.table('devices')
.pluck(
'serial'
, 'channel'
, 'owner'
, 'model'
, 'operator'
, 'manufacturer'
, {group: ['id', 'origin', 'originName', 'lifeTime']}
, {provider: ['name']}
, {network: ['type', 'subtype']}
, {display: ['height', 'width']}
, 'version'
, 'sdk'
, 'abi'
, 'cpuPlatform'
, 'openGLESVersion'
, {phone: ['imei']}
, 'marketName'
)
.changes(), function(err, cursor) {
if (err) {
throw err
}
return cursor
})
.then(function(cursor) {
cursor.each(function(err, data) {
if (err) {
throw err
}
if (data.old_val === null) {
return sendDeviceChange(data.new_val, data.new_val, 'created')
}
else if (data.new_val === null) {
sendDeviceChange(data.old_val, data.old_val, 'deleted')
}
else if (data.new_val.model !== data.old_val.model ||
data.new_val.group.origin !== data.old_val.group.origin ||
data.new_val.operator !== data.old_val.operator ||
data.new_val.hasOwnProperty('network') &&
(!data.old_val.hasOwnProperty('network') ||
data.new_val.network.type !== data.old_val.network.type ||
data.new_val.network.subtype !== data.old_val.network.subtype
) ||
data.new_val.provider.name !== data.old_val.provider.name) {
sendDeviceChange(data.new_val, data.old_val, 'updated')
}
const isDeleted = data.new_val === null
const id = isDeleted ? data.old_val.group.id : data.new_val.group.id
return dbapi.getGroup(id).then(function(group) {
function sendDeviceGroupChangeOnDeviceDeletion() {
const fakeGroup = Object.assign({}, group)
fakeGroup.id = util.format('%s', uuid.v4()).replace(/-/g, '')
fakeGroup.name = 'none'
sendDeviceGroupChange(
group.id
, fakeGroup
, data.old_val.serial
, data.old_val.group.originName
)
}
function sendDeviceGroupChangeOnDeviceCurrentGroupUpdating() {
sendDeviceGroupChange(
data.old_val.group.id
, group
, data.new_val.serial
, data.new_val.group.originName
)
}
if (group) {
if (isDeleted) {
if (data.old_val.owner) {
sendReleaseDeviceControlAndDeviceGroupChange(
data.old_val
, sendDeviceGroupChangeOnDeviceDeletion
)
return
}
sendDeviceGroupChangeOnDeviceDeletion()
return
}
const isChangeCurrentGroup = data.new_val.group.id !== data.old_val.group.id
const isChangeOriginGroup = data.new_val.group.origin !== data.old_val.group.origin
const isChangeLifeTime =
data.new_val.group.lifeTime.start.getTime() !==
data.old_val.group.lifeTime.start.getTime()
if (isChangeLifeTime && !isChangeCurrentGroup && !isChangeOriginGroup) {
sendDeviceGroupChange(
data.old_val.group.id
, group
, data.new_val.serial
, data.new_val.group.originName
)
return
}
if (isChangeCurrentGroup) {
if (data.new_val.owner && group.users.indexOf(data.new_val.owner.email) < 0) {
sendReleaseDeviceControlAndDeviceGroupChange(
data.new_val
, sendDeviceGroupChangeOnDeviceCurrentGroupUpdating
)
}
else {
sendDeviceGroupChangeOnDeviceCurrentGroupUpdating()
}
}
if (isChangeOriginGroup) {
dbapi.getGroup(data.old_val.group.origin).then(function(originGroup) {
if (originGroup) {
dbapi.removeOriginGroupDevice(originGroup, data.new_val.serial)
}
})
dbapi.getGroup(data.new_val.group.origin).then(function(originGroup) {
if (originGroup) {
dbapi.addOriginGroupDevice(originGroup, data.new_val.serial)
}
})
if (!isChangeCurrentGroup) {
sendDeviceGroupChange(
data.new_val.group.id
, group
, data.new_val.serial
, data.new_val.group.originName
)
}
}
}
})
})
})
.catch(function(err) {
log.error('An error occured during DEVICES table watching', err.stack)
})
}

View file

@ -0,0 +1,346 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const wirerouter = require('../../../wire/router')
const Promise = require('bluebird')
const _ = require('lodash')
const r = require('rethinkdb')
const logger = require('../../../util/logger')
const timeutil = require('../../../util/timeutil')
const apiutil = require('../../../util/apiutil')
const wireutil = require('../../../wire/util')
const wire = require('../../../wire')
const dbapi = require('../../../db/api')
const db = require('../../../db')
module.exports = function(push, pushdev, channelRouter) {
const log = logger.createLogger('watcher-groups')
function sendReleaseDeviceControl(serial, channel) {
push.send([
channel
, wireutil.envelope(
new wire.UngroupMessage(
wireutil.toDeviceRequirements({
serial: {
value: serial
, match: 'exact'
}
})
)
)
])
}
function sendGroupChange(
group
, subscribers
, isChangedDates
, isChangedClass
, isAddedUser
, users
, isAddedDevice
, devices
, action) {
function dates2String(dates) {
return dates.map(function(date) {
return {
start: date.start.toJSON()
, stop: date.stop.toJSON()
}
})
}
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.GroupChangeMessage(
new wire.GroupField(
group.id
, group.name
, group.class
, group.privilege
, group.owner
, dates2String(group.dates)
, group.duration
, group.repetitions
, group.devices
, group.users
, group.state
, group.isActive
)
, action
, subscribers
, isChangedDates
, isChangedClass
, isAddedUser
, users
, isAddedDevice
, devices
, timeutil.now('nano')
)
)
])
}
function sendGroupUsersChange(group, users, devices, isAdded, action) {
const isDeletedLater = action === 'GroupDeletedLater'
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.GroupUserChangeMessage(users, isAdded, group.id, isDeletedLater, devices))
])
}
function doUpdateDeviceOriginGroup(group) {
return dbapi.updateDeviceOriginGroup(group.ticket.serial, group).then(function() {
push.send([
wireutil.global
, wireutil.envelope(
new wire.DeviceOriginGroupMessage(group.ticket.signature)
)
])
})
}
function doUpdateDevicesCurrentGroup(group, devices) {
return Promise.map(devices, function(serial) {
return dbapi.updateDeviceCurrentGroup(serial, group)
})
}
function doUpdateDevicesCurrentGroupFromOrigin(devices) {
return Promise.map(devices, function(serial) {
return dbapi.updateDeviceCurrentGroupFromOrigin(serial)
})
}
function doUpdateDevicesCurrentGroupDates(group) {
if (apiutil.isOriginGroup(group.class)) {
return Promise.map(group.devices, function(serial) {
return dbapi.loadDeviceBySerial(serial).then(function(device) {
return device.group.id === group.id ?
doUpdateDevicesCurrentGroup(group, [serial]) :
false
})
})
}
else {
return Promise.map(group.devices, function(serial) {
return doUpdateDevicesCurrentGroup(group, [serial])
})
}
}
function treatGroupUsersChange(group, users, isActive, isAddedUser) {
if (isActive) {
return Promise.map(users, function(email) {
return Promise.map(group.devices, function(serial) {
return dbapi.loadDeviceBySerial(serial).then(function(device) {
if (device && device.group.id === group.id) {
if (!isAddedUser && device.owner && device.owner.email === email) {
return new Promise(function(resolve) {
let messageListener
const responseTimer = setTimeout(function() {
channelRouter.removeListener(wireutil.global, messageListener)
resolve(serial)
}, 5000)
messageListener = wirerouter()
.on(wire.LeaveGroupMessage, function(channel, message) {
if (message.serial === serial &&
message.owner.email === email) {
clearTimeout(responseTimer)
channelRouter.removeListener(wireutil.global, messageListener)
resolve(serial)
}
})
.handler()
channelRouter.on(wireutil.global, messageListener)
sendReleaseDeviceControl(serial, device.channel)
})
}
return serial
}
return false
})
})
.then(function(devices) {
sendGroupUsersChange(
group, [email], _.without(devices, false), isAddedUser, 'GroupUser(s)Updated')
})
})
}
else {
return sendGroupUsersChange(group, users, [], isAddedUser, 'GroupUser(s)Updated')
}
}
function treatGroupDevicesChange(oldGroup, group, devices, isAddedDevice) {
if (isAddedDevice) {
return doUpdateDevicesCurrentGroup(group, devices)
}
else {
return doUpdateDevicesCurrentGroupFromOrigin(devices)
.then(function() {
if (group === null) {
sendGroupUsersChange(oldGroup, oldGroup.users, [], false, 'GroupDeletedLater')
}
})
}
}
function treatGroupDeletion(group) {
if (apiutil.isOriginGroup(group.class)) {
return dbapi.getRootGroup().then(function(rootGroup) {
return Promise.map(group.devices, function(serial) {
return dbapi.updateDeviceOriginGroup(serial, rootGroup)
})
.then(function() {
sendGroupUsersChange(group, group.users, [], false, 'GroupDeletedLater')
})
})
}
else {
return sendGroupUsersChange(group, group.users, [], false, 'GroupDeleted')
}
}
db.run(r
.table('groups')
.pluck(
'id'
, 'name'
, 'class'
, 'privilege'
, 'owner'
, 'dates'
, 'duration'
, 'repetitions'
, 'devices'
, 'users'
, 'state'
, 'isActive'
, 'ticket'
)
.changes(), function(err, cursor) {
if (err) {
throw err
}
return cursor
})
.then(function(cursor) {
cursor.each(function(err, data) {
let users, devices, isBecomeActive, isBecomeUnactive, isActive
, isAddedUser, isAddedDevice, isUpdatedDeviceOriginGroup, isChangedDates
if (err) {
throw err
}
if (data.old_val === null) {
sendGroupChange(
data.new_val
, data.new_val.users
, false
, false
, false
, []
, false
, []
, 'created'
)
return sendGroupUsersChange(
data.new_val
, data.new_val.users
, data.new_val.devices
, true
, 'GroupCreated'
)
}
if (data.new_val === null) {
sendGroupChange(
data.old_val
, data.old_val.users
, false
, false
, false
, []
, false
, []
, 'deleted'
)
users = data.old_val.users
devices = data.old_val.devices
isChangedDates = false
isActive = data.old_val.isActive
isBecomeActive = isBecomeUnactive = false
isAddedUser = isAddedDevice = false
isUpdatedDeviceOriginGroup = false
}
else {
users = _.xor(data.new_val.users, data.old_val.users)
devices = _.xor(data.new_val.devices, data.old_val.devices)
isChangedDates =
data.old_val.dates.length !== data.new_val.dates.length ||
data.old_val.dates[0].start.getTime() !==
data.new_val.dates[0].start.getTime() ||
data.old_val.dates[0].stop.getTime() !==
data.new_val.dates[0].stop.getTime()
isActive = data.new_val.isActive
isBecomeActive = !data.old_val.isActive && data.new_val.isActive
isBecomeUnactive = data.old_val.isActive && !data.new_val.isActive
isAddedUser = data.new_val.users.length > data.old_val.users.length
isAddedDevice = data.new_val.devices.length > data.old_val.devices.length
isUpdatedDeviceOriginGroup =
data.new_val.ticket !== null &&
(data.old_val.ticket === null ||
data.new_val.ticket.signature !== data.old_val.ticket.signature)
if (!isUpdatedDeviceOriginGroup) {
sendGroupChange(
data.new_val
, _.union(data.old_val.users, data.new_val.users)
, isChangedDates
, data.old_val.class !== data.new_val.class
, isAddedUser
, users
, isAddedDevice
, devices
, 'updated'
)
}
}
if (isUpdatedDeviceOriginGroup) {
return doUpdateDeviceOriginGroup(data.new_val)
}
else if (isBecomeActive && data.new_val.devices.length) {
return doUpdateDevicesCurrentGroup(data.new_val, data.new_val.devices)
}
else if (isBecomeUnactive && data.new_val.devices.length) {
return doUpdateDevicesCurrentGroupFromOrigin(data.new_val.devices)
}
else if (devices.length && isActive && !apiutil.isOriginGroup(data.old_val.class)) {
return treatGroupDevicesChange(data.old_val, data.new_val, devices, isAddedDevice)
}
else if (data.new_val === null) {
return treatGroupDeletion(data.old_val)
}
else if (isChangedDates && isActive) {
return doUpdateDevicesCurrentGroupDates(data.new_val)
}
else if (users.length) {
return treatGroupUsersChange(data.old_val, users, isActive, isAddedUser)
}
return true
})
})
.catch(function(err) {
log.error('An error occured during GROUPS table watching', err.stack)
})
}

View file

@ -0,0 +1,94 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const timeutil = require('../../../util/timeutil')
const r = require('rethinkdb')
const _ = require('lodash')
const logger = require('../../../util/logger')
const wireutil = require('../../../wire/util')
const wire = require('../../../wire')
const db = require('../../../db')
module.exports = function(pushdev) {
const log = logger.createLogger('watcher-users')
function sendUserChange(user, isAddedGroup, groups, action, targets) {
pushdev.send([
wireutil.global
, wireutil.envelope(
new wire.UserChangeMessage(
user
, isAddedGroup
, groups
, action
, targets
, timeutil.now('nano')))
])
}
db.run(r
.table('users')
.pluck(
'email'
, 'name'
, 'privilege'
, {groups: ['quotas', 'subscribed']
})
.changes(), function(err, cursor) {
if (err) {
throw err
}
return cursor
})
.then(function(cursor) {
cursor.each(function(err, data) {
if (err) {
throw err
}
if (data.old_val === null) {
sendUserChange(data.new_val, false, [], 'created', ['settings'])
}
else if (data.new_val === null) {
sendUserChange(data.old_val, false, [], 'deleted', ['settings'])
}
else {
const targets = []
if (!_.isEqual(
data.new_val.groups.quotas.allocated
, data.old_val.groups.quotas.allocated)) {
targets.push('settings')
targets.push('view')
}
else if (!_.isEqual(
data.new_val.groups.quotas.consumed
, data.old_val.groups.quotas.consumed)) {
targets.push('view')
}
else if (data.new_val.groups.quotas.defaultGroupsNumber !==
data.old_val.groups.quotas.defaultGroupsNumber ||
data.new_val.groups.quotas.defaultGroupsDuration !==
data.old_val.groups.quotas.defaultGroupsDuration ||
data.new_val.groups.quotas.defaultGroupsRepetitions !==
data.old_val.groups.quotas.defaultGroupsRepetitions ||
data.new_val.groups.quotas.repetitions !==
data.old_val.groups.quotas.repetitions ||
!_.isEqual(data.new_val.groups.subscribed, data.old_val.groups.subscribed)) {
targets.push('settings')
}
if (targets.length) {
sendUserChange(
data.new_val
, data.new_val.groups.subscribed.length > data.old_val.groups.subscribed.length
, _.xor(data.new_val.groups.subscribed, data.old_val.groups.subscribed)
, 'updated'
, targets)
}
}
})
})
.catch(function(err) {
log.error('An error occured during USERS table watching', err.stack)
})
}

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var Promise = require('bluebird')
var logger = require('../../util/logger')
@ -55,17 +59,70 @@ module.exports = db.ensureConnectivity(function(options) {
})
devDealer.on('message', wirerouter()
.on(wire.UpdateAccessTokenMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeleteUserMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeviceChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.UserChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.GroupChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeviceGroupChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.GroupUserChangeMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
// Initial device message
.on(wire.DeviceIntroductionMessage, function(channel, message, data) {
.on(wire.DeviceIntroductionMessage, function(channel, message) {
dbapi.saveDeviceInitialState(message.serial, message)
.then(function() {
.then(function(device) {
devDealer.send([
message.provider.channel
, wireutil.envelope(new wire.DeviceRegisteredMessage(
message.serial
))
])
appDealer.send([channel, data])
appDealer.send([
channel
, wireutil.envelope(new wire.DeviceIntroductionMessage(
message.serial
, message.status
, new wire.ProviderMessage(
message.provider.channel
, message.provider.name
)
, new wire.DeviceGroupMessage(
device.group.id
, device.group.name
, new wire.DeviceGroupOwnerMessage(
device.group.owner.email
, device.group.owner.name
)
, new wire.DeviceGroupLifetimeMessage(
device.group.lifeTime.start.getTime()
, device.group.lifeTime.stop.getTime()
)
, device.group.class
, device.group.repetitions
, device.group.originName
)
))
])
})
.catch(function(err) {
log.error(
'Unable to save the initial state of Device "%s"'
, message.serial
, err.stack
)
})
})
// Workerless messages

View file

@ -37,7 +37,7 @@ module.exports = function(options) {
})
.catch(function(err) {
log.error('Unable to read manifest of "%s"', req.params.id, err.stack)
res.status(500)
res.status(200)
.json({
success: false
})

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var http = require('http')
var events = require('events')
var util = require('util')
@ -23,6 +27,8 @@ var ip = require('./middleware/remote-ip')
var auth = require('./middleware/auth')
var jwtutil = require('../../util/jwtutil')
const apiutil = require('../../util/apiutil')
module.exports = function(options) {
var log = logger.createLogger('websocket')
var server = http.createServer()
@ -118,23 +124,99 @@ module.exports = function(options) {
}
}
let disconnectSocket
var messageListener = wirerouter()
.on(wire.UpdateAccessTokenMessage, function() {
socket.emit('user.keys.accessToken.updated')
})
.on(wire.DeleteUserMessage, function() {
disconnectSocket(true)
})
.on(wire.DeviceChangeMessage, function(channel, message) {
if (user.groups.subscribed.indexOf(message.device.group.origin) > -1 ||
user.groups.subscribed.indexOf(message.oldOriginGroupId) > -1) {
socket.emit('user.settings.devices.' + message.action, message)
}
})
.on(wire.UserChangeMessage, function(channel, message) {
Promise.map(message.targets, function(target) {
socket.emit('user.' + target + '.users.' + message.action, message)
})
})
.on(wire.GroupChangeMessage, function(channel, message) {
if (user.privilege === 'admin' ||
user.email === message.group.owner.email ||
!apiutil.isOriginGroup(message.group.class) &&
(message.action === 'deleted' ||
message.action === 'updated' &&
(message.isChangedDates || message.isChangedClass || message.devices.length))) {
socket.emit('user.settings.groups.' + message.action, message)
}
if (message.subscribers.indexOf(user.email) > -1) {
socket.emit('user.view.groups.' + message.action, message)
}
})
.on(wire.DeviceGroupChangeMessage, function(channel, message) {
if (user.groups.subscribed.indexOf(message.id) > -1) {
if (user.groups.subscribed.indexOf(message.group.id) > -1) {
socket.emit('device.updateGroupDevice', {
important: true
, data: {
serial: message.serial
, group: message.group
}
})
}
else {
socket.emit('device.removeGroupDevices', {important: true, devices: [message.serial]})
}
}
else if (user.groups.subscribed.indexOf(message.group.id) > -1) {
socket.emit('device.addGroupDevices', {important: true, devices: [message.serial]})
}
})
.on(wire.GroupUserChangeMessage, function(channel, message) {
if (message.users.indexOf(user.email) > -1) {
if (message.isAdded) {
user.groups.subscribed = _.union(user.groups.subscribed, [message.id])
if (message.devices.length) {
socket.emit('device.addGroupDevices', {important: true, devices: message.devices})
}
}
else {
if (message.devices.length) {
socket.emit('device.removeGroupDevices', {important: true, devices: message.devices})
}
if (message.isDeletedLater) {
setTimeout(function() {
user.groups.subscribed = _.without(user.groups.subscribed, message.id)
}, 5000)
}
else {
user.groups.subscribed = _.without(user.groups.subscribed, message.id)
}
}
}
})
.on(wire.DeviceLogMessage, function(channel, message) {
socket.emit('device.log', message)
})
.on(wire.DeviceIntroductionMessage, function(channel, message) {
if (user.groups.subscribed.indexOf(message.group.id) > -1) {
socket.emit('device.add', {
important: true
, data: {
serial: message.serial
, present: false
, present: true
, provider: message.provider
, owner: null
, status: message.status
, ready: false
, reverseForwards: []
, group: message.group
}
})
}
})
.on(wire.DeviceReadyMessage, function(channel, message) {
socket.emit('device.change', {
@ -307,6 +389,7 @@ module.exports = function(options) {
joinChannel(user.group)
new Promise(function(resolve) {
disconnectSocket = resolve
socket.on('disconnect', resolve)
// Global messages for all clients using socket.io
//
@ -314,10 +397,12 @@ module.exports = function(options) {
.on('device.note', function(data) {
return dbapi.setDeviceNote(data.serial, data.note)
.then(function() {
return dbapi.loadDevice(data.serial)
return dbapi.loadDevice(user.groups.subscribed, data.serial)
})
.then(function(device) {
if (device) {
.then(function(cursor) {
if (cursor) {
cursor.next(function(err, device) {
if (!err) {
io.emit('device.change', {
important: true
, data: {
@ -327,6 +412,8 @@ module.exports = function(options) {
})
}
})
}
})
})
// Client specific messages
//
@ -364,7 +451,7 @@ module.exports = function(options) {
.on('user.keys.accessToken.remove', function(data) {
return dbapi.removeUserAccessToken(user.email, data.title)
.then(function() {
socket.emit('user.keys.accessToken.removed', data.title)
socket.emit('user.keys.accessToken.updated')
})
})
.on('user.keys.adb.add', function(data) {
@ -916,6 +1003,7 @@ module.exports = function(options) {
channelRouter.removeListener(channel, messageListener)
sub.unsubscribe(channel)
})
socket.disconnect(true)
})
.catch(function(err) {
// Cannot guarantee integrity of client
@ -923,8 +1011,7 @@ module.exports = function(options) {
'Client had an error, disconnecting due to probable loss of integrity'
, err.stack
)
socket.disconnect(true)
// move 'socket.disconnect(true)' statement to finally block instead!
})
})

257
lib/util/apiutil.js Normal file
View file

@ -0,0 +1,257 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const Promise = require('bluebird')
const _ = require('lodash')
const logger = require('./logger')
const datautil = require('./datautil')
const apiutil = Object.create(null)
const log = logger.createLogger('api:controllers:apiutil')
apiutil.PENDING = 'pending'
apiutil.READY = 'ready'
apiutil.WAITING = 'waiting'
apiutil.BOOKABLE = 'bookable'
apiutil.STANDARD = 'standard'
apiutil.ONCE = 'once'
apiutil.DEBUG = 'debug'
apiutil.ORIGIN = 'origin'
apiutil.STANDARDIZABLE = 'standardizable'
apiutil.ROOT = 'root'
apiutil.ADMIN = 'admin'
apiutil.USER = 'user'
apiutil.FIVE_MN = 300 * 1000
apiutil.ONE_HOUR = 3600 * 1000
apiutil.ONE_DAY = 24 * apiutil.ONE_HOUR
apiutil.ONE_WEEK = 7 * apiutil.ONE_DAY
apiutil.ONE_MONTH = 30 * apiutil.ONE_DAY
apiutil.ONE_QUATER = 3 * apiutil.ONE_MONTH
apiutil.ONE_HALF_YEAR = 6 * apiutil.ONE_MONTH
apiutil.ONE_YEAR = 365 * apiutil.ONE_DAY
apiutil.MAX_USER_GROUPS_NUMBER = 5
apiutil.MAX_USER_GROUPS_DURATION = 15 * apiutil.ONE_DAY
apiutil.MAX_USER_GROUPS_REPETITIONS = 10
apiutil.CLASS_DURATION = {
once: Infinity
, bookable: Infinity
, standard: Infinity
, hourly: apiutil.ONE_HOUR
, daily: apiutil.ONE_DAY
, weekly: apiutil.ONE_WEEK
, monthly: apiutil.ONE_MONTH
, quaterly: apiutil.ONE_QUATER
, halfyearly: apiutil.ONE_HALF_YEAR
, yearly: apiutil.ONE_YEAR
, debug: apiutil.FIVE_MN
}
apiutil.isOriginGroup = function(_class) {
return _class === apiutil.BOOKABLE || _class === apiutil.STANDARD
}
apiutil.isAdminGroup = function(_class) {
return apiutil.isOriginGroup(_class) || _class === apiutil.DEBUG
}
apiutil.internalError = function(res, ...args) {
log.error.apply(log, args)
apiutil.respond(res, 500, 'Internal Server Error')
}
apiutil.respond = function(res, code, message, data) {
const status = code >= 200 && code < 300
const response = {
success: status
, description: message
}
if (data) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
response[key] = data[key]
}
}
}
res.status(code).json(response)
return status
}
apiutil.publishGroup = function(group) {
// delete group.lock
delete group.createdAt
delete group.ticket
return group
}
apiutil.publishDevice = function(device, user) {
datautil.normalize(device, user)
// delete device.group.lock
return device
}
apiutil.publishUser = function(user) {
// delete user.groups.lock
return user
}
apiutil.publishAccessToken = function(token) {
delete token.email
delete token.jwt
return token
}
apiutil.filterDevice = function(req, device) {
const fields = req.swagger.params.fields.value
if (fields) {
return _.pick(apiutil.publishDevice(device, req.user), fields.split(','))
}
return apiutil.publishDevice(device, req.user)
}
apiutil.computeDuration = function(group, deviceNumber) {
return (group.devices.length + deviceNumber) *
(group.dates[0].stop - group.dates[0].start) *
(group.repetitions + 1)
}
apiutil.lightComputeStats = function(res, stats) {
if (stats.locked) {
apiutil.respond(res, 503, 'Server too busy, please try again later')
return Promise.reject('busy')
}
return 'not found'
}
apiutil.computeStats = function(res, stats, objectName, ...lock) {
if (!stats.replaced) {
if (stats.skipped) {
return apiutil.respond(res, 404, `Not Found (${objectName})`)
}
if (stats.locked) {
return apiutil.respond(res, 503, 'Server too busy, please try again later')
}
return apiutil.respond(res, 403, `Forbidden (${objectName})`)
}
if (lock.length) {
lock[0][objectName] = stats.changes[0].new_val
}
return true
}
apiutil.lockResult = function(stats) {
const result = {status: false, data: stats}
if (stats.replaced || stats.skipped) {
result.status = true
result.data.locked = false
}
else {
result.data.locked = true
}
return result
}
apiutil.lockDeviceResult = function(stats, fn, groups, serial) {
const result = apiutil.lockResult(stats)
if (!result.status) {
return fn(groups, serial).then(function(devices) {
if (!devices.length) {
result.data.locked = false
result.status = true
}
return result
})
}
return result
}
apiutil.setIntervalWrapper = function(fn, numTimes, delay) {
return fn().then(function(result) {
if (result.status) {
return result.data
}
return new Promise(function(resolve, reject) {
let counter = 0
const interval = setInterval(function() {
return fn().then(function(result) {
if (result.status || ++counter === numTimes) {
if (!result.status && counter === numTimes) {
log.debug('%s() failed %s times in a loop!', fn.name, counter)
}
clearInterval(interval)
resolve(result.data)
}
})
.catch(function(err) {
clearInterval(interval)
reject(err)
})
}, delay)
})
})
}
apiutil.redirectApiWrapper = function(field, fn, req, res) {
if (typeof req.body === 'undefined') {
req.body = {}
}
req.body[field + 's'] = req.swagger.params[field].value
req.swagger.params.redirected = {value: true}
fn(req, res)
}
apiutil.computeGroupDates = function(lifeTime, _class, repetitions) {
const dates = new Array(lifeTime)
for(let repetition = 1
, currentLifeTime = {
start: new Date(lifeTime.start.getTime())
, stop: new Date(lifeTime.stop.getTime())
}
; repetition <= repetitions
; repetition++) {
currentLifeTime.start = new Date(
currentLifeTime.start.getTime() +
apiutil.CLASS_DURATION[_class]
)
currentLifeTime.stop = new Date(
currentLifeTime.stop.getTime() +
apiutil.CLASS_DURATION[_class]
)
dates.push({
start: new Date(currentLifeTime.start.getTime())
, stop: new Date(currentLifeTime.stop.getTime())
})
}
return dates
}
apiutil.checkBodyParameter = function(body, parameter) {
return typeof body !== 'undefined' && typeof body[parameter] !== 'undefined'
}
apiutil.getBodyParameter = function(body, parameter) {
let undef
return apiutil.checkBodyParameter(body, parameter) ? body[parameter] : undef
}
apiutil.checkQueryParameter = function(parameter) {
return typeof parameter !== 'undefined' && typeof parameter.value !== 'undefined'
}
apiutil.getQueryParameter = function(parameter) {
let undef
return apiutil.checkQueryParameter(parameter) ? parameter.value : undef
}
module.exports = apiutil

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var deviceData = require('stf-device-db')
var browserData = require('stf-browser-db')
@ -41,13 +45,14 @@ datautil.applyBrowsers = function(device) {
}
datautil.applyOwner = function(device, user) {
device.using = !!device.owner && device.owner.email === user.email
device.using = !!device.owner &&
(device.owner.email === user.email || user.privilege === 'admin')
return device
}
// Only owner can see this information
datautil.applyOwnerOnlyInfo = function(device, user) {
if (device.owner && device.owner.email === user.email) {
if (device.owner && (device.owner.email === user.email || user.privilege === 'admin')) {
// No-op
}
else {

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var logger = require('./logger')
var log = logger.createLogger('util:deviceutil')
@ -8,7 +12,7 @@ deviceutil.isOwnedByUser = function(device, user) {
return device.present &&
device.ready &&
device.owner &&
device.owner.email === user.email &&
(device.owner.email === user.email || user.privilege === 'admin') &&
device.using
}

View file

@ -2,6 +2,7 @@ var util = require('util')
var split = require('split')
var Promise = require('bluebird')
var androidDeviceList = require('android-device-list')
var devutil = module.exports = Object.create(null)
@ -135,6 +136,7 @@ devutil.makeIdentity = function(serial, properties) {
var product = properties['ro.product.name']
var cpuPlatform = properties['ro.board.platform']
var openGLESVersion = properties['ro.opengles.version']
var marketName = properties['ro.product.device']
openGLESVersion = parseInt(openGLESVersion, 10)
if (isNaN(openGLESVersion)) {
@ -157,6 +159,13 @@ devutil.makeIdentity = function(serial, properties) {
model = model.substr(manufacturer.length)
}
if (marketName) {
var devices = androidDeviceList.getDevicesByDeviceId(marketName)
if (devices.length > 0) {
marketName = devices[0].name
}
}
// Clean up remaining model name
// model = model.replace(/[_ ]/g, '')
return {
@ -171,5 +180,6 @@ devutil.makeIdentity = function(serial, properties) {
, product: product
, cpuPlatform: cpuPlatform
, openGLESVersion: openGLESVersion
, marketName: marketName
}
}

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var util = require('util')
var uuid = require('uuid')
@ -7,10 +11,8 @@ var dbapi = require('../db/api')
var devices = require('stf-device-db/dist/devices-latest')
module.exports.generate = function(wantedModel) {
var serial = util.format(
'fake-%s'
, uuid.v4(null, new Buffer(16)).toString('base64')
)
// no base64 because some characters as '=' or '/' are not compatible through API (delete devices)
const serial = 'fake-' + util.format('%s', uuid.v4()).replace(/-/g, '')
return dbapi.saveDeviceInitialState(serial, {
provider: {
@ -28,7 +30,7 @@ module.exports.generate = function(wantedModel) {
, model: model
, version: '4.1.2'
, abi: 'armeabi-v7a'
, sdk: 8 + Math.floor(Math.random() * 12)
, sdk: (8 + Math.floor(Math.random() * 12)).toString() // string required!
, display: {
density: 3
, fps: 60
@ -49,6 +51,9 @@ module.exports.generate = function(wantedModel) {
, phoneNumber: '0000000000'
}
, product: model
, cpuPlatform: 'msm8996'
, openGLESVersion: '3.1'
, marketName: 'Bar F9+'
})
})
.then(function() {

42
lib/util/fakegroup.js Normal file
View file

@ -0,0 +1,42 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const util = require('util')
const uuid = require('uuid')
const dbapi = require('../db/api')
const apiutil = require('./apiutil')
module.exports.generate = function() {
return dbapi.getRootGroup().then(function(rootGroup) {
const now = Date.now()
return dbapi.createUserGroup({
name: 'fakegroup-' + util.format('%s', uuid.v4()).replace(/-/g, '')
, owner: {
email: rootGroup.owner.email
, name: rootGroup.owner.name
}
, privilege: apiutil.ADMIN
, class: apiutil.BOOKABLE
, repetitions: 0
, isActive: true
, dates: apiutil.computeGroupDates(
{
start: new Date(now)
, stop: new Date(now + apiutil.ONE_YEAR)
}
, apiutil.BOOKABLE
, 0
)
, duration: 0
, state: apiutil.READY
})
.then(function(group) {
if (group) {
return group.id
}
throw new Error('Forbidden (groups number quota is reached)')
})
})
}

14
lib/util/fakeuser.js Normal file
View file

@ -0,0 +1,14 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const util = require('util')
const uuid = require('uuid')
const dbapi = require('../db/api')
module.exports.generate = function() {
const name = 'fakeuser-' + util.format('%s', uuid.v4()).replace(/-/g, '')
const email = name + '@openstf.com'
return dbapi.createUser(email, name, '127.0.0.1').return(email)
}

69
lib/util/lockutil.js Normal file
View file

@ -0,0 +1,69 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const apiutil = require('./apiutil')
const dbapi = require('../db/api')
const lockutil = Object.create(null)
lockutil.unlockDevice = function(lock) {
if (lock.device) {
dbapi.unlockDevice(lock.device.serial)
}
}
lockutil.lockUser = function(email, res, lock) {
return dbapi.lockUser(email)
.then(function(stats) {
return apiutil.computeStats(res, stats, 'user', lock)
})
}
lockutil.unlockUser = function(lock) {
if (lock.user) {
dbapi.unlockUser(lock.user.email)
}
}
lockutil.lockGroupAndUser = function(req, res, lock) {
return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
return lockingSuccessed ?
lockutil.lockUser(req.user.email, res, lock) :
false
})
}
lockutil.unlockGroupAndUser = function(lock) {
lockutil.unlockGroup(lock)
lockutil.unlockUser(lock)
}
lockutil.lockGroup = function(req, res, lock) {
const id = req.swagger.params.id.value
const email = req.user.email
return dbapi.lockGroupByOwner(email, id).then(function(stats) {
return apiutil.computeStats(res, stats, 'group', lock)
})
}
lockutil.unlockGroup = function(lock) {
if (lock.group) {
dbapi.unlockGroup(lock.group.id)
}
}
lockutil.unlockGroupAndDevice = function(lock) {
lockutil.unlockGroup(lock)
lockutil.unlockDevice(lock)
}
lockutil.lockGenericDevice = function(req, res, lock, lockDevice) {
return lockDevice(req.user.groups.subscribed, req.swagger.params.serial.value)
.then(function(stats) {
return apiutil.computeStats(res, stats, 'device', lock)
})
}
module.exports = lockutil

View file

@ -36,6 +36,8 @@ module.exports.readAll = function(stream) {
stream.on('readable', readableListener)
stream.on('end', endListener)
readableListener()
return resolver.promise.finally(function() {
stream.removeListener('error', errorListener)
stream.removeListener('readable', readableListener)

22
lib/util/timeutil.js Normal file
View file

@ -0,0 +1,22 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const timeutil = Object.create(null)
timeutil.now = function(unit) {
const hrTime = process.hrtime()
switch (unit) {
case 'milli':
return hrTime[0] * 1000 + hrTime[1] / 1000000
case 'micro':
return hrTime[0] * 1000000 + hrTime[1] / 1000
case 'nano':
return hrTime[0] * 1000000000 + hrTime[1]
default:
return hrTime[0] * 1000000000 + hrTime[1]
}
}
module.exports = timeutil

View file

@ -1,3 +1,7 @@
//
// Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
// Message wrapper
enum MessageType {
@ -78,6 +82,160 @@ enum MessageType {
FileSystemGetMessage = 82;
ConnectStartedMessage = 92;
ConnectStoppedMessage = 93;
GroupUserChangeMessage = 1200;
DeviceGroupChangeMessage = 1201;
DeviceOriginGroupMessage = 1202;
DeleteUserMessage = 1203;
UpdateAccessTokenMessage = 1204;
GroupChangeMessage = 1205;
UserChangeMessage = 1206;
DeviceChangeMessage = 1207;
}
message UpdateAccessTokenMessage {
}
message DeleteUserMessage {
required string email = 1;
}
message DeviceOriginGroupMessage {
required string signature = 1;
}
message UserQuotasDetailField {
required double duration = 1;
required uint32 number = 2;
}
message UserQuotasField {
required UserQuotasDetailField allocated = 1;
required UserQuotasDetailField consumed = 2;
required uint32 defaultGroupsDuration = 3;
required uint32 defaultGroupsNumber = 4;
required uint32 defaultGroupsRepetitions = 5;
required uint32 repetitions = 6;
}
message UserGroupsField {
required UserQuotasField quotas = 1;
repeated string subscribed = 2;
}
message UserField {
required string email = 1;
required string name = 2;
required string privilege = 3;
required UserGroupsField groups = 4;
}
message UserChangeMessage {
required UserField user = 1;
required bool isAddedGroup = 2;
repeated string groups = 3;
required string action = 4;
repeated string targets = 5;
required double timeStamp = 6;
}
message DeviceNetworkField {
optional string type = 1;
optional string subtype = 2;
}
message DeviceDisplayField {
optional uint32 height = 1;
optional uint32 width = 2;
}
message DevicePhoneField {
optional string imei = 1;
}
message DeviceProviderField {
optional string name = 1;
}
message DeviceGroupField {
optional string origin = 1;
optional string originName = 2;
}
message DeviceField {
required string serial = 1;
optional string model = 2;
optional string version = 3;
optional string operator = 4;
optional DeviceNetworkField network = 5;
optional DeviceDisplayField display = 6;
optional string manufacturer = 7;
optional string sdk = 8;
optional string abi = 9;
optional string cpuPlatform = 10;
optional string openGLESVersion = 11;
optional DevicePhoneField phone = 12;
optional DeviceProviderField provider = 13;
optional DeviceGroupField group = 14;
optional string marketName = 15;
}
message DeviceChangeMessage {
required DeviceField device = 1;
required string action = 2;
required string oldOriginGroupId = 3;
required double timeStamp = 4;
}
message GroupDateField {
required string start = 1;
required string stop = 2;
}
message GroupOwnerField {
required string email = 1;
required string name = 2;
}
message GroupField {
required string id = 1;
required string name = 2;
required string class = 3;
required string privilege = 4;
required GroupOwnerField owner = 5;
repeated GroupDateField dates = 6;
required uint32 duration = 7;
required uint32 repetitions = 8;
repeated string devices = 9;
repeated string users = 10;
required string state = 11;
required bool isActive = 12;
}
message GroupChangeMessage {
required GroupField group = 1;
required string action = 2;
repeated string subscribers = 3;
required bool isChangedDates = 4;
required bool isChangedClass = 5;
required bool isAddedUser = 6;
repeated string users = 7;
required bool isAddedDevice = 8;
repeated string devices = 9;
required double timeStamp = 10;
}
message DeviceGroupChangeMessage {
required string id = 1;
required DeviceGroupMessage group = 2;
required string serial = 3;
}
message GroupUserChangeMessage {
repeated string users = 1;
required bool isAdded = 2;
required string id = 3;
required bool isDeletedLater = 4;
repeated string devices = 5;
}
message ConnectStartedMessage {
@ -132,6 +290,26 @@ message DeviceLogMessage {
// Introductions
message DeviceGroupOwnerMessage {
required string email = 1;
required string name = 2;
}
message DeviceGroupLifetimeMessage {
required double start = 1;
required double stop = 2;
}
message DeviceGroupMessage {
required string id = 1;
required string name = 2;
required DeviceGroupOwnerMessage owner = 3;
required DeviceGroupLifetimeMessage lifeTime = 4;
required string class = 5;
required uint32 repetitions = 6;
required string originName = 7;
}
message ProviderMessage {
required string channel = 1;
required string name = 2;
@ -145,6 +323,7 @@ message DeviceIntroductionMessage {
required string serial = 1;
required DeviceStatus status = 2;
required ProviderMessage provider = 3;
optional DeviceGroupMessage group = 4;
}
message DeviceRegisteredMessage {
@ -230,6 +409,7 @@ message DeviceIdentityMessage {
optional string product = 12;
optional string cpuPlatform = 13;
optional string openGLESVersion = 14;
optional string marketName = 15;
}
message DeviceProperty {

View file

@ -37,6 +37,7 @@
"adbkit": "^2.11.1",
"adbkit-apkreader": "^3.1.1",
"adbkit-monkey": "^1.0.1",
"android-device-list": "^1.2.1",
"aws-sdk": "^2.4.13",
"basic-auth": "^1.0.3",
"bluebird": "^2.10.1",
@ -62,9 +63,9 @@
"lodash": "^4.14.2",
"markdown-serve": "^0.3.2",
"mime": "^1.3.4",
"minicap-prebuilt": "^2.3.0",
"minicap-prebuilt-beta": "^2.4.0",
"minimatch": "^3.0.3",
"minitouch-prebuilt": "^1.2.0",
"minitouch-prebuilt-beta": "^1.3.0",
"my-local-ip": "^1.0.0",
"openid": "^2.0.1",
"passport": "^0.3.2",
@ -109,6 +110,7 @@
"exports-loader": "^0.6.2",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"fs-extra": "^8.1.0",
"gulp": "^3.8.11",
"gulp-angular-gettext": "^2.1.0",
"gulp-eslint": "^3.0.1",
@ -118,15 +120,16 @@
"gulp-run": "^1.6.12",
"gulp-util": "^3.0.7",
"html-loader": "^0.4.0",
"http-https": "^1.0.0",
"imports-loader": "^0.6.5",
"jasmine-core": "^2.4.1",
"jasmine-reporters": "^2.1.1",
"jasmine-reporters": "^2.3.2",
"json-loader": "^0.5.4",
"karma": "^1.1.2",
"karma-chrome-launcher": "^1.0.1",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.0.0",
"karma-ie-launcher": "^1.0.0",
"karma-jasmine": "^1.0.2",
"karma-jasmine": "^2.0.1",
"karma-junit-reporter": "^1.1.0",
"karma-opera-launcher": "^1.0.0",
"karma-phantomjs-launcher": "^1.0.0",
@ -138,8 +141,8 @@
"node-libs-browser": "^1.0.0",
"node-sass": "^3.4.2",
"phantomjs-prebuilt": "^2.1.11",
"protractor": "^4.0.3",
"protractor-html-screenshot-reporter": "0.0.21",
"protractor": "^5.4.1",
"protractor-html-reporter-2": "1.0.4",
"raw-loader": "^0.5.1",
"sass-loader": "^4.0.0",
"script-loader": "^0.7.0",
@ -151,7 +154,7 @@
"then-jade": "^2.4.1",
"url-loader": "^0.5.7",
"webpack": "^1.12.11",
"webpack-dev-server": "^1.14.1"
"webpack-dev-server": "^3.1.11"
},
"engines": {
"node": ">= 6.9"

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
require.ensure([], function(require) {
require('angular')
require('angular-route')
@ -10,13 +14,15 @@ require.ensure([], function(require) {
require('angular-hotkeys').name,
require('./layout').name,
require('./device-list').name,
require('./group-list').name,
require('./control-panes').name,
require('./menu').name,
require('./settings').name,
require('./docs').name,
require('./user').name,
require('./../common/lang').name,
require('stf/standalone').name
require('stf/standalone').name,
require('./group-list').name
])
.config(function($routeProvider, $locationProvider) {
$locationProvider.hashPrefix('!')

View file

@ -0,0 +1,15 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function() {
return {
restrict: 'E',
scope: {
buttonStyle: '@?',
columnData: '=',
resetData: '&'
},
template: require('./column-choice.pug'),
}
}

View file

@ -0,0 +1,23 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
.stf-column-choice .stf-column-customize {
white-space: nowrap;
padding: 10px;
padding-bottom: 0;
column-count: 2;
-moz-column-count: 2;
-webkit-column-count: 2;
max-width: 800px;
}
.stf-column-choice .stf-column-customize .checkbox {
margin-bottom: 10px;
}
.stf-column-choice .stf-column-customize .checkbox-label {
margin-left: 10px;
}

View file

@ -0,0 +1,24 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.stf-column-choice
.btn-group(uib-dropdown auto-close='outsideClick')
button.btn.btn-sm.btn-primary-outline(
style='margin-top: 5px; {{buttonStyle}}'
type='button'
uib-dropdown-toggle)
i.fa.fa-columns
span(translate) Customize
ul.dropdown-menu.pointer.stf-column-customize(
uib-dropdown-menu role='menu'
ng-click='$event.stopPropagation()')
li(ng-repeat='column in columnData')
label.checkbox.pointer
input(type='checkbox' ng-model='column.selected')
span.checkbox-label(ng-bind-template='{{::column.name | translate}}')
li
button.btn.btn-xs.btn-danger-outline.checkbox(ng-click='resetData()')
i.fa.fa-trash-o
span(ng-bind='"Reset"|translate')

View file

@ -0,0 +1,12 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
require('./column-choice.css')
module.exports = angular.module('stf.column-choice', [
require('stf/common-ui').name
])
.directive('stfColumnChoice', require('./column-choice-directive'))

View file

@ -1,4 +1,9 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf/common-ui', [
require('./pagination').name,
require('./safe-apply').name,
require('./clear-button').name,
require('./filter-button').name,

View file

@ -0,0 +1,38 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports =
function GenericModalServiceFactory($uibModal) {
const service = {}
const ModalInstanceCtrl = function($scope, $uibModalInstance, data) {
$scope.data = data
$scope.ok = function() {
$uibModalInstance.close(true)
}
$scope.cancel = function() {
$uibModalInstance.dismiss('cancel')
}
}
service.open = function(data) {
var modalInstance = $uibModal.open({
template: require('./generic-modal.pug'),
controller: ModalInstanceCtrl,
size: data.size,
animation: true,
resolve: {
data: function() {
return data
}
}
})
return modalInstance.result
}
return service
}

View file

@ -0,0 +1,15 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
describe('GenericModalService', function() {
beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function() {
//expect(FatalMessageService.doSomething()).toEqual('something');
}))
})

View file

@ -0,0 +1,36 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.stf-generic-modal.stf-modal
.modal-header
h4.modal-title.text-warning(ng-if="data.type === 'Warning'")
i.fa.fa-warning
.button-spacer
span(translate) {{data.type}}
h4.modal-title.text-info(ng-if="data.type === 'Information'")
i.fa.fa-info-circle
.button-spacer
span(translate) {{data.type}}
h4.modal-title.text-danger(ng-if="data.type === 'Error'")
i.fa.fa-times-circle
.button-spacer
span(translate) {{data.type}}
.modal-body
label.control-label
span(translate) {{data.message}}
.modal-footer
button.btn.btn-primary(
type='button'
ng-click='ok()')
span(translate) OK
button.btn.btn-warning(
type='button'
ng-if='data.cancel'
ng-click='cancel()')
span(translate) Cancel

View file

@ -0,0 +1,8 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.generic-modal', [
require('stf/common-ui/modals/common').name
])
.factory('GenericModalService', require('./generic-modal-service'))

View file

@ -1,4 +1,9 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.modals', [
require('./generic-modal').name,
require('./fatal-message').name,
require('./socket-disconnected').name,
require('./version-update').name,

View file

@ -0,0 +1,12 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
require('./pagination.css')
module.exports = angular.module('stf.pagination', [
])
.filter('pagedObjectsFilter', require('./pagination-filter'))
.directive('stfPager', require('./pagination-directive'))
.factory('ItemsPerPageOptionsService', require('./pagination-service'))

View file

@ -0,0 +1,24 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function() {
return {
restrict: 'E',
scope: {
tooltipLabel: '@',
iconStyle: '@?',
itemsSearchStyle: '@?',
itemsSearch: '=',
itemsPerPageOptions: '<',
itemsPerPage: '=',
totalItems: '<',
totalItemsStyle: '@?',
currentPage: '='
},
template: require('./pagination.pug'),
link: function(scope, element, attrs) {
scope.currentPage = 1
}
}
}

View file

@ -0,0 +1,16 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function() {
return function(objects, scope, currentPage, maxItems, searchItems) {
scope[searchItems] = objects
if (scope[maxItems].value === 0) {
return objects
}
return objects.slice(
(scope[currentPage] - 1) * scope[maxItems].value
, scope[currentPage] * scope[maxItems].value
)
}
}

View file

@ -0,0 +1,21 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function ItemsPerPageOptionsServiceFactory() {
const service = [
{name: '1', value: 1}
, {name: '5', value: 5}
, {name: '10', value: 10}
, {name: '20', value: 20}
, {name: '50', value: 50}
, {name: '100', value: 100}
, {name: '200', value: 200}
, {name: '500', value: 500}
, {name: '1000', value: 1000}
, {name: '*', value: 0}
]
return service
}

View file

@ -0,0 +1,4 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/

View file

@ -0,0 +1,34 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.input-group(style='margin-right: 5px; {{itemsSearchStyle}}' class='{{itemsSearchStyle}}')
.input-group-addon.input-sm
i.glyphicon.glyphicon-search(
class='{{iconStyle}}'
uib-tooltip='{{tooltipLabel}}'
tooltip-placement='auto top-right'
tooltip-popup-delay='500')
input.form-control.input-sm(type='text' placeholder='Search' ng-model='itemsSearch')
select.custon-select.form-control.input-sm(
ng-model='itemsPerPage'
ng-options='option as option.name for option in itemsPerPageOptions track by option.value')
uib-pagination(
style='vertical-align: middle; width: -moz-max-content'
total-items='totalItems'
items-per-page='itemsPerPage.value'
class='pagination-sm'
max-size='1'
boundary-links='true'
boundary-link-numbers='false'
previous-text='<' next-text='>' first-text='First' last-text='Last'
rotate='true'
ng-model='currentPage')
button.btn.btn-sm.btn-info(
type='button'
class='{{totalItemsStyle}}'
style='pointer-events: none')
span {{totalItems}}

View file

@ -1,6 +1,11 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var oboe = require('oboe')
var _ = require('lodash')
var EventEmitter = require('eventemitter3')
let Promise = require('bluebird')
module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceService) {
var deviceService = {}
@ -93,6 +98,12 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi
if (index >= 0) {
devices.splice(index, 1)
delete devicesBySerial[data.serial]
for (var serial in devicesBySerial) {
if (devicesBySerial[serial] > index) {
devicesBySerial[serial]--
}
}
sync(data)
this.emit('remove', data)
}
}.bind(this)
@ -131,6 +142,8 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi
}
notify(event)
}
/** code removed to avoid to show forbidden devices in user view!
else {
if (options.filter(event.data)) {
insert(event.data)
@ -139,6 +152,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi
notify(event)
}
}
**/
}
scopedSocket.on('device.add', addListener)
@ -153,6 +167,43 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi
}
this.devices = devices
function addGroupDevicesListener(event) {
return Promise.map(event.devices, function(serial) {
return deviceService.load(serial).then(function(device) {
return device
})
})
.then(function(_devices) {
_devices.forEach(function(device) {
if (device && typeof devicesBySerial[device.serial] === 'undefined') {
insert(device)
notify(event)
}
})
})
}
function removeGroupDevicesListener(event) {
event.devices.forEach(function(serial) {
if (typeof devicesBySerial[serial] !== 'undefined') {
remove(devices[devicesBySerial[serial]])
notify(event)
}
})
}
function updateGroupDeviceListener(event) {
let device = get(event.data)
if (device) {
modify(device, event.data)
notify(event)
}
}
scopedSocket.on('device.addGroupDevices', addGroupDevicesListener)
scopedSocket.on('device.removeGroupDevices', removeGroupDevicesListener)
scopedSocket.on('device.updateGroupDevice', updateGroupDeviceListener)
}
Tracker.prototype = new EventEmitter()

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function EnhanceDeviceServiceFactory($filter, AppState) {
var service = {}
@ -62,6 +66,8 @@ module.exports = function EnhanceDeviceServiceFactory($filter, AppState) {
device.enhancedUserProfileUrl = enhanceUserProfileUrl(device.owner.email)
device.enhancedUserName = device.owner.name || 'No name'
}
device.enhancedGroupOwnerProfileUrl = enhanceUserProfileUrl(device.group.owner.email)
}
function enhanceUserProfileUrl(email) {

View file

@ -0,0 +1,107 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const oboe = require('oboe')
module.exports = function DevicesServiceFactory(
$rootScope
, $http
, socket
, CommonService
) {
const DevicesService = {}
function buildQueryParameters(filters) {
var query = ''
if (filters.present !== 'Any') {
query += 'present=' + filters.present.toLowerCase()
}
if (filters.booked !== 'Any') {
query += (query === '' ? '' : '&') + 'booked=' + filters.booked.toLowerCase()
}
if (filters.annotated !== 'Any') {
query += (query === '' ? '' : '&') + 'annotated=' + filters.annotated.toLowerCase()
}
if (filters.controlled !== 'Any') {
query += (query === '' ? '' : '&') + 'controlled=' + filters.controlled.toLowerCase()
}
return query === '' ? query : '?' + query
}
DevicesService.getOboeDevices = function(target, fields, addDevice) {
return oboe(CommonService.getBaseUrl()
+ '/api/v1/devices?target=' + target + '&fields=' + fields)
.node('devices[*]', function(device) {
addDevice(device)
})
}
DevicesService.getDevices = function(target, fields) {
return $http.get('/api/v1/devices?target=' + target + '&fields=' + fields)
}
DevicesService.getDevice = function(serial, fields) {
return $http.get('/api/v1/devices/' + serial + '?fields=' + fields)
}
DevicesService.removeDevice = function(serial, filters) {
return $http.delete('/api/v1/devices/' + serial + buildQueryParameters(filters))
}
DevicesService.removeDevices = function(filters, serials) {
return $http({
method: 'DELETE',
url: '/api/v1/devices' + buildQueryParameters(filters),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials})
})
}
DevicesService.addOriginGroupDevice = function(id, serial) {
return $http.put('/api/v1/devices/' + serial + '/groups/' + id)
}
DevicesService.addOriginGroupDevices = function(id, serials) {
return $http({
method: 'PUT',
url: '/api/v1/devices/groups/' + id + '?fields=""',
data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials})
})
}
DevicesService.removeOriginGroupDevice = function(id, serial) {
return $http.delete('/api/v1/devices/' + serial + '/groups/' + id)
}
DevicesService.removeOriginGroupDevices = function(id, serials) {
return $http({
method: 'DELETE',
url: '/api/v1/devices/groups/' + id + '?fields=""',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials})
})
}
socket.on('user.settings.devices.created', function(device) {
$rootScope.$broadcast('user.settings.devices.created', device)
$rootScope.$apply()
})
socket.on('user.settings.devices.deleted', function(device) {
$rootScope.$broadcast('user.settings.devices.deleted', device)
$rootScope.$apply()
})
socket.on('user.settings.devices.updated', function(device) {
$rootScope.$broadcast('user.settings.devices.updated', device)
$rootScope.$apply()
})
return DevicesService
}

View file

@ -0,0 +1,9 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.devices', [
require('stf/util/common').name,
require('stf/socket').name
])
.factory('DevicesService', require('./devices-service'))

View file

@ -0,0 +1,184 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const oboe = require('oboe')
module.exports = function GroupsServiceFactory(
$rootScope
, $http
, socket
, CommonService
) {
const GroupsService = {}
GroupsService.getGroupUsers = function(id, fields) {
return $http.get('/api/v1/groups/' + id + '/users?fields=' + fields)
}
GroupsService.getOboeGroupUsers = function(id, fields, addGroupUser) {
return oboe(CommonService.getBaseUrl()
+ '/api/v1/groups/' + id + '/users?fields=' + fields)
.node('users[*]', function(user) {
addGroupUser(user)
})
}
GroupsService.getGroupDevices = function(id, bookable, fields) {
return $http.get('/api/v1/groups/' + id + '/devices?bookable=' + bookable + '&fields=' + fields)
}
GroupsService.getOboeGroupDevices = function(id, bookable, fields, addGroupDevice) {
return oboe(CommonService.getBaseUrl()
+ '/api/v1/groups/' + id + '/devices?bookable=' + bookable + '&fields=' + fields)
.node('devices[*]', function(device) {
addGroupDevice(device)
})
}
GroupsService.getGroupDevice = function(id, serial, fields) {
return $http.get('/api/v1/groups/' + id + '/devices/' + serial + '?fields=' + fields)
}
GroupsService.addGroupDevice = function(id, serial) {
return $http.put('/api/v1/groups/' + id + '/devices/' + serial)
}
GroupsService.addGroupDevices = function(id, serials) {
return $http({
method: 'PUT',
url: '/api/v1/groups/' + id + '/devices',
data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials})
})
}
GroupsService.removeGroupDevice = function(id, serial) {
return $http.delete('/api/v1/groups/' + id + '/devices/' + serial)
}
GroupsService.removeGroupDevices = function(id, serials) {
return $http({
method: 'DELETE',
url: '/api/v1/groups/' + id + '/devices',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials})
})
}
GroupsService.addGroupUser = function(id, email) {
return $http.put('/api/v1/groups/' + id + '/users/' + email)
}
GroupsService.addGroupUsers = function(id, emails) {
return $http({
method: 'PUT',
url: '/api/v1/groups/' + id + '/users',
data: typeof emails === 'undefined' ? emails : JSON.stringify({emails: emails})
})
}
GroupsService.removeGroupUser = function(id, email) {
return $http.delete('/api/v1/groups/' + id + '/users/' + email)
}
GroupsService.removeGroupUsers = function(id, emails) {
return $http({
method: 'DELETE',
url: '/api/v1/groups/' + id + '/users',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: typeof emails === 'undefined' ? emails : JSON.stringify({emails: emails})
})
}
GroupsService.getOboeGroups = function(addGroup) {
return oboe(CommonService.getBaseUrl() + '/api/v1/groups')
.node('groups[*]', function(group) {
addGroup(group)
})
}
GroupsService.getGroups = function() {
return $http.get('/api/v1/groups')
}
GroupsService.getOboeMyGroups = function(addGroup) {
return oboe(CommonService.getBaseUrl() + '/api/v1/groups?owner=true')
.node('groups[*]', function(group) {
addGroup(group)
})
}
GroupsService.getMyGroups = function() {
return $http.get('/api/v1/groups?owner=true')
}
GroupsService.getGroup = function(id) {
return $http.get('/api/v1/groups/' + id)
}
GroupsService.removeGroup = function(id) {
return $http.delete('/api/v1/groups/' + id)
}
GroupsService.removeGroups = function(ids) {
return $http({
method: 'DELETE',
url: '/api/v1/groups?_=' + Date.now(),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: typeof ids === 'undefined' ? ids : JSON.stringify({ids: ids})
})
}
GroupsService.createGroup = function() {
return $http({
method: 'POST',
url: '/api/v1/groups',
data: JSON.stringify({'state': 'pending'})
})
}
GroupsService.updateGroup = function(id, data) {
return $http({
method: 'PUT',
url: '/api/v1/groups/' + id,
data: JSON.stringify(data)
})
}
socket.on('user.settings.groups.created', function(group) {
$rootScope.$broadcast('user.settings.groups.created', group)
$rootScope.$apply()
})
socket.on('user.settings.groups.deleted', function(group) {
$rootScope.$broadcast('user.settings.groups.deleted', group)
$rootScope.$apply()
})
socket.on('user.settings.groups.updated', function(group) {
$rootScope.$broadcast('user.settings.groups.updated', group)
$rootScope.$apply()
})
socket.on('user.view.groups.created', function(group) {
$rootScope.$broadcast('user.view.groups.created', group)
$rootScope.$apply()
})
socket.on('user.view.groups.deleted', function(group) {
$rootScope.$broadcast('user.view.groups.deleted', group)
$rootScope.$apply()
})
socket.on('user.view.groups.updated', function(group) {
$rootScope.$broadcast('user.view.groups.updated', group)
$rootScope.$apply()
})
return GroupsService
}

View file

@ -0,0 +1,8 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.groups', [
require('stf/util/common').name
])
.factory('GroupsService', require('./groups-service'))

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function AccessTokenServiceFactory(
$rootScope
, $http
@ -26,7 +30,7 @@ module.exports = function AccessTokenServiceFactory(
$rootScope.$apply()
})
socket.on('user.keys.accessToken.removed', function() {
socket.on('user.keys.accessToken.updated', function() {
$rootScope.$broadcast('user.keys.accessTokens.updated')
$rootScope.$apply()
})

View file

@ -1,4 +1,6 @@
module.exports = angular.module('stf/user', [
require('stf/socket').name,
require('stf/common-ui').name,
require('stf/app-state').name
])
.factory('UserService', require('./user-service'))

View file

@ -1,5 +1,10 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function UserServiceFactory(
$rootScope
, $http
, socket
, AppState
, AddAdbKeyModalService
@ -8,6 +13,10 @@ module.exports = function UserServiceFactory(
var user = UserService.currentUser = AppState.user
UserService.getUser = function() {
return $http.get('/api/v1/user')
}
UserService.getAdbKeys = function() {
return (user.adbKeys || (user.adbKeys = []))
}

View file

@ -0,0 +1,8 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.users', [
require('stf/util/common').name
])
.factory('UsersService', require('./users-service'))

View file

@ -0,0 +1,96 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const oboe = require('oboe')
module.exports = function UsersServiceFactory(
$rootScope
, $http
, socket
, CommonService
) {
const UsersService = {}
function buildQueryParameters(filters) {
var query = ''
if (filters.groupOwner !== 'Any') {
query += 'groupOwner=' + filters.groupOwner.toLowerCase()
}
return query === '' ? query : '?' + query
}
UsersService.getOboeUsers = function(fields, addUser) {
return oboe(CommonService.getBaseUrl() + '/api/v1/users?fields=' + fields)
.node('users[*]', function(user) {
addUser(user)
})
}
UsersService.getUsers = function(fields) {
return $http.get('/api/v1/users?fields=' + fields)
}
UsersService.getUser = function(email, fields) {
return $http.get('/api/v1/users/' + email + '?fields=' + fields)
}
UsersService.removeUser = function(email, filters) {
return $http.delete('/api/v1/users/' + email + buildQueryParameters(filters))
}
UsersService.removeUsers = function(filters, emails) {
return $http({
method: 'DELETE',
url: '/api/v1/users' + buildQueryParameters(filters),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: typeof emails === 'undefined' ? emails : JSON.stringify({emails: emails})
})
}
UsersService.updateUserGroupsQuotas = function(email, number, duration, repetitions) {
return $http.put(
'/api/v1/users/' + email +
'/groupsQuotas?number=' + number +
'&duration=' + duration +
'&repetitions=' + repetitions
)
}
UsersService.updateDefaultUserGroupsQuotas = function(number, duration, repetitions) {
return $http.put(
'/api/v1/users/groupsQuotas?number=' + number +
'&duration=' + duration +
'&repetitions=' + repetitions
)
}
UsersService.createUser = function(name, email) {
return $http.post('/api/v1/users/' + email + '?name=' + name)
}
socket.on('user.settings.users.created', function(user) {
$rootScope.$broadcast('user.settings.users.created', user)
$rootScope.$apply()
})
socket.on('user.settings.users.deleted', function(user) {
$rootScope.$broadcast('user.settings.users.deleted', user)
$rootScope.$apply()
})
socket.on('user.view.users.updated', function(user) {
$rootScope.$broadcast('user.view.users.updated', user)
$rootScope.$apply()
})
socket.on('user.settings.users.updated', function(user) {
$rootScope.$broadcast('user.settings.users.updated', user)
$rootScope.$apply()
})
return UsersService
}

View file

@ -0,0 +1,223 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const _ = require('lodash')
module.exports = function CommonServiceFactory(
$window,
$location,
GenericModalService
) {
const service = {}
const FIVE_MN = 300 * 1000
const ONE_HOUR = 3600 * 1000
const ONE_DAY = 24 * ONE_HOUR
const ONE_WEEK = 7 * ONE_DAY
const ONE_MONTH = 30 * ONE_DAY
const ONE_QUATER = 3 * ONE_MONTH
const ONE_HALF_YEAR = 6 * ONE_MONTH
const ONE_YEAR = 365 * ONE_DAY
function getClassOptionsField(id, field) {
for(var i in service.classOptions) {
if (service.classOptions[i].id === id) {
return service.classOptions[i][field]
}
}
return ''
}
service.classOptions = [
{name: 'Once', id: 'once', privilege: 'user', duration: Infinity},
{name: 'Hourly', id: 'hourly', privilege: 'user', duration: ONE_HOUR},
{name: 'Daily', id: 'daily', privilege: 'user', duration: ONE_DAY},
{name: 'Weekly', id: 'weekly', privilege: 'user', duration: ONE_WEEK},
{name: 'Monthly', id: 'monthly', privilege: 'user', duration: ONE_MONTH},
{name: 'Quaterly', id: 'quaterly', privilege: 'user', duration: ONE_QUATER},
{name: 'Halfyearly', id: 'halfyearly', privilege: 'user', duration: ONE_HALF_YEAR},
{name: 'Yearly', id: 'yearly', privilege: 'user', duration: ONE_YEAR},
{name: 'Debug', id: 'debug', privilege: 'admin', duration: FIVE_MN},
{name: 'Bookable', id: 'bookable', privilege: 'admin', duration: Infinity},
{name: 'Standard', id: 'standard', privilege: 'admin', duration: Infinity}
]
service.getClassName = function(id) {
return getClassOptionsField(id, 'name')
}
service.getClassDuration = function(id) {
return getClassOptionsField(id, 'duration')
}
service.getDuration = function(ms) {
if (ms < 1000) {
return '0s'
}
var s = Math.floor(ms / 1000)
var m = Math.floor(s / 60)
s %= 60
var h = Math.floor(m / 60)
m %= 60
var d = Math.floor(h / 24)
h %= 24
return (d === 0 ? '' : d + 'd') +
(h === 0 ? '' : (d === 0 ? '' : ' ') + h + 'h') +
(m === 0 ? '' : (h === 0 ? '' : ' ') + m + 'm') +
(s === 0 ? '' : (m === 0 ? '' : ' ') + s + 's')
}
service.errorWrapper = function(fn, args) {
return fn.apply(null, args).catch(function(error) {
return GenericModalService.open({
message: error.data ?
error.data.description :
error.status + ' ' + error.statusText
, type: 'Error'
, size: 'lg'
, cancel: false
})
.then(function() {
return error
})
})
}
service.getIndex = function(array, value, property) {
for(var i in array) {
if (array[i][property] === value) {
return i
}
}
return -1
}
service.merge = function(oldObject, newObject) {
var undefinedValue
return _.merge(oldObject, newObject, function(a, b) {
return _.isArray(b) ? b : undefinedValue
})
}
service.isAddable = function(object, timeStamp) {
return typeof object === 'undefined' ||
timeStamp >= object.timeStamp && object.index === -1
}
service.isExisting = function(object) {
return typeof object !== 'undefined' &&
object.index !== -1
}
service.isRemovable = function(object, timeStamp) {
return service.isExisting(object) &&
timeStamp >= object.timeStamp
}
service.add = function(array, objects, value, property, timeStamp) {
if (service.isAddable(objects[value[property]], timeStamp)) {
objects[value[property]] = {
index: array.push(value) - 1
, timeStamp: timeStamp
}
return array[objects[value[property]].index]
}
return null
}
service.update = function(array, objects, value, property, timeStamp, noAdding) {
if (service.isExisting(objects[value[property]])) {
service.merge(array[objects[value[property]].index], value)
objects[value[property]].timeStamp = timeStamp
return array[objects[value[property]].index]
}
else if (!noAdding) {
return service.add(array, objects, value, property, timeStamp)
}
return null
}
service.delete = function(array, objects, key, timeStamp) {
if (service.isRemovable(objects[key], timeStamp)) {
const index = objects[key].index
const value = array.splice(index, 1)[0]
objects[key].index = -1
objects[key].timeStamp = timeStamp
for (var k in objects) {
if (objects[k].index > index) {
objects[k].index--
}
}
return value
}
else if (typeof objects[key] === 'undefined') {
objects[key] = {
index: -1
, timeStamp: timeStamp
}
}
return null
}
service.sortBy = function(data, column) {
const index = service.getIndex(data.columns, column.name, 'name')
if (index !== data.sort.index) {
data.sort.reverse = false
column.sort = 'sort-asc'
data.columns[data.sort.index].sort = 'none'
data.sort.index = index
}
else {
data.sort.reverse = !data.sort.reverse
column.sort = column.sort === 'sort-asc' ? 'sort-desc' : 'sort-asc'
}
return service
}
service.isOriginGroup = function(_class) {
return _class === 'bookable' || _class === 'standard'
}
service.isNoRepetitionsGroup = function(_class) {
return service.isOriginGroup(_class) || _class === 'once'
}
service.url = function(url) {
const a = $window.document.createElement('a')
$window.document.body.appendChild(a)
a.href = url
a.click()
$window.document.body.removeChild(a)
return service
}
service.copyToClipboard = function(data) {
const input = $window.document.createElement('input')
$window.document.body.appendChild(input)
input.value = data
input.select()
$window.document.execCommand('copy')
$window.document.body.removeChild(input)
return service
}
service.getBaseUrl = function() {
return $location.protocol()
+ '://'
+ $location.host()
+ ':'
+ $location.port()
}
return service
}

View file

@ -0,0 +1,8 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.util.common', [
require('stf/common-ui').name
])
.factory('CommonService', require('./common-service'))

View file

@ -13,5 +13,4 @@ describe('StoreAccountCtrl', function() {
expect(1).toEqual(1)
}))
})

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports =
function ControlPanesController($scope, $http, gettext, $routeParams,
$timeout, $location, DeviceService, GroupService, ControlService,
@ -85,7 +89,9 @@ module.exports =
$scope.$watch('device.state', function(newValue, oldValue) {
if (newValue !== oldValue) {
if (oldValue === 'using') {
/*************** fix bug: it seems automation state was forgotten ? *************/
if (oldValue === 'using' || oldValue === 'automation') {
/******************************************************************************/
FatalMessageService.open($scope.device, false)
}
}

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var _ = require('lodash')
var filterOps = {
@ -18,7 +22,7 @@ var filterOps = {
}
}
module.exports = function DeviceColumnService($filter, gettext) {
module.exports = function DeviceColumnService($filter, gettext, SettingsService, AppState) {
// Definitions for all possible values.
return {
state: DeviceStatusCell({
@ -27,6 +31,52 @@ module.exports = function DeviceColumnService($filter, gettext) {
return $filter('translate')(device.enhancedStateAction)
}
})
, group: TextCell({
title: gettext('Group Name')
, value: function(device) {
return $filter('translate')(device.group.name)
}
})
, groupSchedule: TextCell({
title: gettext('Group Class')
, value: function(device) {
return $filter('translate')(device.group.class)
}
})
, groupOwner: LinkCell({
title: gettext('Group Owner')
, target: '_blank'
, value: function(device) {
return $filter('translate')(device.group.owner.name)
}
, link: function(device) {
return device.enhancedGroupOwnerProfileUrl
}
})
, groupEndTime: TextCell({
title: gettext('Group Expiration Date')
, value: function(device) {
return $filter('date')(device.group.lifeTime.stop, SettingsService.get('dateFormat'))
}
})
, groupStartTime: TextCell({
title: gettext('Group Starting Date')
, value: function(device) {
return $filter('date')(device.group.lifeTime.start, SettingsService.get('dateFormat'))
}
})
, groupRepetitions: TextCell({
title: gettext('Group Repetitions')
, value: function(device) {
return device.group.repetitions
}
})
, groupOrigin: TextCell({
title: gettext('Group Origin')
, value: function(device) {
return $filter('translate')(device.group.originName)
}
})
, model: DeviceModelCell({
title: gettext('Model')
, value: function(device) {
@ -38,7 +88,7 @@ module.exports = function DeviceColumnService($filter, gettext) {
, value: function(device) {
return device.name || device.model || device.serial
}
})
}, AppState.user.email)
, operator: TextCell({
title: gettext('Carrier')
, value: function(device) {
@ -179,6 +229,12 @@ module.exports = function DeviceColumnService($filter, gettext) {
return device.manufacturer || ''
}
})
, marketName: TextCell({
title: gettext('Market name')
, value: function(device) {
return device.marketName || ''
}
})
, sdk: NumberCell({
title: gettext('SDK')
, defaultOrder: 'desc'
@ -305,8 +361,10 @@ function zeroPadTwoDigit(digit) {
}
function compareIgnoreCase(a, b) {
var la = (a || '').toLowerCase()
var lb = (b || '').toLowerCase()
/***** fix bug: cast to String for Safari compatibility ****/
var la = (String(a) || '').toLowerCase()
var lb = (String(b) || '').toLowerCase()
/***********************************************************/
if (la === lb) {
return 0
}
@ -316,8 +374,10 @@ function compareIgnoreCase(a, b) {
}
function filterIgnoreCase(a, filterValue) {
var va = (a || '').toLowerCase()
var vb = filterValue.toLowerCase()
/***** fix bug: cast to String for Safari compatibility ****/
var va = (String(a) || '').toLowerCase()
var vb = String(filterValue).toLowerCase()
/***********************************************************/
return va.indexOf(vb) !== -1
}
@ -551,7 +611,7 @@ function DeviceModelCell(options) {
})
}
function DeviceNameCell(options) {
function DeviceNameCell(options, ownerEmail) {
return _.defaults(options, {
title: options.title
, defaultOrder: 'asc'
@ -566,11 +626,11 @@ function DeviceNameCell(options) {
var a = td.firstChild
var t = a.firstChild
if (device.using) {
if (device.using && device.owner.email === ownerEmail) {
a.className = 'device-product-name-using'
a.href = '#!/control/' + device.serial
}
else if (device.usable) {
else if (device.usable && !device.using) {
a.className = 'device-product-name-usable'
a.href = '#!/control/' + device.serial
}

View file

@ -1,4 +1,10 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = angular.module('stf.device-list.column', [
require('gettext').name
require('gettext').name,
require('stf/settings').name,
require('stf/app-state').name
])
.service('DeviceColumnService', require('./device-column-service'))

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
var QueryParser = require('./util/query-parser')
module.exports = function DeviceListCtrl(
@ -55,6 +59,10 @@ module.exports = function DeviceListCtrl(
name: 'manufacturer'
, selected: false
}
, {
name: 'marketName'
, selected: false
}
, {
name: 'sdk'
, selected: false
@ -123,6 +131,34 @@ module.exports = function DeviceListCtrl(
name: 'owner'
, selected: true
}
, {
name: 'group'
, selected: false
}
, {
name: 'groupSchedule'
, selected: false
}
, {
name: 'groupStartTime'
, selected: false
}
, {
name: 'groupEndTime'
, selected: false
}
, {
name: 'groupRepetitions'
, selected: false
}
, {
name: 'groupOwner'
, selected: false
}
, {
name: 'groupOrigin'
, selected: false
}
]
$scope.columns = defaultColumns

View file

@ -96,7 +96,6 @@ module.exports = function DeviceListIconsDirective(
a.removeAttribute('href')
li.classList.add('device-is-busy')
}
return li
}
}
@ -169,8 +168,7 @@ module.exports = function DeviceListIconsDirective(
}
if (device.using) {
if (e.target.classList.contains('btn') &&
e.target.classList.contains('state-using')) {
if (e.target.classList.contains('btn') && e.target.classList.contains('state-using')) {
kickDevice(device)
e.preventDefault()
}

View file

@ -1,3 +1,7 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function DeviceListStatsDirective(
UserService
) {
@ -73,8 +77,11 @@ module.exports = function DeviceListStatsDirective(
var newStats = updateStats(device)
scope.counter.total -= 1
scope.counter.busy += newStats.busy - oldStats.busy
scope.counter.using += newStats.using - oldStats.using
scope.counter.usable -= newStats.usable
scope.counter.busy -= newStats.busy
scope.counter.using -= newStats.using
//scope.counter.busy += newStats.busy - oldStats.busy
//scope.counter.using += newStats.using - oldStats.using
delete mapping[device.serial]

View file

@ -0,0 +1,464 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const _ = require('lodash')
module.exports = function GroupListCtrl(
$scope
, $filter
, GroupsService
, UserService
, UsersService
, DevicesService
, SettingsService
, ItemsPerPageOptionsService
, CommonService
) {
const users = []
const usersByEmail = {}
const devices = []
const devicesBySerial = {}
const groupsById = {}
const groupsEnv = {}
const groupUserToAdd = {}
const userFields =
'email,' +
'name,' +
'privilege'
const deviceFields =
'serial,' +
'version,' +
'manufacturer,' +
'marketName,' +
'sdk,' +
'display.width,' +
'display.height,' +
'model'
function incrStateStats(group, incr) {
if (group.isActive) {
$scope.activeGroups += incr
}
else if (group.state === 'pending') {
$scope.pendingGroups += incr
}
$scope.readyGroups = $scope.groups.length - $scope.activeGroups - $scope.pendingGroups
}
function updateStateStats(oldGroup, newGroup) {
if (oldGroup === null) {
incrStateStats(newGroup, 1)
}
else if (newGroup === null) {
incrStateStats(oldGroup, -1)
}
else {
if (newGroup.isActive && !oldGroup.isActive) {
incrStateStats(newGroup, 1)
}
else if (!newGroup.isActive && oldGroup.isActive) {
incrStateStats(oldGroup, -1)
}
else if (newGroup.state === 'ready' && oldGroup.state === 'pending') {
incrStateStats(oldGroup, -1)
}
}
}
function updateGroupExtraProperties(group) {
const status = {pending: 'Pending', waiting: 'Waiting', ready: 'Ready'}
group.status = group.isActive ? 'Active' : status[group.state]
group.startTime = $filter('date')(group.dates[0].start, SettingsService.get('dateFormat'))
group.stopTime = $filter('date')(group.dates[0].stop, SettingsService.get('dateFormat'))
}
function updateQuotaBar(bar, consumed, allocated) {
bar.value = (consumed / allocated) * 100 | 0
if (bar.value < 25) {
bar.type = 'success'
}
else if (bar.value < 50) {
bar.type = 'info'
}
else if (bar.value < 75) {
bar.type = 'warning'
}
else {
bar.type = 'danger'
}
}
function updateQuotaBars() {
updateQuotaBar(
$scope.numberBar
, $scope.user.groups.quotas.consumed.number
, $scope.user.groups.quotas.allocated.number
)
updateQuotaBar(
$scope.durationBar
, $scope.user.groups.quotas.consumed.duration
, $scope.user.groups.quotas.allocated.duration
)
}
function addGroup(group, timeStamp) {
if (CommonService.add(
$scope.groups
, groupsById
, group
, 'id'
, timeStamp)) {
$scope.groupsEnv[group.id] = {
devices: []
, users: []
}
groupsEnv[group.id] = {
devicesBySerial: {}
, usersByEmail: {}
}
updateStateStats(null, group)
updateGroupExtraProperties(group)
return group
}
return null
}
function updateGroup(group, timeStamp) {
return CommonService.update(
$scope.groups
, groupsById
, group
, 'id'
, timeStamp)
}
function deleteGroup(id, timeStamp) {
const group = CommonService.delete(
$scope.groups
, groupsById
, id
, timeStamp)
if (group) {
updateStateStats(group, null)
delete $scope.groupsEnv[group.id]
delete groupsEnv[group.id]
}
return group
}
function addUser(user, timeStamp) {
if (CommonService.add(
users
, usersByEmail
, user
, 'email'
, timeStamp
) && typeof groupUserToAdd[user.email] !== 'undefined') {
addGroupUser(
groupUserToAdd[user.email].id
, user.email
, groupUserToAdd[user.email].timeStamp)
delete groupUserToAdd[user.email]
}
}
function deleteUser(email, timeStamp) {
return CommonService.delete(
users
, usersByEmail
, email
, timeStamp)
}
function addDevice(device, timeStamp) {
return CommonService.add(
devices
, devicesBySerial
, device
, 'serial'
, timeStamp)
}
function updateDevice(device, timeStamp) {
return CommonService.update(
devices
, devicesBySerial
, device
, 'serial'
, timeStamp)
}
function deleteDevice(serial, timeStamp) {
return CommonService.delete(
devices
, devicesBySerial
, serial
, timeStamp)
}
function addGroupUser(id, email, timeStamp) {
if (CommonService.isExisting(usersByEmail[email])) {
CommonService.add(
$scope.groupsEnv[id].users
, groupsEnv[id].usersByEmail
, users[usersByEmail[email].index]
, 'email'
, timeStamp)
}
else {
groupUserToAdd[email] = {id: id, timeStamp: timeStamp}
}
}
function deleteGroupUser(id, email, timeStamp) {
CommonService.delete(
$scope.groupsEnv[id].users
, groupsEnv[id].usersByEmail
, email
, timeStamp)
}
function addGroupDevice(id, serial, timeStamp) {
if (CommonService.isExisting(devicesBySerial[serial])) {
CommonService.add(
$scope.groupsEnv[id].devices
, groupsEnv[id].devicesBySerial
, devices[devicesBySerial[serial].index]
, 'serial'
, timeStamp)
}
else {
GroupsService.getGroupDevice(id, serial, deviceFields)
.then(function(response) {
if (addDevice(response.data.device, timeStamp)) {
CommonService.add(
$scope.groupsEnv[id].devices
, groupsEnv[id].devicesBySerial
, devices[devicesBySerial[serial].index]
, 'serial'
, timeStamp)
}
})
}
}
function deleteGroupDevice(id, serial, timeStamp) {
CommonService.delete(
$scope.groupsEnv[id].devices
, groupsEnv[id].devicesBySerial
, serial
, timeStamp)
}
function updateGroupDevices(group, isAddedDevice, devices, timeStamp) {
if (devices.length) {
if (isAddedDevice) {
devices.forEach(function(serial) {
addGroupDevice(group.id, serial, timeStamp)
})
}
else {
devices.forEach(function(serial) {
deleteGroupDevice(group.id, serial, timeStamp)
})
}
}
}
function updateGroupUsers(group, isAddedUser, users, timeStamp) {
if (users.length) {
if (isAddedUser) {
users.forEach(function(email) {
addGroupUser(group.id, email, timeStamp)
})
}
else {
users.forEach(function(email) {
deleteGroupUser(group.id, email, timeStamp)
})
}
}
}
function initScope() {
GroupsService.getOboeGroups(function(group) {
addGroup(group, -1)
})
.done(function() {
$scope.$digest()
})
UserService.getUser().then(function(response) {
$scope.user = response.data.user
updateQuotaBars()
})
UsersService.getOboeUsers(userFields, function(user) {
addUser(user, -1)
})
}
$scope.scopeGroupListCtrl = $scope
$scope.sortBy = CommonService.sortBy
$scope.getDuration = CommonService.getDuration
$scope.getClassName = CommonService.getClassName
$scope.user = UserService.currentUser
$scope.numberBar = {}
$scope.durationBar = {}
$scope.groupsEnv = {}
$scope.groups = []
$scope.activeGroups = $scope.readyGroups = $scope.pendingGroups = 0
$scope.itemsPerPageOptions = ItemsPerPageOptionsService
SettingsService.bind($scope, {
target: 'groupItemsPerPage'
, source: 'groupViewItemsPerPage'
, defaultValue: $scope.itemsPerPageOptions[2]
})
$scope.groupColumns = [
{name: 'Status', property: 'status'}
, {name: 'Name', property: 'name'}
, {name: 'Identifier', property: 'id'}
, {name: 'Owner', property: 'owner.name'}
, {name: 'Devices', property: 'devices.length'}
, {name: 'Users', property: 'users.length'}
, {name: 'Class', property: 'class'}
, {name: 'Repetitions', property: 'repetitions'}
, {name: 'Duration', property: 'duration'}
, {name: 'Starting Date', property: 'startTime'}
, {name: 'Expiration Date', property: 'stopTime'}
]
$scope.defaultGroupData = {
columns: [
{name: 'Status', selected: true, sort: 'none'}
, {name: 'Name', selected: true, sort: 'sort-asc'}
, {name: 'Identifier', selected: false, sort: 'none'}
, {name: 'Owner', selected: true, sort: 'none'}
, {name: 'Devices', selected: true, sort: 'none'}
, {name: 'Users', selected: true, sort: 'none'}
, {name: 'Class', selected: true, sort: 'none'}
, {name: 'Repetitions', selected: true, sort: 'none'}
, {name: 'Duration', selected: true, sort: 'none'}
, {name: 'Starting Date', selected: true, sort: 'none'}
, {name: 'Expiration Date', selected: true, sort: 'none'}
]
, sort: {index: 1, reverse: false}
}
SettingsService.bind($scope, {
target: 'groupData'
, source: 'groupData'
, defaultValue: $scope.defaultGroupData
})
$scope.mailToGroupOwners = function(groups) {
CommonService.copyToClipboard(_.uniq(groups.map(function(group) {
return group.owner.email
}))
.join(SettingsService.get('emailSeparator')))
.url('mailto:?body=*** Paste the email addresses from the clipboard! ***')
}
$scope.mailToGroupUsers = function(group, users) {
// group unused actually..
CommonService.copyToClipboard(users.map(function(user) {
return user.email
})
.join(SettingsService.get('emailSeparator')))
.url('mailto:?body=*** Paste the email addresses from the clipboard! ***')
}
$scope.getTooltip = function(objects) {
var tooltip = ''
objects.forEach(function(object) {
tooltip += object + '\n'
})
return tooltip
}
$scope.resetData = function() {
$scope.groupData = JSON.parse(JSON.stringify($scope.defaultGroupData))
}
$scope.initGroupUsers = function(group) {
if (typeof $scope.groupsEnv[group.id].userCurrentPage === 'undefined') {
$scope.groupsEnv[group.id].userCurrentPage = 1
$scope.groupsEnv[group.id].userItemsPerPage = $scope.itemsPerPageOptions[1]
}
group.users.forEach(function(email) {
addGroupUser(group.id, email, -1)
})
}
$scope.initGroupDevices = function(group) {
if (typeof $scope.groupsEnv[group.id].deviceCurrentPage === 'undefined') {
$scope.groupsEnv[group.id].deviceCurrentPage = 1
$scope.groupsEnv[group.id].deviceItemsPerPage = $scope.itemsPerPageOptions[1]
}
GroupsService.getOboeGroupDevices(group.id, false, deviceFields, function(device) {
addDevice(device, -1)
addGroupDevice(group.id, device.serial, -1)
})
.done(function() {
$scope.$digest()
})
}
$scope.$on('user.view.groups.created', function(event, message) {
addGroup(message.group, message.timeStamp)
})
$scope.$on('user.view.groups.deleted', function(event, message) {
deleteGroup(message.group.id, message.timeStamp)
})
$scope.$on('user.view.groups.updated', function(event, message) {
if (CommonService.isExisting(groupsById[message.group.id])) {
if (message.group.users.indexOf(UserService.currentUser.email) < 0) {
deleteGroup(message.group.id, message.timeStamp)
}
else {
updateStateStats($scope.groups[groupsById[message.group.id].index], message.group)
updateGroupDevices(message.group, message.isAddedDevice, message.devices, message.timeStamp)
updateGroupUsers(message.group, message.isAddedUser, message.users, message.timeStamp)
updateGroup(message.group, message.timeStamp)
updateGroupExtraProperties($scope.groups[groupsById[message.group.id].index])
}
}
else {
addGroup(message.group, message.timeStamp)
}
})
$scope.$on('user.settings.users.created', function(event, message) {
addUser(message.user, message.timeStamp)
})
$scope.$on('user.settings.users.deleted', function(event, message) {
deleteUser(message.user.email, message.timeStamp)
})
$scope.$on('user.view.users.updated', function(event, message) {
if (message.user.email === $scope.user.email) {
$scope.user = message.user
updateQuotaBars()
}
})
$scope.$on('user.settings.devices.created', function(event, message) {
addDevice(message.device, message.timeStamp)
})
$scope.$on('user.settings.devices.deleted', function(event, message) {
deleteDevice(message.device.serial, message.timeStamp)
})
$scope.$on('user.settings.devices.updated', function(event, message) {
updateDevice(message.device, message.timeStamp)
})
initScope()
}

View file

@ -0,0 +1,196 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
.stf-pager-group-devices-search {
width: 160px;
}
.stf-pager-group-list-total-items {
margin-top: 5px;
}
.stf-group-list .selectable {
user-select: text;
}
.stf-group-list .group-list .stf-pager-group-devices-search i.stf-pager-group-devices-search-icon {
font-size: 12px;
margin-right: 0px;
}
.stf-group-list .group-list {
min-height: 600px;
}
.stf-group-list .group-list-header {
margin: 20px 0px 20px 15px;
}
.stf-group-list .btn-devices, .btn-users {
padding: 0px;
margin: 0px;
}
.stf-group-list .group-devices {
width: auto;
min-width: 475px;
max-width: 600px;
}
.stf-group-list .group-users {
width: auto;
min-width: 475px;
max-width: 600px;
}
.stf-group-list .group-icon {
font-size: 15px;
vertical-align: middle;
margin-right: 10px;
}
.stf-group-list .group-device-icon {
font-size: 25px;
vertical-align: middle;
margin-right: 10px;
}
.stf-group-list .group-user-icon {
font-size: 20px;
vertical-align: middle;
margin-right: 10px;
}
.stf-group-list .group-device-details, .group-user-details {
display: inline-block;
line-height: 2;
margin-left: 10px;
}
.stf-group-list .group-device-name, .group-user-name {
color: #007aff;
font-size: 14px;
font-weight: 300;
margin: 2px 0 6px;
}
.stf-group-list .group-device-id, .group-user-id {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
font-size: 10px;
margin-bottom: 2px;
color: #999999;
font-weight: 300;
}
.stf-group-list td,span {
white-space: nowrap;
}
.stf-group-list .user-line, .device-line {
padding: 9px 15px 9px 2px;
margin-left: 14px;
border-bottom: 1px solid #dddddd;
}
.stf-group-list .mailto {
padding-bottom: 1px;
margin-top: 5px;
}
.stf-group-list i.mailto {
vertical-align: initial;
}
.group-stats {
min-height: 100px;
height: 100px;
text-align: center;
}
.group-stats [class^="col-"],
.group-stats [class*="col-"] {
height: 100%;
margin-bottom: 0;
}
.group-stats [class^="col-"]:last-child,
.group-stats [class*="col-"]:last-child {
border: 0;
}
.group-stats [class^="col-"] .number,
.group-stats [class*="col-"] .number {
font-size: 3.4em;
font-weight: 100;
line-height: 1.5em;
letter-spacing: -0.06em;
}
.group-stats [class^="col-"] .number .icon,
.group-stats [class*="col-"] .number .icon {
width: 50px;
height: 38px;
display: inline-block;
vertical-align: top;
margin: 20px 12px 0 0;
}
.group-quota-stats {
min-height: 75px;
height: 75px;
text-align: center;
}
.group-quota-stats .bar {
height: 20px;
vertical-align: top;
margin: 14px 12px 12px 12px;
}
.group-quota-stats .text,
.group-stats [class^="col-"] .text,
.group-stats [class*="col-"] .text {
font-weight: 300;
color: #aeaeae;
text-transform: uppercase;
font-size: 12px;
}
.group-stats .fa {
font-size: 0.8em;
}
@media (max-width: 600px) {
.group-stats {
min-height: 60px;
height: 60px;
text-align: center;
}
.group-stats .fa {
font-size: 0.6em;
}
.group-stats [class^="col-"] .number,
.group-stats [class*="col-"] .number {
font-size: 1.8em;
line-height: normal;
font-weight: 300;
}
.group-stats [class^="col-"] .number .icon,
.group-stats [class*="col-"] .number .icon {
width: 25px;
height: 19px;
margin: 10px 6px 0 0;
}
.group-stats [class^="col-"] .text,
.group-stats [class*="col-"] .text {
font-size: 0.8em;
font-weight: 500;
}
}

View file

@ -0,0 +1,14 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.stf-group-list
.row.unselectable
.col-md-12
div(ng-include="'group-list/stats/group-stats.pug'")
.row.unselectable
.col-md-12
div(ng-include="'group-list/stats/group-quota-stats.pug'")
.row.unselectable
.col-md-12
div(ng-include="'group-list/groups/groups.pug'")

View file

@ -0,0 +1,167 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.widget-container.fluid-height.overflow-auto.group-list
.heading
nothing-to-show(
icon='fa-object-group'
message='{{"No Groups" | translate}}' ng-if='!groups.length')
div(ng-if='groups.length')
form.form-inline
.form-group.group-list-header
stf-pager(
tooltip-label="{{'Group selection' | translate}}"
total-items='filteredGroups.length'
total-items-style='stf-pager-group-list-total-items'
items-per-page='scopeGroupListCtrl.groupItemsPerPage'
items-per-page-options='itemsPerPageOptions'
current-page='scopeGroupListCtrl.groupCurrentPage'
items-search='search')
.form-group.group-list-header
stf-column-choice(reset-data='resetData()' column-data='groupData.columns')
button.btn.btn-xs.btn-primary-outline.pull-right(
type='button'
uib-tooltip="{{'Write an email to the group owner selection' | translate}}"
ng-disabled='!filteredGroups.length'
ng-click='mailToGroupOwners(filteredGroups)'
tooltip-placement='top'
tooltip-popup-delay='500')
i.fa.fa-envelope-o
span(translate) Contact Owners
table.table.table-hover.dataTable.ng-table
thead
tr
th.header.sortable(
ng-class='[column.sort]'
ng-repeat='column in groupData.columns | filter: {selected: true}'
ng-click='sortBy(groupData, column)')
div.strong(ng-bind-template='{{::column.name | translate}}')
tbody
tr(ng-repeat="group in groups \
| filter:search \
| orderBy:groupColumns[groupData.sort.index].property:groupData.sort.reverse \
| pagedObjectsFilter:scopeGroupListCtrl:'groupCurrentPage':'groupItemsPerPage':'filteredGroups' \
track by group.id")
td(ng-if='groupData.columns[0].selected'
ng-class="{'color-green': group.status === 'Active', \
'color-red': group.status === 'Pending', \
'color-orange': group.status === 'Ready'}") {{group.status | translate}}
td.selectable(ng-if='groupData.columns[1].selected')
i.fa.fa-object-group.group-icon
span {{group.name}}
td.selectable(ng-if='groupData.columns[2].selected') {{::group.id}}
td(ng-if='groupData.columns[3].selected')
a(ng-href="{{::'mailto:' + group.owner.email}}") {{::group.owner.name}}
td(ng-if='groupData.columns[4].selected')
.btn-group.btn-devices(uib-dropdown auto-close='outsideClick')
button.btn.btn-sm.btn-primary-outline.btn-devices(
type='button'
ng-disabled='!group.devices.length'
ng-click='initGroupDevices(group)'
uib-dropdown-toggle)
span {{group.devices.length}}
ul.dropdown-menu.group-devices(
ng-if='groupsEnv[group.id].deviceCurrentPage && groupsEnv[group.id].devices.length'
uib-dropdown-menu role='menu' ng-click='$event.stopPropagation()')
li
a
form.form-inline
.form-group
stf-pager(
items-search-style='stf-pager-group-devices-search'
icon-style='stf-pager-group-devices-search-icon'
tooltip-label="{{'Device selection' | translate}}"
total-items='groupsEnv[group.id].filteredDevices.length'
items-per-page='groupsEnv[group.id].deviceItemsPerPage'
items-per-page-options='itemsPerPageOptions'
current-page='groupsEnv[group.id].deviceCurrentPage'
items-search='deviceSearch')
li(ng-repeat="device in groupsEnv[group.id].devices \
| filter:deviceSearch \
| orderBy: 'model' \
| pagedObjectsFilter:groupsEnv[group.id]:'deviceCurrentPage':'deviceItemsPerPage':'filteredDevices' \
track by device.serial")
.device-line
i.fa.fa-mobile.group-device-icon
.group-device-details.selectable
a.group-device-name(ng-bind-template="{{device.manufacturer + ' ' + device.model + ' (' + device.marketName + ')'}}")
.group-device-id
span(translate) Serial
span(ng-bind-template="{{::': ' + device.serial + ' - '}}")
span(translate) OS
span(ng-bind-template="{{': ' + device.version + ' - '}}")
span(translate) Screen
span(ng-bind-template="{{': ' + device.display.width + 'x' + device.display.height + ' - '}}")
span(translate) SDK
span(ng-bind-template="{{': ' + device.sdk}}")
td(ng-if='groupData.columns[5].selected')
.btn-group.btn-users(uib-dropdown auto-close='outsideClick')
button.btn.btn-sm.btn-primary-outline.btn-users(
type='button'
ng-disabled='!group.users.length'
ng-click='initGroupUsers(group)'
uib-dropdown-toggle)
span {{group.users.length}}
ul.dropdown-menu.group-users(
ng-if='groupsEnv[group.id].userCurrentPage && groupsEnv[group.id].users'
uib-dropdown-menu role='menu' ng-click='$event.stopPropagation()')
li
.user-line
form
.form-group.mailto
button.btn.btn-xs.btn-primary-outline.pull-right(
type='button'
uib-tooltip="{{'Write an email to the group user selection' | translate}}"
ng-disabled='!groupsEnv[group.id].filteredUsers.length'
ng-click='mailToGroupUsers(group, groupsEnv[group.id].filteredUsers)'
tooltip-placement='top'
tooltip-popup-delay='500')
i.fa.fa-envelope-o
span(translate) Contact Users
form.form-inline
.form-group
stf-pager(
items-search-style='stf-pager-group-devices-search'
icon-style='stf-pager-group-devices-search-icon'
tooltip-label="{{'User selection' | translate}}"
total-items='groupsEnv[group.id].filteredUsers.length'
items-per-page='groupsEnv[group.id].userItemsPerPage'
items-per-page-options='itemsPerPageOptions'
current-page='groupsEnv[group.id].userCurrentPage'
items-search='userSearch')
li(ng-repeat="user in groupsEnv[group.id].users \
| filter:userSearch \
| orderBy: 'name' \
| pagedObjectsFilter:groupsEnv[group.id]:'userCurrentPage':'userItemsPerPage':'filteredUsers' \
track by user.email")
.user-line
i.fa.fa-user.group-user-icon
.group-user-details.selectable
a.group-user-name(
ng-href="{{::'mailto:' + user.email}}"
ng-bind-template="{{::user.name}}")
.group-user-id
span(translate) Email
span(ng-bind-template="{{::': ' + user.email + ' - '}}")
span(translate) Privilege
span(ng-bind-template="{{::': ' + user.privilege}}")
td(ng-if='groupData.columns[6].selected') {{getClassName(group.class) | translate}}
td(ng-if='groupData.columns[7].selected') {{group.repetitions}}
td(ng-if='groupData.columns[8].selected') {{getDuration(group.duration)}}
td(ng-if='groupData.columns[9].selected') {{group.startTime}}
td(ng-if='groupData.columns[10].selected') {{group.stopTime}}

View file

@ -0,0 +1,35 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
require('./group-list.css')
module.exports = angular.module('group-list', [
require('stf/column-choice').name,
require('stf/groups').name,
require('stf/user').name,
require('stf/users').name,
require('stf/devices').name,
require('stf/settings').name,
require('stf/util/common').name,
require('stf/common-ui').name
])
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/groups', {
template: require('./group-list.pug'),
controller: 'GroupListCtrl'
})
}])
.run(['$templateCache', function($templateCache) {
$templateCache.put(
'group-list/stats/group-stats.pug', require('./stats/group-stats.pug')
)
$templateCache.put(
'group-list/stats/group-quota-stats.pug', require('./stats/group-quota-stats.pug')
)
$templateCache.put(
'group-list/groups/groups.pug', require('./groups/groups.pug')
)
}])
.controller('GroupListCtrl', require('./group-list-controller'))

View file

@ -0,0 +1,14 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.widget-container.group-quota-stats
.col-xs-6
uib-progressbar.bar(class='progress-striped' value='numberBar.value' type='{{numberBar.type}}')
b {{numberBar.value}}%
.text(translate) {{user.name}} groups number use
.col-xs-6
uib-progressbar.bar(class='progress-striped' value='durationBar.value' type='{{durationBar.type}}')
b {{durationBar.value}}%
.text(translate) {{user.name}} groups duration use

View file

@ -0,0 +1,25 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.widget-container.group-stats
.col-xs-3
.number.color-blue
.icon.fa.fa-globe
span(ng-bind='groups.length')
.text(translate) Total groups
.col-xs-3
.number.color-green
.icon.fa.fa-play
span(ng-bind='activeGroups')
.text(translate) Active groups
.col-xs-3
.number.color-orange
.icon.fa.fa-pause
span(ng-bind='readyGroups')
.text(translate) Ready groups
.col-xs-3
.number.color-pink
.icon.fa.fa-stop
span(ng-bind='pendingGroups')
.text(translate) Pending groups

View file

@ -1,6 +1,14 @@
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
require('./menu.css')
require('angular-cookies')
module.exports = angular.module('stf.menu', [
'ngCookies',
require('stf/socket').name,
require('stf/util/common').name,
require('stf/nav-menu').name,
require('stf/settings').name,
require('stf/common-ui/modals/external-url-modal').name,

View file

@ -1,5 +1,18 @@
module.exports = function MenuCtrl($scope, $rootScope, SettingsService,
$location, LogcatService) {
/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
module.exports = function MenuCtrl(
$scope
, $rootScope
, SettingsService
, $location
, $http
, CommonService
, LogcatService
, socket
, $cookies
, $window) {
SettingsService.bind($scope, {
target: 'lastUsedDevice'
@ -15,4 +28,21 @@ module.exports = function MenuCtrl($scope, $rootScope, SettingsService,
$scope.isControlRoute = $location.path().search('/control') !== -1
})
$scope.mailToSupport = function() {
CommonService.url('mailto:' + $scope.contactEmail)
}
$http.get('/auth/contact').then(function(response) {
$scope.contactEmail = response.data.contact.email
})
$scope.logout = function() {
$cookies.remove('XSRF-TOKEN', {path: '/'})
$cookies.remove('ssid', {path: '/'})
$cookies.remove('ssid.sig', {path: '/'})
$window.location = '/'
setTimeout(function() {
socket.disconnect()
}, 100)
}
}

View file

@ -1,3 +1,7 @@
//
Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.navbar.stf-menu(ng-controller='MenuCtrl')
.container-fluid.stf-top-bar
a.stf-logo(ng-href="/#!/devices") STF
@ -9,6 +13,9 @@
a(ng-href='/#!/devices', accesskey='1')
span.fa.fa-sitemap
span(ng-if='!$root.basicMode', translate) Devices
a(ng-href='/#!/groups')
span.fa.fa-object-group
span(ng-if='!$root.basicMode', translate) Groups
a(ng-href='/#!/settings')
span.fa.fa-gears
span(ng-if='!$root.basicMode', translate) Settings
@ -18,7 +25,22 @@
button(type='button', ng-model='$root.platform', uib-btn-radio="'web'", translate).btn.btn-sm.btn-default-outline Web
button(type='button', ng-model='$root.platform', uib-btn-radio="'native'", translate).btn.btn-sm.btn-default-outline Native
li.stf-nav-web-native-button(ng-if='!$root.basicMode')
button.btn.btn-sm.btn-default-outline(
type='button'
ng-click='mailToSupport()')
i.fa.fa-envelope-o
span(translate) Contact Support
li.stf-nav-web-native-button(ng-if='!$root.basicMode')
button.btn.btn-sm.btn-default-outline(
type='button'
ng-click='logout()')
i.fa.fa-sign-out
span(translate) Logout
li(ng-show='!$root.basicMode')
a(ng-href='/#!/help', accesskey='6')
i.fa.fa-question-circle.fa-fw
| {{ "Help" | translate }}

View file

@ -0,0 +1,169 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
const _ = require('lodash')
module.exports = function DevicesCtrl(
$scope
, DevicesService
, SettingsService
, ItemsPerPageOptionsService
, GenericModalService
, CommonService
) {
const devicesBySerial = {}
const deviceFields =
'model,' +
'serial,' +
'version,' +
'display.height,' +
'display.width,' +
'manufacturer,' +
'sdk,' +
'abi,' +
'cpuPlatform,' +
'openGLESVersion,' +
'marketName,' +
'phone.imei,' +
'provider.name,' +
'group.originName'
function publishDevice(device) {
if (!device.model) {
device.display = {}
}
else {
device.displayStr = device.display.width + 'x' + device.display.height
}
for (var i in device) {
if (device[i] === null) {
device[i] = ''
}
}
return device
}
function addDevice(device, timeStamp) {
return CommonService.add(
$scope.devices
, devicesBySerial
, device
, 'serial'
, timeStamp)
}
function updateDevice(device, timeStamp) {
return CommonService.update(
$scope.devices
, devicesBySerial
, device
, 'serial'
, timeStamp)
}
function deleteDevice(serial, timeStamp) {
return CommonService.delete(
$scope.devices
, devicesBySerial
, serial
, timeStamp)
}
function initScope() {
DevicesService.getOboeDevices('user', deviceFields, function(device) {
addDevice(publishDevice(device), -1)
})
.done(function() {
$scope.$digest()
})
}
SettingsService.bind($scope, {
target: 'removingFilters'
, source: 'DevicesRemovingFilters'
, defaultValue: {
present: 'False'
, booked: 'False'
, annotated: 'False'
, controlled: 'False'
}
})
$scope.devices = []
$scope.confirmRemove = {value: true}
$scope.scopeDevicesCtrl = $scope
$scope.itemsPerPageOptions = ItemsPerPageOptionsService
SettingsService.bind($scope, {
target: 'deviceItemsPerPage'
, source: 'deviceItemsPerPage'
, defaultValue: $scope.itemsPerPageOptions[2]
})
$scope.removingFilterOptions = ['True', 'False', 'Any']
$scope.removeDevice = function(serial, askConfirmation) {
if (askConfirmation) {
GenericModalService.open({
message: 'Really delete this device?'
, type: 'Warning'
, size: 'sm'
, cancel: true
})
.then(function() {
CommonService.errorWrapper(
DevicesService.removeDevice
, [serial, $scope.removingFilters]
)
})
}
else {
CommonService.errorWrapper(
DevicesService.removeDevice
, [serial, $scope.removingFilters]
)
}
}
$scope.removeDevices = function(search, filteredDevices, askConfirmation) {
function removeDevices() {
CommonService.errorWrapper(
DevicesService.removeDevices
, search ?
[$scope.removingFilters, filteredDevices.map(function(device) {
return device.serial
})
.join()] :
[$scope.removingFilters]
)
}
if (askConfirmation) {
GenericModalService.open({
message: 'Really delete this selection of devices?'
, type: 'Warning'
, size: 'sm'
, cancel: true
})
.then(function() {
removeDevices()
})
}
else {
removeDevices()
}
}
$scope.$on('user.settings.devices.created', function(event, message) {
addDevice(publishDevice(message.device), message.timeStamp)
})
$scope.$on('user.settings.devices.deleted', function(event, message) {
deleteDevice(message.device.serial, message.timeStamp)
})
$scope.$on('user.settings.devices.updated', function(event, message) {
updateDevice(publishDevice(message.device), message.timeStamp)
})
initScope()
}

View file

@ -0,0 +1,21 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
describe('DevicesCtrl', function() {
beforeEach(angular.mock.module(require('./index').name))
var scope, ctrl
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new()
ctrl = $controller('DevicesCtrl', {$scope: scope})
}))
it('should ...', inject(function() {
expect(1).toEqual(1)
}))
})

View file

@ -0,0 +1,65 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
.stf-devices .selectable {
user-select: text;
}
.stf-pager-devices-total-items {
margin-top: 5px;
}
.stf-devices .device-header {
margin-left: 10px;
}
.stf-devices .heading .device-header-icon {
font-size: 16px;
}
.stf-devices .device-list-icon {
margin-right: 10px;
}
.stf-devices .device-filters-items {
margin-top: 5px;
}
.stf-devices .device-filters-item {
margin: 0px 10px 15px 15px;
}
.stf-devices .devices-list .device-line {
padding: 10px;
border-bottom: 1px solid #dddddd;
margin-left: 0px;
}
.stf-devices .devices-list .device-line.device-actions {
padding-bottom: 23px;
}
.stf-devices .device-list-details {
display: inline-block;
}
.stf-devices .device-list-label {
font-weight: bold;
margin-right: 10px;
}
.stf-devices .device-list-name {
color: #007aff;
font-size: 14px;
font-weight: 300;
margin: 2px 0 6px;
}
.stf-devices .device-list-id {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
font-size: 10px;
margin-bottom: 2px;
color: #999999;
font-weight: 300;
}

View file

@ -0,0 +1,137 @@
//
Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
//
.widget-container.fluid-height.stf-devices(ng-controller='DevicesCtrl')
.heading
i.fa.fa-mobile.device-header-icon
span(translate) Device list
a.pull-right.btn.btn-sm(ng-href='')
i.fa.fa-question-circle.fa-fw(uib-tooltip='{{"More about Devices" | translate}}' tooltip-placement='left')
.widget-content.padded
nothing-to-show(icon='fa-mobile' message='{{"No Devices" | translate}}' ng-if='!devices.length')
div(ng-if='devices.length')
ul.list-group.devices-list
li.list-group-item
.device-line.device-actions
form.form-inline.device-header
.form-group
stf-pager(
tooltip-label="{{'Device selection' | translate}}"
total-items='filteredDevices.length'
total-items-style='stf-pager-devices-total-items'
items-per-page='scopeDevicesCtrl.deviceItemsPerPage'
items-per-page-options='itemsPerPageOptions'
current-page='scopeDevicesCtrl.deviceCurrentPage'
items-search='search')
button.btn.btn-xs.btn-danger.pull-right(
type='button'
uib-tooltip="{{'Remove the device selection' | translate}}"
tooltip-placement='bottom'
tooltip-popup-delay='500'
ng-disabled='!filteredDevices.length'
ng-click='removeDevices(search, filteredDevices, confirmRemove.value)')
i.fa.fa-trash-o
span(translate) Remove
button.btn.btn-xs.btn-success.pull-right(
type='button'
uib-tooltip="{{'Enable/Disable confirmation for device removing' | translate}}"
tooltip-placement='top'
tooltip-popup-delay='500'
ng-click='confirmRemove.value = !confirmRemove.value'
ng-class='{"btn-warning-outline": !confirmRemove.value, "btn-success": confirmRemove.value}')
i.fa.fa-lock(ng-if='confirmRemove.value')
i.fa.fa-unlock(ng-if='!confirmRemove.value')
span(translate) Confirm Remove
button.btn.btn-xs.btn-danger-outline.pull-right(
type='button'
uib-tooltip="{{'Set filters for device removing' | translate}}"
tooltip-placement='top'
tooltip-popup-delay='500'
ng-click='showFilters = !showFilters'
ng-class='{"btn-danger-outline": !showFilters, "btn-danger": showFilters}')
i.fa.fa-trash-o
span(translate) Filters
li.list-group-item(ng-if='showFilters')
.device-line
.heading
i.fa.fa-trash-o
span(translate) Removing filters
form.form-inline.device-filters-items
.form-group.device-filters-item
label.device-list-label(
translate
uib-tooltip="{{'Device presence state' | translate}}"
tooltip-placement='top'
tooltip-popup-delay='500') Present
select(
ng-model='removingFilters.present'
ng-options='option for option in removingFilterOptions')
.form-group.device-filters-item
label.device-list-label(
translate
uib-tooltip="{{'Device booking state' | translate}}"
tooltip-placement='top'
tooltip-popup-delay='500') Booked
select(
ng-model='removingFilters.booked'
ng-options='option for option in removingFilterOptions')
.form-group.device-filters-item
label.device-list-label(
translate
uib-tooltip="{{'Device notes state' | translate}}"
tooltip-placement='top'
tooltip-popup-delay='500') Annotated
select(
ng-model='removingFilters.annotated'
ng-options='option for option in removingFilterOptions')
.form-group.device-filters-item
label.device-list-label(
translate
uib-tooltip="{{'Device controlling state' | translate}}"
tooltip-placement='top'
tooltip-popup-delay='500') Controlled
select(
ng-model='removingFilters.controlled'
ng-options='option for option in removingFilterOptions')
li.list-group-item(ng-repeat="device in devices \
| filter:search \
| orderBy: 'model' \
| pagedObjectsFilter:scopeDevicesCtrl:'deviceCurrentPage':'deviceItemsPerPage':'filteredDevices' \
track by device.serial")
.device-line.device-actions
i.fa.fa-mobile.fa-2x.fa-fw.device-list-icon
.device-list-details.selectable
.device-list-name(ng-bind-template="{{device.manufacturer + ' ' + device.model + ' (' + device.marketName + ')'}}")
.device-list-id
span(translate) Serial
span(ng-bind-template="{{::': ' + device.serial + ' - '}}")
span(translate) OS
span(ng-bind-template="{{': ' + device.version + ' - '}}")
span(translate) Screen
span(ng-bind-template="{{': ' + device.displayStr + ' - '}}")
span(translate) SDK
span(ng-bind-template="{{': ' + device.sdk + ' - '}}")
span(translate) Location
span(ng-bind-template="{{': ' + device.provider.name + ' - '}}")
span(translate) Group Origin
span(ng-bind-template="{{': ' + device.group.originName}}")
button.btn.btn-xs.btn-danger-outline.pull-right(
type='button'
ng-click='removeDevice(device.serial, confirmRemove.value)')
i.fa.fa-trash-o
span(translate) Remove

View file

@ -0,0 +1,18 @@
/**
* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/
require('./devices.css')
module.exports = angular.module('stf.settings.devices', [
require('stf/common-ui').name,
require('stf/settings').name,
require('stf/util/common').name,
require('stf/devices').name
])
.run(['$templateCache', function($templateCache) {
$templateCache.put(
'settings/devices/devices.pug', require('./devices.pug')
)
}])
.controller('DevicesCtrl', require('./devices-controller'))

Some files were not shown because too many files have changed in this diff Show more