Feature/add users api (#77)

* initial changes for jwt auth

* return permissions as an array of string

* Add JWT expiration environment variable (#74)

* allow fromHost in createCall REST API

* add JWT_EXPIRES_IN=<mins> env variable, 60 mins by default

* add jwt expiration in register.js and signin.js

* fix tests - add permissions and scope to encoded obj in jwt

Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>

* return only the jwt-token in the api response

* update swagger.yaml

* add /users api

* apply review comments

* add users test case

* added User model

* bugfix: admin user should be able to create a carrier for a service provider

Co-authored-by: EgleH <egle.helms@gmail.com>
Co-authored-by: eglehelms <e.helms@cognigy.com>
This commit is contained in:
Dave Horton
2022-11-07 13:47:18 -05:00
committed by GitHub
parent 46eee0cc60
commit 1b67d5f89d
8 changed files with 727 additions and 1100 deletions

85
lib/models/user.js Normal file
View File

@@ -0,0 +1,85 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlAccount = 'SELECT * FROM users WHERE account_sid = ?';
const sqlSP = 'SELECT * FROM users WHERE service_provider_sid = ?';
class User extends Model {
constructor() {
super();
}
static async retrieveAllForAccount(account_sid) {
const [rows] = await promisePool.query(sqlAccount, [account_sid]);
return rows;
}
static async retrieveAllForServiceProvider(service_provider_sid) {
const [rows] = await promisePool.query(sqlSP, [service_provider_sid]);
return rows;
}
}
User.table = 'users';
User.fields = [
{
name: 'user_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'email',
type: 'string',
required: true
},
{
name: 'pending_email',
type: 'string'
},
{
name: 'phone',
type: 'string'
},
{
name: 'hashed_password',
type: 'string'
},
{
name: 'account_sid',
type: 'string'
},
{
name: 'service_provider_sid',
type: 'string'
},
{
name: 'force_change',
type: 'number'
},
{
name: 'provider',
type: 'string'
},
{
name: 'provider_userid',
type: 'string'
},
{
name: 'email_activation_code',
type: 'string'
},
{
name: 'email_validated',
type: 'number'
},
{
name: 'is_active',
type: 'number'
},
];
module.exports = User;

View File

@@ -26,10 +26,6 @@ router.post('/:sid', async(req, res) => {
let service_provider_sid;
const {account_sid} = req.user;
if (!account_sid) {
if (!req.user.hasScope('service_provider')) {
logger.error({user: req.user}, 'invalid creds');
return res.sendStatus(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
try {

View File

@@ -1,6 +1,6 @@
//const assert = require('assert');
//const debug = require('debug')('jambonz:api-server');
const router = require('express').Router();
const User = require('../../models/user');
const jwt = require('jsonwebtoken');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
@@ -45,6 +45,55 @@ const validateRequest = async(user_sid, payload) => {
return user;
};
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
let usersList;
try {
let results;
if (decodedJwt.scope === 'admin') {
results = await User.retrieveAll();
}
else if (decodedJwt.scope === 'account') {
results = await User.retrieveAllForAccount(decodedJwt.account_sid);
}
else if (decodedJwt.scope === 'service_provider') {
results = await User.retrieveAllForServiceProvider(decodedJwt.service_provider_sid);
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
if (results.length === 0) throw new Error('failure retrieving users list');
usersList = results.map((user) => {
const {user_sid, name, email, force_change, is_active} = user;
let scope;
if (!user.account_sid && !user.service_provider_sid) {
scope = 'admin';
} else if (user.service_provider_sid) {
scope = 'service_provider';
} else {
scope = 'account';
}
return {
user_sid,
name,
email,
scope,
force_change,
is_active
};
});
} catch (err) {
sysError(logger, res, err);
}
res.status(200).json(usersList);
});
router.get('/me', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.user;

View File

@@ -515,6 +515,28 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Users:
get:
summary: list all users
operationId: listUsers
responses:
200:
description: list of users
content:
application/json:
schema:
type:
array
items:
$ref: '#/components/schemas/Users'
403:
description: unauthorized
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Users/{UserSid}:
parameters:
- name: UserSid

1598
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
"test": "NODE_ENV=test JAMBONES_AUTH_USE_JWT=1 APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
"integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
@@ -18,10 +18,10 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@google-cloud/speech": "^4.10.2",
"@google-cloud/text-to-speech": "^3.4.0",
"@jambonz/db-helpers": "^0.6.19",
"@jambonz/realtimedb-helpers": "^0.5.1",
"@google-cloud/speech": "^5.1.0",
"@google-cloud/text-to-speech": "^4.0.3",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.5.7",
"@jambonz/time-series": "^0.2.5",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1152.0",

View File

@@ -12,6 +12,7 @@ require('./sbcs');
require('./ms-teams');
require('./speech-credentials');
require('./recent-calls');
require('./users');
require('./webapp_tests');
// require('./homer');
require('./call-test');

54
test/users.js Normal file
View File

@@ -0,0 +1,54 @@
const test = require('tape') ;
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const exec = require('child_process').exec ;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('add an admin user', (t) => {
exec(`${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
console.log(stderr);
console.log(stdout);
if (err) return t.end(err);
t.pass('successfully added admin user');
t.end();
});
});
test('user tests', async(t) => {
const app = require('../app');
let sid;
try {
let result;
/* login as admin to get a jwt */
result = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: 'admin',
password: 'admin',
}
});
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as admin');
/* retrieve list of users */
const authAdmin = {bearer: result.body.token};
result = await request.get(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
//console.log(result.body);
t.ok(result.statusCode === 200 && result.body.length === 1, 'successfully user list');
} catch (err) {
console.error(err);
t.end(err);
}
});