add list api keys for account, track last_used for api_keys

This commit is contained in:
Dave Horton
2020-07-22 11:31:05 -04:00
parent 4efee5a8b8
commit 3be0412de1
7 changed files with 208 additions and 28 deletions
+2
View File
@@ -56,6 +56,8 @@ token CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36),
service_provider_sid CHAR(36),
expires_at TIMESTAMP,
last_used TIMESTAMP ON UPDATE NOW(),
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (api_key_sid)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
+20 -9
View File
@@ -137,12 +137,12 @@
<comment><![CDATA[An authorization token that is used to access the REST api]]></comment>
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>1302.00</x>
<y>61.00</y>
<x>1319.00</x>
<y>38.00</y>
</location>
<size>
<width>252.00</width>
<height>120.00</height>
<height>160.00</height>
</size>
<zorder>1</zorder>
<SQLField>
@@ -196,6 +196,17 @@
<type><![CDATA[TIMESTAMP]]></type>
<uid><![CDATA[DE86BC18-858E-4D7E-9B83-891DB2861434]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[last_used]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<uid><![CDATA[11A93288-B892-436B-9BB4-D5C3B70DB061]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[created_at]]></name>
<type><![CDATA[TIMESTAMP]]></type>
<defaultValue><![CDATA[NOW()]]></defaultValue>
<uid><![CDATA[C84C9B6A-80B5-4B0B-8C14-EB02F7421BBE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[13]]></labelWindowIndex>
<objectComment><![CDATA[An authorization token that is used to access the REST api]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
@@ -1072,17 +1083,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[1788.000000]]></RightSidebarWidth>
<RightSidebarWidth><![CDATA[1944.000000]]></RightSidebarWidth>
<sidebarIndex><![CDATA[2]]></sidebarIndex>
<snapToGrid><![CDATA[0]]></snapToGrid>
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
<windowHeight><![CDATA[1250.000000]]></windowHeight>
<windowLocationX><![CDATA[408.000000]]></windowLocationX>
<windowLocationY><![CDATA[94.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 0.5}]]></windowScrollOrigin>
<windowWidth><![CDATA[2065.000000]]></windowWidth>
<windowHeight><![CDATA[1194.000000]]></windowHeight>
<windowLocationX><![CDATA[26.000000]]></windowLocationX>
<windowLocationY><![CDATA[100.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 5}]]></windowScrollOrigin>
<windowWidth><![CDATA[2221.000000]]></windowWidth>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>
+54
View File
@@ -1,11 +1,53 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
class ApiKey extends Model {
constructor() {
super();
}
/**
* list all api keys for an account
*/
static retrieveAll(account_sid) {
const sql = account_sid ?
'SELECT * from api_keys WHERE account_sid = ?' :
'SELECT * from api_keys WHERE account_sid IS NULL';
const args = account_sid ? [account_sid] : [];
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, args, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* update last_used api key for an account
*/
static updateLastUsed(account_sid) {
const sql = 'UPDATE api_keys SET last_used = NOW() WHERE account_sid = ?';
const args = [account_sid];
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, args, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
}
ApiKey.table = 'api_keys';
ApiKey.fields = [
{
@@ -25,6 +67,18 @@ ApiKey.fields = [
{
name: 'service_provider_sid',
type: 'string'
},
{
name: 'expires_at',
type: 'date'
},
{
name: 'created_at',
type: 'date'
},
{
name: 'last_used',
type: 'date'
}
];
+28
View File
@@ -3,6 +3,7 @@ const request = require('request');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const ApiKey = require('../../models/api-key');
const ServiceProvider = require('../../models/service-provider');
const decorate = require('./decorate');
const snakeCase = require('../../utils/snake-case');
@@ -27,6 +28,15 @@ function coerceNumbers(callInfo) {
return callInfo;
}
async function updateLastUsed(logger, sid, req) {
if (req.user.hasAdminAuth || req.user.hasServiceProviderAuth) return;
try {
await ApiKey.updateLastUsed(sid);
} catch (err) {
logger.error({err}, `Error updating last used for accountSid ${sid}`);
}
}
function validateUpdateCall(opts) {
// only one type of update can be supplied per request
const hasWhisper = opts.whisper;
@@ -251,6 +261,19 @@ router.put('/:sid', async(req, res) => {
return res.status(404).end();
}
res.status(204).end();
updateLastUsed(logger, sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
/* retrieve account level api keys */
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ApiKey.retrieveAll(req.params.sid);
res.status(200).json(results);
updateLastUsed(logger, req.params.sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -276,6 +299,7 @@ router.post('/:sid/Calls', async(req, res) => {
await validateCreateCall(logger, sid, req);
logger.debug({payload: req.body}, `sending createCall API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
@@ -308,6 +332,7 @@ router.get('/:sid/Calls', async(req, res) => {
const calls = await listCalls(accountSid);
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -331,6 +356,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
logger.debug(`call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -354,6 +380,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
logger.debug(`call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -384,6 +411,7 @@ router.post('/:sid/Calls/:callSid', async(req, res) => {
logger.debug(`updateCall: call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
+55 -2
View File
@@ -37,7 +37,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulApiKeyAdd'
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
@@ -222,7 +222,8 @@ paths:
description: password successfully changed
content:
application/json:
schema: '#/components/schemas/Login'
schema:
$ref: '#/components/schemas/Login'
403:
description: password change failed
content:
@@ -1201,6 +1202,34 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/ApiKeys:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
get:
summary: get all api keys for an account
operationId: getAccountApiKeys
responses:
200:
description: list of api keys
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ApiKey'
404:
description: account not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Applications:
post:
@@ -1701,6 +1730,30 @@ components:
- account_sid
- inbound_hook
- inbound_status_hook
ApiKey:
type: object
properties:
api_key_sid:
type: string
format: uuid
token:
type: string
format: uuid
account_sid:
type: string
format: uuid
service_provider_sid:
type: string
format: uuid
expires_at:
type: dateTime
created_at:
type: dateTime
last_used:
type: dateTime
required:
- api_key_sid
- token
PhoneNumber:
type: object
properties:
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "1.1.6",
"version": "1.1.7",
"description": "",
"main": "app.js",
"scripts": {
@@ -15,27 +15,27 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"@jambonz/db-helpers": "^0.3.8",
"@jambonz/realtimedb-helpers": "0.2.15",
"mysql2": "^2.0.2",
"passport": "^0.4.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"mysql2": "^2.1.0",
"passport": "^0.4.1",
"passport-http-bearer": "^1.0.1",
"pino": "^5.14.0",
"request": "^2.88.0",
"pino": "^5.17.0",
"request": "^2.88.2",
"request-debug": "^0.2.0",
"swagger-ui-express": "^4.1.2",
"uuid": "^3.3.3",
"swagger-ui-express": "^4.1.4",
"uuid": "^3.4.0",
"yamljs": "^0.3.0"
},
"devDependencies": {
"eslint": "^6.8.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^15.0.1",
"request-promise-native": "^1.0.8",
"nyc": "^15.1.0",
"request-promise-native": "^1.0.9",
"tap-dot": "^2.0.0",
"tap-spec": "^5.0.0",
"tape": "^4.13.2"
"tape": "^4.13.3"
}
}
+37 -5
View File
@@ -4,7 +4,11 @@ const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createVoipCarrier, createServiceProvider, createPhoneNumber, deleteObjectBySid} = require('./utils');
const {
createVoipCarrier,
createServiceProvider,
createPhoneNumber,
deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -38,6 +42,26 @@ test('account tests', async(t) => {
t.ok(result.statusCode === 201, 'successfully created account');
const sid = result.body.sid;
/* add an account level api key */
result = await request.post(`/ApiKeys`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
account_sid: sid
}
});
t.ok(result.statusCode === 201 && result.body.token, 'successfully created account level token');
const apiKeySid = result.body.sid;
const accountLevelToken = result.body.token;
/* query all account level api keys */
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
auth: {bearer: accountLevelToken},
json: true,
});
t.ok(Array.isArray(result) && result.length === 1, 'successfully queried account level keys');
/* query all accounts */
result = await request.get('/Accounts', {
auth: authAdmin,
@@ -54,9 +78,9 @@ test('account tests', async(t) => {
});
t.ok(result.name === 'daveh' , 'successfully retrieved account by sid');
/* update accounts */
/* update account with account level token */
result = await request.put(`/Accounts/${sid}`, {
auth: authAdmin,
auth: {bearer: accountLevelToken},
json: true,
resolveWithFullResponse: true,
body: {
@@ -67,8 +91,15 @@ test('account tests', async(t) => {
}
}
});
t.ok(result.statusCode === 204, 'successfully updated account');
t.ok(result.statusCode === 204, 'successfully updated account using account level token');
/* verify that account level api key last_used was updated*/
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
auth: {bearer: accountLevelToken},
json: true,
});
t.ok(typeof result[0].last_used === 'string', 'api_key last_used timestamp was updated');
result = await request.get(`/Accounts/${sid}`, {
auth: authAdmin,
json: true,
@@ -97,6 +128,7 @@ test('account tests', async(t) => {
t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete account with phone numbers', 'cannot delete account with phone numbers');
/* delete account */
await request.delete(`ApiKeys/${apiKeySid}`, {auth: {bearer: accountLevelToken}});
await request.delete(`/PhoneNumbers/${phone_number_sid}`, {auth: authAdmin});
result = await request.delete(`/Accounts/${sid}`, {
auth: authAdmin,
@@ -109,7 +141,7 @@ test('account tests', async(t) => {
t.end();
}
catch (err) {
//console.error(err);
console.error(err);
t.end(err);
}
});