diff --git a/jest.config.ts b/jest.config.ts index c386f8a7..d9e46d99 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,72 +1,86 @@ import type { Config } from '@jest/types'; const config: Config.InitialOptions = { - coverageDirectory: '/coverage/', - projects: [ - { - displayName: "ACCOUNTS", - testMatch: ['/packages/accounts/__tests__/**/*.test.ts'], - }, - { - displayName: "APPLICATIONS", - testMatch: ['/packages/applications/__tests__/**/*.test.ts'], - }, - { - displayName: "AUTH", - testMatch: ['/packages/auth/__tests__/**/*.test.ts'], - }, - { - displayName: "JWT", - testMatch: ['/packages/jwt/__tests__/**/*.test.ts'], - }, - { - displayName: "MESSAGES", - testMatch: ['/packages/messages/__tests__/**/*.test.ts'], - }, - { - displayName: "NUMBER INSIGHTS", - testMatch: ['/packages/number-insights/__tests__/**/*.test.ts'], - }, - { - displayName: "NUMBERS", - testMatch: ['/packages/numbers/__tests__/**/*.test.ts'], - }, - { - displayName: "PRICING", - testMatch: ['/packages/pricing/__tests__/**/*.test.ts'], - }, - { - displayName: "SERVER CLIENT", - testMatch: ['/packages/server-client/__tests__/**/*.test.ts'], - }, - { - displayName: "SERVER SDK", - testMatch: ['/packages/server-sdk/__tests__/**/*.test.ts'], - }, - { - displayName: "SMS", - testMatch: ['/packages/sms/__tests__/**/*.test.ts'], - }, - { - displayName: "VERIFY", - testMatch: ['/packages/verify/__tests__/**/*.test.ts'], - }, - { - displayName: "VETCH", - testMatch: ['/packages/vetch/__tests__/**/*.test.ts'], - }, - { - displayName: "VIDEO", - testMatch: ['/packages/video/__tests__/**/*.test.ts'], - }, - { - displayName: "VOICE", - testMatch: ['/packages/voice/__tests__/**/*.test.ts'], - }, - ], - moduleNameMapper: { - '@vonage/(.+)': '/packages/$1/lib', + coverageDirectory: '/coverage/', + projects: [ + { + displayName: 'ACCOUNTS', + testMatch: ['/packages/accounts/__tests__/**/*.test.ts'], }, + { + displayName: 'APPLICATIONS', + testMatch: [ + '/packages/applications/__tests__/**/*.test.ts', + ], + }, + { + displayName: 'AUDIT', + testMatch: ['/packages/audit/__tests__/**/*.test.ts'], + }, + { + displayName: 'AUDIT', + testMatch: ['/packages/audit/tests__/**/*.test.ts'], + }, + { + displayName: 'AUTH', + testMatch: ['/packages/auth/__tests__/**/*.test.ts'], + }, + { + displayName: 'JWT', + testMatch: ['/packages/jwt/__tests__/**/*.test.ts'], + }, + { + displayName: 'MESSAGES', + testMatch: ['/packages/messages/__tests__/**/*.test.ts'], + }, + { + displayName: 'NUMBER INSIGHTS', + testMatch: [ + '/packages/number-insights/__tests__/**/*.test.ts', + ], + }, + { + displayName: 'NUMBERS', + testMatch: ['/packages/numbers/__tests__/**/*.test.ts'], + }, + { + displayName: 'PRICING', + testMatch: ['/packages/pricing/__tests__/**/*.test.ts'], + }, + { + displayName: 'SERVER CLIENT', + testMatch: [ + '/packages/server-client/__tests__/**/*.test.ts', + ], + }, + { + displayName: 'SERVER SDK', + testMatch: ['/packages/server-sdk/__tests__/**/*.test.ts'], + }, + { + displayName: 'SMS', + testMatch: ['/packages/sms/__tests__/**/*.test.ts'], + }, + { + displayName: 'VERIFY', + testMatch: ['/packages/verify/__tests__/**/*.test.ts'], + }, + { + displayName: 'VETCH', + testMatch: ['/packages/vetch/__tests__/**/*.test.ts'], + }, + { + displayName: 'VIDEO', + testMatch: ['/packages/video/__tests__/**/*.test.ts'], + }, + { + displayName: 'VOICE', + testMatch: ['/packages/voice/__tests__/**/*.test.ts'], + }, + ], + moduleNameMapper: { + '@vonage/(.+)': '/packages/$1/lib', + }, }; export default config; diff --git a/package-lock.json b/package-lock.json index aaf3e4eb..6476826a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5370,6 +5370,10 @@ "resolved": "packages/applications", "link": true }, + "node_modules/@vonage/audit": { + "resolved": "packages/audit", + "link": true + }, "node_modules/@vonage/auth": { "resolved": "packages/auth", "link": true @@ -16984,6 +16988,18 @@ "nock": "^13.2.9" } }, + "packages/audit": { + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@vonage/auth": "^1.0.5", + "@vonage/server-client": "^1.0.8", + "@vonage/vetch": "^1.0.6" + }, + "devDependencies": { + "nock": "^13.2.9" + } + }, "packages/auth": { "name": "@vonage/auth", "version": "1.0.5", @@ -21190,6 +21206,15 @@ "nock": "^13.2.9" } }, + "@vonage/audit": { + "version": "file:packages/audit", + "requires": { + "@vonage/auth": "^1.0.5", + "@vonage/server-client": "^1.0.8", + "@vonage/vetch": "^1.0.6", + "nock": "^13.2.9" + } + }, "@vonage/auth": { "version": "file:packages/auth", "requires": { diff --git a/packages/audit/README.md b/packages/audit/README.md new file mode 100644 index 00000000..da6dcc00 --- /dev/null +++ b/packages/audit/README.md @@ -0,0 +1,114 @@ +# Vonage Number SDK for Node.js + +![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/vonage/vonage-node-sdk/Vonage/3.x?logo=github&style=flat-square&label=Workflow%20Build) +[![Codecov](https://img.shields.io/codecov/c/github/vonage/vonage-node-sdk?label=Codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/Vonage/vonage-server-sdk) +![Latest Release](https://img.shields.io/npm/v/@vonage/numbers) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg?style=flat-square)](../../CODE_OF_CONDUCT.md) +[![License](https://img.shields.io/npm/l/@vonage/numbers?label=License&style=flat-square)][license] + +Vonage + +This is the Vonage Number SDK for Node.js for use with +[Vonage APIs](https://www.vonage.com/). To use it you will need a Vonage +account. Sign up [for free at vonage.com][signup]. + +We recommend using this package as part of the overall [ +`@vonage/server-sdk` package](https://github.com/vonage/vonage-node-sdk). + +For full API documentation refer to [developer.nexmo.com](https://developer.nexmo.com/). + +* [Installation](#installation) +* [Usage](#using-the-vonage-numbers-sdk) +* [Promises](#promises) +* [Testing](#testing) + +## Installation + +We recommend using this SDK as part of the overall [ +`@vonage/server-sdk` package](https://github.com/vonage/vonage-node-sdk). +Please see the main package for installation. + +You can also use this SDK standalone if you only need access to just the +Numbers API. + +### With NPM + +```bash +npm install @vonage/numbers +``` + +### With Yarn + +```bash +yarn add @vonage/numbers +``` + +## Using the Vonage Numbers SDK + +### As part of the Vonage Server SDK + +If you are using this SDK as part of the Vonage Server SDK, you can access it +as the `numbers` property off of the client that you instantiate. + +```js +const { Vonage, Auth } = require('@vonage/server-sdk'); + +const credentials = new Auth({ + apiKey: API_KEY, + apiSecret: API_SECRET +}); +const options = {}; +const vonage = new Vonage(credentials, options); + +vonage.numbers.getAvailableNumbers() + .then(resp => console.log(resp)) + .catch(err => console.error(err)); +``` + +### Standalone + +The SDK can be used standalone from the main +[Vonage Server SDK for Node.js](https://github.com/vonage/vonage-node-sdk) if +you only need to use the Numbers API. All you need to do is +`require('@vonage/numbers')`, and use the returned object to create your own +client. + +```js +const { Auth } = require('@vonage/auth'); +const { Numbers } = require('@vonage/numbers'); + +const credentials = new Auth({ + apiKey: API_KEY, + apiSecret: API_SECRET +}); +const options = {}; + +const numbersClient = new Numbers(credentials, options); +``` + +Where `credentials` is any option from [`@vonage/auth`](https://github.com/Vonage/vonage-node-sdk/tree/3.x/readme/packages/auth#options), +and `options` is any option from [`@vonage/server-client`](https://github.com/Vonage/vonage-node-sdk/tree/3.x/readme/packages/server-client#options) + +## Promises + +Most methods that interact with the Vonage API uses Promises. You can either +resolve these yourself, or use `await` to wait for a response. + +```js +const resp = await vonage.numbers.basicLookup(PHONE_NUMBER) + +vonage.numbers.getAvailableNumbers() + .then(resp => console.log(resp)) + .catch(err => console.error(err)); +``` + +## Testing + +Run: + +```bash +npm run test +``` + +[signup]: https://dashboard.nexmo.com/sign-up?utm_source=DEV_REL&utm_medium=github&utm_campaign=node-server-sdk +[license]: ../../LICENSE.txt diff --git a/packages/audit/__tests__/__dataSets__/getEvent.ts b/packages/audit/__tests__/__dataSets__/getEvent.ts new file mode 100644 index 00000000..af56af9c --- /dev/null +++ b/packages/audit/__tests__/__dataSets__/getEvent.ts @@ -0,0 +1,26 @@ +const BASE_URL = 'https://api.nexmo.com/'; +const CLIENT_METHOD = 'getEvent'; + +export default [ + { + label: 'get event', + clientMethod: CLIENT_METHOD, + exception: false, + request: { + url: BASE_URL, + intercept: [`/beta/audit/events/asdf`, 'GET'], + reply: [ + 200, + { + id: 'asdf', + created_at: '2022-11-15T17:30:33', + }, + ], + }, + parameters: ['asdf'], + expected: { + id: 'asdf', + createdAt: '2022-11-15T17:30:33', + }, + }, +]; diff --git a/packages/audit/__tests__/__dataSets__/getEvents.ts b/packages/audit/__tests__/__dataSets__/getEvents.ts new file mode 100644 index 00000000..f1627b6a --- /dev/null +++ b/packages/audit/__tests__/__dataSets__/getEvents.ts @@ -0,0 +1,196 @@ +import { AuditEvent } from '../../lib/types.ts'; +import { AuditEventTypes } from '../../lib/enums'; +import { Client } from '@vonage/server-client'; + +const BASE_URL = 'https://api.nexmo.com/'; +const CLIENT_METHOD = 'getEvents'; + +const createEvent = (event: AuditEvent): AuditEvent => ({ + id: event.id, + eventType: event.eventType, + createdAt: '2022-11-15T17:30:33', + userEmail: event.userEmail, + userId: event.userId, + accountId: event.accountId, + source: event.source, + sourceDescription: event.sourceDescription, + sourceCountry: event.sourceCountry, + context: event.context, +}); + +const onePageEvent = [createEvent({ id: '1' })]; + +const twoPageEvent = [createEvent({ id: '2' }), createEvent({ id: '3' })]; + +const eventParameters = new URLSearchParams([ + ['event_type', AuditEventTypes.ACCOUNT_SECRET_CREATE], + ['date_from', createEvent({}).createdAt], + ['date_to', createEvent({}).createdAt], + ['search_text', 'fizz-buzz'], + ['page', 42], + ['size', 21], +]); + +export default [ + { + label: 'get events on one page', + clientMethod: CLIENT_METHOD, + exception: false, + request: { + url: BASE_URL, + requests: [ + { + request: [`/beta/audit/events?`, 'GET'], + reply: [ + 200, + { + _embedded: { + events: onePageEvent.map( + Client.transformers.snakeCaseObjectKeys, + ), + }, + page: { + size: 20, + totalElements: 1, + totalPages: 1, + page: 1, + }, + }, + ], + }, + ], + }, + parameters: [], + expected: onePageEvent, + }, + { + label: 'get events on multiple pages', + clientMethod: CLIENT_METHOD, + exception: false, + request: { + url: BASE_URL, + requests: [ + { + request: [`/beta/audit/events?`, 'GET'], + reply: [ + 200, + { + _embedded: { + events: twoPageEvent.map( + Client.transformers.snakeCaseObjectKeys, + ), + }, + page: { + size: 20, + totalElements: 3, + totalPages: 2, + page: 1, + }, + }, + ], + }, + { + request: [`/beta/audit/events?`, 'GET'], + reply: [ + 200, + { + _embedded: { + events: onePageEvent.map( + Client.transformers.snakeCaseObjectKeys, + ), + }, + page: { + size: 20, + totalElements: 3, + totalPages: 2, + page: 2, + }, + }, + ], + }, + ], + }, + parameters: [], + expected: [...twoPageEvent, ...onePageEvent], + }, + { + label: 'get events and handle non 200 on subsequent pages', + clientMethod: CLIENT_METHOD, + exception: false, + request: { + url: BASE_URL, + requests: [ + { + request: [`/beta/audit/events?`, 'GET'], + reply: [ + 200, + { + _embedded: { + events: twoPageEvent.map( + Client.transformers.snakeCaseObjectKeys, + ), + }, + page: { + size: 20, + totalElements: 3, + totalPages: 2, + page: 1, + }, + }, + ], + }, + { + request: [`/beta/audit/events?`, 'GET'], + reply: [ + 401, + { + status: 401, + error: 'Unauthorized', + message: 'Failed', + }, + ], + }, + ], + }, + parameters: [], + expected: [...twoPageEvent], + }, + { + label: 'get events with parameters', + clientMethod: CLIENT_METHOD, + exception: false, + request: { + url: BASE_URL, + requests: [ + { + request: [ + `/beta/audit/events?${eventParameters.toString()}`, + 'GET', + ], + reply: [ + 200, + { + _embedded: { + events: onePageEvent.map( + Client.transformers.snakeCaseObjectKeys, + ), + }, + page: { + size: 20, + totalElements: 1, + totalPages: 1, + page: 1, + }, + }, + ], + }, + ], + }, + parameters: [ + Client.transformers.camelCaseObjectKeys( + Object.fromEntries(eventParameters), + ), + ], + expected: onePageEvent, + }, +]; diff --git a/packages/audit/__tests__/__dataSets__/index.ts b/packages/audit/__tests__/__dataSets__/index.ts new file mode 100644 index 00000000..103173a1 --- /dev/null +++ b/packages/audit/__tests__/__dataSets__/index.ts @@ -0,0 +1,8 @@ +import getEvents from './getEvents'; + +export default [ + { + label: 'Get Events', + tests: getEvents, + }, +]; diff --git a/packages/audit/__tests__/audit.test.ts b/packages/audit/__tests__/audit.test.ts new file mode 100644 index 00000000..903c5abb --- /dev/null +++ b/packages/audit/__tests__/audit.test.ts @@ -0,0 +1,63 @@ +import nock from 'nock'; +import { Auth } from '@vonage/auth'; +import { Audit } from '../lib/index'; +import getEvents from './__dataSets__/getEvents'; +import getEvent from './__dataSets__/getEvent'; + +describe('Audit Events', () => { + let client: Audit; + + beforeEach(function () { + client = new Audit(new Auth({ apiKey: '12345', apiSecret: 'ABCDE' })); + }); + + afterEach(function () { + client = null; + nock.cleanAll(); + }); + + test.each(getEvents)( + 'Can $label', + async ({ request, parameters, expected }) => { + const { url, requests } = request; + + const scope = nock(url, { + reqheaders: { + authorization: 'Basic MTIzNDU6QUJDREU=', + }, + }); + requests.forEach(({ request, reply }) => { + scope.intercept(...request).reply(...reply); + }); + + const results = []; + for await (const event of client.getEvents(...parameters)) { + results.push(event); + } + + expect(results).toEqual(expected); + expect(scope.isDone()).toBeTruthy(); + }, + ); + + test('Can get event', async () => { + const scope = nock('https://api.nexmo.com/', { + reqheaders: { + authorization: 'Basic MTIzNDU6QUJDREU=', + }, + }) + .intercept(`/beta/audit/events/asdf`, 'GET') + .reply(200, { + id: 'asdf', + created_at: '2022-11-15T17:30:33', + }); + + const result = await client.getEvent('asdf'); + + expect(result).toEqual({ + id: 'asdf', + createdAt: '2022-11-15T17:30:33', + }); + expect(scope.isDone()).toBeTruthy(); + }); +}); diff --git a/packages/audit/lib/audit.ts b/packages/audit/lib/audit.ts new file mode 100644 index 00000000..6acacfc0 --- /dev/null +++ b/packages/audit/lib/audit.ts @@ -0,0 +1,40 @@ +import { AuthenticationType, Client } from '@vonage/server-client'; +import { AuditParams, AuditEvent, AuditEventListResponse } from './types'; + +export class Audit extends Client { + protected BASE_PATH = 'beta'; + protected authType = AuthenticationType.BASIC; + + async *getEvents(params: AuditParams): AsyncGenerator { + let totalPages = 0; + let page = params?.page || 1; + do { + try { + const resp = await this.sendGetRequest( + `${this.config.apiHost}/${this.BASE_PATH}/audit/events`, + Client.transformers.snakeCaseObjectKeys(params), + ); + + const events = (resp.data?._embedded.events || []).map( + (event) => + Client.transformers.camelCaseObjectKeys(event, true), + ); + + totalPages = resp.data?.page?.totalPages || 0; + + yield* events; + page++; + } catch (error) { + // TODO Logging + return; + } + } while (page <= totalPages); + } + + public async getEvent(eventId: string): Promise { + const resp = await this.sendGetRequest( + `${this.config.apiHost}/${this.BASE_PATH}/audit/events/${eventId}`, + ); + return Client.transformers.camelCaseObjectKeys(resp.data, true); + } +} diff --git a/packages/audit/lib/enums.ts b/packages/audit/lib/enums.ts new file mode 100644 index 00000000..862c1e92 --- /dev/null +++ b/packages/audit/lib/enums.ts @@ -0,0 +1,29 @@ +export enum AuditEventTypes { + 'USER_STATUS' = 'USER_STATUS', + 'USER_UPDATE' = 'USER_UPDATE', + 'USER_BILLING_UPDATE' = 'USER_BILLING_UPDATE', + 'USER_CREATE' = 'USER_CREATE', + 'USER_LOGIN' = 'USER_LOGIN', + 'USER_LOGOUT' = 'USER_LOGOUT', + 'USER_PRODUCT_SEARCH' = 'USER_PRODUCT_SEARCH', + 'USER_API_KEYS_UPDATE' = 'USER_API_KEYS_UPDATE', + 'ACCOUNT_SECRET_DELETE' = 'ACCOUNT_SECRET_DELETE', + 'ACCOUNT_SECRET_CREATE' = 'ACCOUNT_SECRET_CREATE', + 'ACCOUNT_UPDATE_SPAMMER' = 'ACCOUNT_UPDATE_SPAMMER', + 'ACCOUNT_UPDATE_SETTINGS_API' = 'ACCOUNT_UPDATE_SETTINGS_API', + 'NUMBER_ASSIGN' = 'NUMBER_ASSIGN', + 'NUMBER_UPDATED' = 'NUMBER_UPDATED', + 'NUMBER_RELEASE' = 'NUMBER_RELEASE', + 'NUMBER_LINKED' = 'NUMBER_LINKED', + 'NUMBER_UNLINKED' = 'NUMBER_UNLINKED', + 'APP_CREATE' = 'APP_CREATE', + 'APP_UPDATE' = 'APP_UPDATE', + 'APP_DELETE' = 'APP_DELETE', + 'APP_DISABLE' = 'APP_DISABLE', + 'APP_ENABLE' = 'APP_ENABLE', + 'IP_WHITELIST_CREATE' = 'IP_WHITELIST_CREATE', + 'IP_WHITELIST_DELETE' = 'IP_WHITELIST_DELETE', + 'AUTORELOAD_ENABLE' = 'AUTORELOAD_ENABLE', + 'AUTORELOAD_UPDATE' = 'AUTORELOAD_UPDATE', + 'AUTORELOAD_DISABLE' = 'AUTORELOAD_DISABLE', +} diff --git a/packages/audit/lib/index.ts b/packages/audit/lib/index.ts new file mode 100644 index 00000000..4a4d5d03 --- /dev/null +++ b/packages/audit/lib/index.ts @@ -0,0 +1,5 @@ +export { Audit } from './audit'; + +export * from './enums'; + +export * from './types'; diff --git a/packages/audit/lib/types.ts b/packages/audit/lib/types.ts new file mode 100644 index 00000000..b769cc56 --- /dev/null +++ b/packages/audit/lib/types.ts @@ -0,0 +1,42 @@ +import { VetchResponse, VetchOptions } from '@vonage/vetch'; +import { AuditEventTypes } from './enums'; +import { APILinks } from '@vonage/server-client'; + +export type AuditResponse = VetchResponse + +export type AuditParams = { + eventType?: AuditEventTypes + dateFrom?: string + dateTo?: string + searchText?: string + page?: number + size?: number +} + +export type AuditEvent = { + id: string + eventType: AuditEventTypes + createdAt: string + userEmail?: string + userId?: string + accountId: string + source: string + sourceDescription: string + sourceCountry: string + context?: object +} + +export interface AuditEventPage { + size: number + totalElements: number + totalPages: number + number: number +} + +export interface AuditEventListResponse { + _embedded?: { + events: AuditEvent[] + } + _links: APILinks + page: AuditEventPage +} diff --git a/packages/audit/package.json b/packages/audit/package.json new file mode 100644 index 00000000..3e0625b8 --- /dev/null +++ b/packages/audit/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vonage/audit", + "version": "1.0.0", + "description": "Vonage Audit SDK Package", + "homepage": "https://github.com/vonage/vonage-node-sdk/tree/master/packages/numbers#readme", + "bugs": { + "url": "https://github.com/Vonage/vonage-node-sdk/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Vonage/vonage-node-sdk.git" + }, + "license": "Apache-2.0", + "author": "Kelly J Andrews ", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "directories": { + "lib": "dist", + "test": "__tests__" + }, + "files": [ + "/dist" + ], + "scripts": { + "build": "npm run clean && npm run compile", + "clean": "npx shx rm -rf dist tsconfig.tsbuildinfo", + "compile": "npx tsc --build --verbose" + }, + "dependencies": { + "@vonage/auth": "^1.0.5", + "@vonage/server-client": "^1.0.8", + "@vonage/vetch": "^1.0.6" + }, + "devDependencies": { + "nock": "^13.2.9" + }, + "publishConfig": { + "directory": "dist" + }, + "gitHead": "328f18e5c8a458cb4d06d7955ec2399a6ce6f5d8" +} diff --git a/packages/audit/tsconfig.json b/packages/audit/tsconfig.json new file mode 100644 index 00000000..e9b438f7 --- /dev/null +++ b/packages/audit/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + + "compilerOptions": { + "rootDir": "lib", + "outDir": "dist" + }, + + "exclude": [ + "__tests__", + "jest.config.js", + "dist" + ], + + "references": [ + { "path": "../auth" }, + { "path": "../server-client" }, + { "path": "../vetch" } + ], + + "ts-node": { + "esm": true + } +} diff --git a/packages/server-client/lib/index.ts b/packages/server-client/lib/index.ts index f8b61b58..f15d4993 100644 --- a/packages/server-client/lib/index.ts +++ b/packages/server-client/lib/index.ts @@ -1,2 +1,3 @@ -export { Client } from './client' -export { AuthenticationType } from './enums/AuthenticationType' +export { Client } from './client'; +export { AuthenticationType } from './enums/AuthenticationType'; +export { APILink, APILinks } from './types'; diff --git a/packages/server-client/lib/types.ts b/packages/server-client/lib/types.ts new file mode 100644 index 00000000..b37b88da --- /dev/null +++ b/packages/server-client/lib/types.ts @@ -0,0 +1,12 @@ +export type APILink = { + href: string + // TODO Add more from RFC 5988? +} + +export type APILinks = { + _links: { + self: APILink + next?: APILink + prev?: APILink + } +}