mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-10-29 19:04:12 +00:00
feature/user-api-calls (#80)
* initial changes for jwt auth * return permissions as an array of string * basic GET, POST, DELETE user api calls * add permission checks * hide hashed_password * cleanup * add check if admin user is active * return account and serviceProvider sid un user object * add more values to user PUT * logout user after self delete, fix scope assignment * add admin scope user tests * fix test case and align jwt and api key data model in req.user * fixes for ibm speech * add limits license_count and voice_call_minutes * update limits enum again * rebase to main * allow predefined carriers and speech credentials for Account user * reverse the hasAccountPermissions changes * SpeechCredentials permissions * fix /Users/me api non-saas jambonz Co-authored-by: Dave Horton <daveh@beachdognet.com> Co-authored-by: eglehelms <e.helms@cognigy.com>
This commit is contained in:
14 files changed
+759
-133
No files matched your search
+284
-80
@@ -1,6 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const User = require('../../models/user');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
@@ -11,6 +12,11 @@ FROM users user
|
||||
JOIN accounts AS account ON account.account_sid = user.account_sid
|
||||
LEFT JOIN service_providers as sp ON account.service_provider_sid = sp.service_provider_sid
|
||||
WHERE user.user_sid = ?`;
|
||||
const retrieveMyDetails2 = `SELECT *
|
||||
FROM users user
|
||||
LEFT JOIN accounts AS account ON account.account_sid = user.account_sid
|
||||
LEFT JOIN service_providers as sp ON sp.service_provider_sid = user.service_provider_sid
|
||||
WHERE user.user_sid = ?`;
|
||||
const retrieveSql = 'SELECT * from users where user_sid = ?';
|
||||
const retrieveProducts = `SELECT *
|
||||
FROM account_products
|
||||
@@ -23,7 +29,15 @@ const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WH
|
||||
const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?';
|
||||
|
||||
const validateRequest = async(user_sid, payload) => {
|
||||
const {old_password, new_password, name, email, email_activation_code} = payload;
|
||||
const {
|
||||
old_password,
|
||||
new_password,
|
||||
initial_password,
|
||||
name,
|
||||
email,
|
||||
email_activation_code,
|
||||
force_change,
|
||||
is_active} = payload;
|
||||
|
||||
const [r] = await promisePool.query(retrieveSql, user_sid);
|
||||
if (r.length === 0) return null;
|
||||
@@ -37,10 +51,11 @@ const validateRequest = async(user_sid, payload) => {
|
||||
throw new DbErrorBadRequest('can not change password when using oauth2');
|
||||
}
|
||||
|
||||
if ((email && !email_activation_code) || (email_activation_code && !email)) {
|
||||
if (email_activation_code && !email) {
|
||||
throw new DbErrorBadRequest('email and email_activation_code both required');
|
||||
}
|
||||
if (!name && !new_password && !email) throw new DbErrorBadRequest('no updates requested');
|
||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
|
||||
throw new DbErrorBadRequest('no updates requested');
|
||||
|
||||
return user;
|
||||
};
|
||||
@@ -69,14 +84,14 @@ router.get('/', async(req, res) => {
|
||||
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;
|
||||
const {user_sid, name, email, force_change, is_active, account_sid, service_provider_sid} = user;
|
||||
let scope;
|
||||
if (!user.account_sid && !user.service_provider_sid) {
|
||||
scope = 'admin';
|
||||
} else if (user.service_provider_sid) {
|
||||
if (account_sid && service_provider_sid) {
|
||||
scope = 'account';
|
||||
} else if (service_provider_sid) {
|
||||
scope = 'service_provider';
|
||||
} else {
|
||||
scope = 'account';
|
||||
scope = 'admin';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -85,7 +100,9 @@ router.get('/', async(req, res) => {
|
||||
email,
|
||||
scope,
|
||||
force_change,
|
||||
is_active
|
||||
is_active,
|
||||
account_sid,
|
||||
service_provider_sid
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -100,80 +117,116 @@ router.get('/me', async(req, res) => {
|
||||
|
||||
if (!user_sid) return res.sendStatus(403);
|
||||
|
||||
let payload;
|
||||
try {
|
||||
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
|
||||
logger.debug(r, 'retrieved user details');
|
||||
const payload = r[0];
|
||||
const {user, account, sp} = payload;
|
||||
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
|
||||
delete user[prop];
|
||||
});
|
||||
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
|
||||
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
|
||||
account.root_domain = sp.root_domain;
|
||||
delete payload.sp;
|
||||
|
||||
/* get api keys */
|
||||
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
|
||||
payload.api_keys = keys.map((k) => {
|
||||
return {
|
||||
api_key_sid: k.api_key_sid,
|
||||
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
|
||||
token: k.token,
|
||||
last_used: k.last_used,
|
||||
created_at: k.created_at
|
||||
};
|
||||
});
|
||||
|
||||
/* get products */
|
||||
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
|
||||
if (!products.length || !products[0].account_subscriptions) {
|
||||
throw new Error('account is missing a subscription');
|
||||
}
|
||||
const account_subscription = products[0].account_subscriptions;
|
||||
payload.subscription = {
|
||||
status: 'active',
|
||||
account_subscription_sid: account_subscription.account_subscription_sid,
|
||||
start_date: account_subscription.effective_start_date,
|
||||
products: products.map((prd) => {
|
||||
return {
|
||||
name: prd.products.name,
|
||||
units: prd.products.unit_label,
|
||||
quantity: prd.account_products.quantity
|
||||
};
|
||||
})
|
||||
};
|
||||
if (account_subscription.pending) {
|
||||
Object.assign(payload.subscription, {
|
||||
status: 'suspended',
|
||||
suspend_reason: account_subscription.pending_reason
|
||||
if (process.env.JAMBONES_HOSTING) {
|
||||
const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid);
|
||||
logger.debug(r, 'retrieved user details');
|
||||
payload = r[0];
|
||||
const {user, account, sp} = payload;
|
||||
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => {
|
||||
delete user[prop];
|
||||
});
|
||||
}
|
||||
const {
|
||||
last4,
|
||||
exp_month,
|
||||
exp_year,
|
||||
card_type,
|
||||
stripe_statement_descriptor
|
||||
} = account_subscription;
|
||||
if (last4) {
|
||||
const real_last4 = decrypt(last4);
|
||||
Object.assign(payload.subscription, {
|
||||
last4: real_last4,
|
||||
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
|
||||
['is_active'].forEach((prop) => account[prop] = !!account[prop]);
|
||||
account.root_domain = sp.root_domain;
|
||||
delete payload.sp;
|
||||
|
||||
/* get api keys */
|
||||
const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid);
|
||||
payload.api_keys = keys.map((k) => {
|
||||
return {
|
||||
api_key_sid: k.api_key_sid,
|
||||
//token: k.token.replace(/.(?=.{4,}$)/g, '*'),
|
||||
token: k.token,
|
||||
last_used: k.last_used,
|
||||
created_at: k.created_at
|
||||
};
|
||||
});
|
||||
|
||||
/* get products */
|
||||
const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid);
|
||||
if (!products.length || !products[0].account_subscriptions) {
|
||||
throw new Error('account is missing a subscription');
|
||||
}
|
||||
const account_subscription = products[0].account_subscriptions;
|
||||
payload.subscription = {
|
||||
status: 'active',
|
||||
account_subscription_sid: account_subscription.account_subscription_sid,
|
||||
start_date: account_subscription.effective_start_date,
|
||||
products: products.map((prd) => {
|
||||
return {
|
||||
name: prd.products.name,
|
||||
units: prd.products.unit_label,
|
||||
quantity: prd.account_products.quantity
|
||||
};
|
||||
})
|
||||
};
|
||||
if (account_subscription.pending) {
|
||||
Object.assign(payload.subscription, {
|
||||
status: 'suspended',
|
||||
suspend_reason: account_subscription.pending_reason
|
||||
});
|
||||
}
|
||||
const {
|
||||
last4,
|
||||
exp_month,
|
||||
exp_year,
|
||||
card_type,
|
||||
statement_descriptor: stripe_statement_descriptor
|
||||
stripe_statement_descriptor
|
||||
} = account_subscription;
|
||||
if (last4) {
|
||||
const real_last4 = decrypt(last4);
|
||||
Object.assign(payload.subscription, {
|
||||
last4: real_last4,
|
||||
exp_month,
|
||||
exp_year,
|
||||
card_type,
|
||||
statement_descriptor: stripe_statement_descriptor
|
||||
});
|
||||
}
|
||||
|
||||
/* get static ips */
|
||||
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
|
||||
payload.static_ips = static_ips.map((r) => r.public_ipv4);
|
||||
}
|
||||
else {
|
||||
const [r] = await promisePool.query({sql: retrieveMyDetails2, nestTables: true}, user_sid);
|
||||
logger.debug(r, 'retrieved user details');
|
||||
payload = r[0];
|
||||
const {user} = payload;
|
||||
['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code'].forEach((prop) => {
|
||||
delete user[prop];
|
||||
});
|
||||
['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]);
|
||||
}
|
||||
logger.debug({payload}, 'returning user details');
|
||||
res.json(payload);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const token = req.user.jwt;
|
||||
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const {user_sid} = req.params;
|
||||
|
||||
try {
|
||||
const [user] = await User.retrieve(user_sid);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {hashed_password, ...rest} = user;
|
||||
if (!user) throw new Error('failure retrieving user');
|
||||
|
||||
if (decodedJwt.scope === 'admin' ||
|
||||
decodedJwt.scope === 'account' && decodedJwt.account_sid === user.account_sid ||
|
||||
decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user.service_provider_sid) {
|
||||
res.status(200).json(rest);
|
||||
} else {
|
||||
res.sendStatus(403);
|
||||
}
|
||||
|
||||
/* get static ips */
|
||||
const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid);
|
||||
payload.static_ips = static_ips.map((r) => r.public_ipv4);
|
||||
|
||||
logger.debug({payload}, 'returning user details');
|
||||
|
||||
res.json(payload);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -182,9 +235,30 @@ router.get('/me', async(req, res) => {
|
||||
router.put('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
const {old_password, new_password, name, email, email_activation_code} = req.body;
|
||||
const user = await User.retrieve(user_sid);
|
||||
const token = req.user.jwt;
|
||||
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const {
|
||||
old_password,
|
||||
new_password,
|
||||
initial_password,
|
||||
email_activation_code,
|
||||
email,
|
||||
name,
|
||||
is_active,
|
||||
force_change,
|
||||
account_sid,
|
||||
service_provider_sid
|
||||
} = req.body;
|
||||
|
||||
if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
|
||||
if (decodedJwt.scope !== 'admin' &&
|
||||
!(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) &&
|
||||
!(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid) &&
|
||||
(req.user.user_sid && req.user.user_sid !== user_sid)) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await validateRequest(user_sid, req.body);
|
||||
@@ -209,10 +283,51 @@ router.put('/:user_sid', async(req, res) => {
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (email) {
|
||||
if (initial_password) {
|
||||
const passwordHash = await generateHashedPassword(initial_password);
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
|
||||
[email, email_activation_code, user_sid]);
|
||||
'UPDATE users SET hashed_password = ? WHERE user_sid = ?',
|
||||
[passwordHash, user_sid]
|
||||
);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (is_active) {
|
||||
const r = await promisePool.execute('UPDATE users SET is_active = ? WHERE user_sid = ?', [is_active, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (force_change) {
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET force_change = ? WHERE user_sid = ?',
|
||||
[force_change, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (account_sid || account_sid === null) {
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET account_sid = ? WHERE user_sid = ?',
|
||||
[account_sid, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (service_provider_sid || service_provider_sid === null) {
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET service_provider_sid = ? WHERE user_sid = ?',
|
||||
[service_provider_sid, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
|
||||
if (email) {
|
||||
if (email_activation_code) {
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?',
|
||||
[email, email_activation_code, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
}
|
||||
const r = await promisePool.execute(
|
||||
'UPDATE users SET email = ? WHERE user_sid = ?',
|
||||
[email, user_sid]);
|
||||
if (0 === r.changedRows) throw new Error('database update failed');
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
@@ -225,5 +340,94 @@ router.put('/:user_sid', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const passwordHash = await generateHashedPassword(req.body.initial_password);
|
||||
const payload = {
|
||||
...req.body,
|
||||
provider: 'local',
|
||||
hashed_password: passwordHash,
|
||||
};
|
||||
const allUsers = await User.retrieveAll();
|
||||
const token = req.user.jwt;
|
||||
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
|
||||
delete payload.initial_password;
|
||||
|
||||
try {
|
||||
const email = allUsers.find((e) => e.email === payload.email);
|
||||
if (email) {
|
||||
logger.debug({payload}, 'user with this email already exists');
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
if (decodedJwt.scope === 'admin') {
|
||||
logger.debug({payload}, 'POST /users');
|
||||
const uuid = await User.make(payload);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else if (decodedJwt.scope === 'account') {
|
||||
logger.debug({payload}, 'POST /users');
|
||||
const uuid = await User.make({
|
||||
...payload,
|
||||
account_sid: decodedJwt.account_sid,
|
||||
});
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else if (decodedJwt.scope === 'service_provider') {
|
||||
logger.debug({payload}, 'POST /users');
|
||||
const uuid = await User.make({
|
||||
...payload,
|
||||
service_provider_sid: decodedJwt.service_provider_sid,
|
||||
});
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else {
|
||||
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
|
||||
}
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
const token = req.user.jwt;
|
||||
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const allUsers = await User.retrieveAll();
|
||||
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
|
||||
const user = await User.retrieve(user_sid);
|
||||
|
||||
try {
|
||||
if (decodedJwt.scope === 'admin' && activeAdminUsers.length === 1) {
|
||||
throw new Error('cannot delete this admin user - there are no other active admin users');
|
||||
}
|
||||
|
||||
if (decodedJwt.scope === 'admin' ||
|
||||
(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) ||
|
||||
(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid)) {
|
||||
await User.remove(user_sid);
|
||||
//logout user after self-delete
|
||||
if (decodedJwt.user_sid === user_sid) {
|
||||
request({
|
||||
url:'http://localhost:3000/v1/logout',
|
||||
method: 'POST',
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, 'could not log out user');
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
logger.debug({user}, 'user deleted and logged out');
|
||||
});
|
||||
}
|
||||
return res.sendStatus(204);
|
||||
} else {
|
||||
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
|
||||
}
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
Reference in new issue
Block a user