diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8b808bb --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +export MAILTRAP_ACCOUNT_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_id" +export MAILTRAP_ORGANIZATION_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_id" +export MAILTRAP_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_api_token" +export MAILTRAP_ORGANIZATION_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_api_token" diff --git a/.gitignore b/.gitignore index 3b74a4a..2900538 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist node_modules yarn-error.log coverage +.idea/ diff --git a/src/__tests__/lib/api/General.test.ts b/src/__tests__/lib/api/General.test.ts index 74cb18f..51928e2 100644 --- a/src/__tests__/lib/api/General.test.ts +++ b/src/__tests__/lib/api/General.test.ts @@ -14,6 +14,7 @@ describe("lib/api/General: ", () => { expect(generalAPI).toHaveProperty("accounts"); expect(generalAPI).toHaveProperty("permissions"); expect(generalAPI).toHaveProperty("billing"); + expect(generalAPI).toHaveProperty("apiTokens"); }); it("lazily instantiates account-specific APIs via getters when accountId is provided.", () => { @@ -21,6 +22,7 @@ describe("lib/api/General: ", () => { expect(generalAPI.permissions).toBeDefined(); expect(generalAPI.billing).toBeDefined(); expect(generalAPI.accounts).toBeDefined(); + expect(generalAPI.apiTokens).toBeDefined(); }); }); @@ -67,6 +69,15 @@ describe("lib/api/General: ", () => { "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." ); }); + + it("throws error when accessing apiTokens without accountId.", () => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + generalAPI.apiTokens; + }).toThrow( + "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." + ); + }); }); describe("account discovery functionality: ", () => { diff --git a/src/__tests__/lib/api/Organizations.test.ts b/src/__tests__/lib/api/Organizations.test.ts new file mode 100644 index 0000000..c32a445 --- /dev/null +++ b/src/__tests__/lib/api/Organizations.test.ts @@ -0,0 +1,18 @@ +import axios from "axios"; + +import OrganizationsBaseAPI from "../../../lib/api/Organizations"; + +describe("lib/api/Organizations: ", () => { + const organizationId = 1001; + const organizationsAPI = new OrganizationsBaseAPI(axios, organizationId); + + describe("class OrganizationsBaseAPI(): ", () => { + describe("init: ", () => { + it("exposes subAccounts resource.", () => { + expect(organizationsAPI).toHaveProperty("subAccounts"); + expect(typeof organizationsAPI.subAccounts.getList).toBe("function"); + expect(typeof organizationsAPI.subAccounts.create).toBe("function"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/Webhooks.test.ts b/src/__tests__/lib/api/Webhooks.test.ts new file mode 100644 index 0000000..52d248a --- /dev/null +++ b/src/__tests__/lib/api/Webhooks.test.ts @@ -0,0 +1,20 @@ +import axios from "axios"; + +import WebhooksBaseAPI from "../../../lib/api/Webhooks"; + +describe("lib/api/Webhooks: ", () => { + const accountId = 100; + const webhooksAPI = new WebhooksBaseAPI(axios, accountId); + + describe("class WebhooksBaseAPI(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(webhooksAPI).toHaveProperty("getList"); + expect(webhooksAPI).toHaveProperty("create"); + expect(webhooksAPI).toHaveProperty("get"); + expect(webhooksAPI).toHaveProperty("update"); + expect(webhooksAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/ApiTokens.test.ts b/src/__tests__/lib/api/resources/ApiTokens.test.ts new file mode 100644 index 0000000..bc127f5 --- /dev/null +++ b/src/__tests__/lib/api/resources/ApiTokens.test.ts @@ -0,0 +1,273 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ApiTokensApi from "../../../../lib/api/resources/ApiTokens"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/ApiTokens: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const apiTokensAPI = new ApiTokensApi(axios, accountId); + + describe("class ApiTokensApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(apiTokensAPI).toHaveProperty("getList"); + expect(apiTokensAPI).toHaveProperty("create"); + expect(apiTokensAPI).toHaveProperty("get"); + expect(apiTokensAPI).toHaveProperty("reset"); + expect(apiTokensAPI).toHaveProperty("delete"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + const responseData = [ + { + id: 12345, + name: "My API Token", + last_4_digits: "x7k9", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + }, + ]; + + it("gets the list of API tokens.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await apiTokensAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("create(): ", () => { + const params = { + name: "My API Token", + resources: [ + { + resource_type: "account" as const, + resource_id: 3229, + access_level: 100 as const, + }, + ], + }; + + const responseData = { + id: 12345, + name: "My API Token", + last_4_digits: "x7k9", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + token: "a1b2c3d4e5f6", + }; + + it("creates an API token and returns the full token value.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; + + expect.assertions(3); + + mock.onPost(endpoint, params).reply(200, responseData); + const result = await apiTokensAPI.create(params); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual(params); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.create(params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("get(): ", () => { + const tokenId = 12345; + const responseData = { + id: tokenId, + name: "My API Token", + last_4_digits: "x7k9", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + }; + + it("gets an API token by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await apiTokensAPI.get(tokenId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.get(tokenId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("reset(): ", () => { + const tokenId = 12345; + const responseData = { + id: tokenId, + name: "My API Token", + last_4_digits: "p3q4", + created_by: "user@example.com", + expires_at: null, + resources: [ + { + resource_type: "account", + resource_id: 3229, + access_level: 100, + }, + ], + token: "newtokenvalue", + }; + + it("resets an API token and returns the new token value.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}/reset`; + + expect.assertions(2); + + mock.onPost(endpoint).reply(200, responseData); + const result = await apiTokensAPI.reset(tokenId); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.reset(tokenId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("delete(): ", () => { + const tokenId = 12345; + + it("deletes an API token by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens/${tokenId}`; + + expect.assertions(1); + + mock.onDelete(endpoint).reply(204); + await apiTokensAPI.delete(tokenId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await apiTokensAPI.delete(tokenId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/SubAccounts.test.ts b/src/__tests__/lib/api/resources/SubAccounts.test.ts new file mode 100644 index 0000000..00bdddb --- /dev/null +++ b/src/__tests__/lib/api/resources/SubAccounts.test.ts @@ -0,0 +1,111 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import SubAccountsApi from "../../../../lib/api/resources/SubAccounts"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/SubAccounts: ", () => { + let mock: AxiosMockAdapter; + const organizationId = 1001; + const subAccountsAPI = new SubAccountsApi(axios, organizationId); + + describe("class SubAccountsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(subAccountsAPI).toHaveProperty("getList"); + expect(subAccountsAPI).toHaveProperty("create"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + const responseData = [ + { id: 12345, name: "Development Team Account" }, + { id: 12346, name: "QA Team Account" }, + ]; + + it("gets the list of sub accounts.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/organizations/${organizationId}/sub_accounts`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await subAccountsAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await subAccountsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("create(): ", () => { + const params = { name: "New Team Account" }; + const responseData = { id: 12347, name: "New Team Account" }; + + it("creates a sub account.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/organizations/${organizationId}/sub_accounts`; + const expectedBody = { account: params }; + + expect.assertions(3); + + mock.onPost(endpoint, expectedBody).reply(200, responseData); + const result = await subAccountsAPI.create(params); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual(expectedBody); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await subAccountsAPI.create(params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/Webhooks.test.ts b/src/__tests__/lib/api/resources/Webhooks.test.ts new file mode 100644 index 0000000..6af83f6 --- /dev/null +++ b/src/__tests__/lib/api/resources/Webhooks.test.ts @@ -0,0 +1,291 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import WebhooksApi from "../../../../lib/api/resources/Webhooks"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/Webhooks: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const webhooksAPI = new WebhooksApi(axios, accountId); + + describe("class WebhooksApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(webhooksAPI).toHaveProperty("getList"); + expect(webhooksAPI).toHaveProperty("create"); + expect(webhooksAPI).toHaveProperty("get"); + expect(webhooksAPI).toHaveProperty("update"); + expect(webhooksAPI).toHaveProperty("delete"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + const responseData = { + data: [ + { + id: 1, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce"], + }, + { + id: 2, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "audit_log", + payload_format: "json", + }, + ], + }; + + it("gets the list of webhooks.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await webhooksAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("create(): ", () => { + const params = { + url: "https://example.com/mailtrap/webhooks", + webhook_type: "email_sending" as const, + payload_format: "json" as const, + sending_stream: "transactional" as const, + event_types: ["delivery" as const, "bounce" as const], + domain_id: 435, + }; + + const responseData = { + data: { + id: 1, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce"], + signing_secret: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + }, + }; + + it("creates a webhook and returns the signing_secret.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks`; + const expectedBody = { webhook: params }; + + expect.assertions(3); + + mock.onPost(endpoint, expectedBody).reply(200, responseData); + const result = await webhooksAPI.create(params); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual(expectedBody); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.create(params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("get(): ", () => { + const webhookId = 1; + const responseData = { + data: { + id: webhookId, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce"], + }, + }; + + it("gets a webhook by id.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks/${webhookId}`; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, responseData); + const result = await webhooksAPI.get(webhookId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.get(webhookId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("update(): ", () => { + const webhookId = 1; + const params = { + active: false, + event_types: [ + "delivery" as const, + "bounce" as const, + "unsubscribe" as const, + ], + }; + + const responseData = { + data: { + id: webhookId, + url: "https://example.com/mailtrap/webhooks", + active: false, + webhook_type: "email_sending", + payload_format: "json", + sending_stream: "transactional", + domain_id: 435, + event_types: ["delivery", "bounce", "unsubscribe"], + }, + }; + + it("updates a webhook.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks/${webhookId}`; + const expectedBody = { webhook: params }; + + expect.assertions(3); + + mock.onPatch(endpoint, expectedBody).reply(200, responseData); + const result = await webhooksAPI.update(webhookId, params); + + expect(mock.history.patch[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.patch[0].data)).toEqual(expectedBody); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.update(webhookId, params); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("delete(): ", () => { + const webhookId = 1; + const responseData = { + data: { + id: webhookId, + url: "https://example.com/mailtrap/webhooks", + active: true, + webhook_type: "audit_log", + payload_format: "json", + }, + }; + + it("deletes a webhook and returns it.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks/${webhookId}`; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(200, responseData); + const result = await webhooksAPI.delete(webhookId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + expect(result).toEqual(responseData); + }); + + it("fails with error.", async () => { + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + try { + await webhooksAPI.delete(webhookId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/config/index.ts b/src/config/index.ts index e03712b..6f5d0e1 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -9,6 +9,8 @@ export default { TEST_INBOX_ID_MISSING: "testInboxId is missing, testing API will not work.", ACCOUNT_ID_MISSING: "accountId is missing, please provide a valid accountId.", + ORGANIZATION_ID_MISSING: + "organizationId is missing, please provide a valid organizationId.", BULK_SANDBOX_INCOMPATIBLE: "Bulk mode is not applicable for sandbox API.", }, CLIENT_SETTINGS: { diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index e5037c1..9428b5a 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -18,8 +18,10 @@ import GeneralAPI from "./api/General"; import SendingDomainsBaseAPI from "./api/SendingDomains"; import StatsBaseAPI from "./api/Stats"; import SuppressionsBaseAPI from "./api/Suppressions"; +import OrganizationsBaseAPI from "./api/Organizations"; import TemplatesBaseAPI from "./api/Templates"; import TestingAPI from "./api/Testing"; +import WebhooksBaseAPI from "./api/Webhooks"; import CONFIG from "../config"; @@ -40,8 +42,12 @@ const { BULK_ENDPOINT, USER_AGENT, } = CLIENT_SETTINGS; -const { ACCOUNT_ID_MISSING, BULK_SANDBOX_INCOMPATIBLE, TEST_INBOX_ID_MISSING } = - ERRORS; +const { + ACCOUNT_ID_MISSING, + BULK_SANDBOX_INCOMPATIBLE, + ORGANIZATION_ID_MISSING, + TEST_INBOX_ID_MISSING, +} = ERRORS; /** * Mailtrap client class. Initializes instance with available methods. @@ -53,6 +59,8 @@ export default class MailtrapClient { private accountId?: number; + private organizationId?: number; + private bulk: boolean; private sandbox: boolean; @@ -64,6 +72,7 @@ export default class MailtrapClient { token, testInboxId, accountId, + organizationId, bulk = false, sandbox = false, userAgent, @@ -88,6 +97,7 @@ export default class MailtrapClient { this.testInboxId = testInboxId; this.accountId = accountId; + this.organizationId = organizationId; this.bulk = bulk; this.sandbox = sandbox; } @@ -102,6 +112,16 @@ export default class MailtrapClient { return this.accountId; } + /** + * Validates that organization ID is present, throws MailtrapError if missing. + */ + private validateOrganizationIdPresence(): number { + if (!this.organizationId) { + throw new MailtrapError(ORGANIZATION_ID_MISSING); + } + return this.organizationId; + } + /** * Validates that test inbox ID is present, throws MailtrapError if missing. */ @@ -215,6 +235,22 @@ export default class MailtrapClient { return new EmailLogsBaseAPI(this.axios, accountId); } + /** + * Getter for Webhooks API. + */ + get webhooks() { + const accountId = this.validateAccountIdPresence(); + return new WebhooksBaseAPI(this.axios, accountId); + } + + /** + * Getter for Organizations API. Requires `organizationId` in config. + */ + get organizations() { + const organizationId = this.validateOrganizationIdPresence(); + return new OrganizationsBaseAPI(this.axios, organizationId); + } + /** * Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously, * then reject with Mailtrap Error. diff --git a/src/lib/api/General.ts b/src/lib/api/General.ts index 2ac485d..0a20e07 100644 --- a/src/lib/api/General.ts +++ b/src/lib/api/General.ts @@ -2,6 +2,7 @@ import { AxiosInstance } from "axios"; import AccountAccessesApi from "./resources/AccountAccesses"; import AccountsApi from "./resources/Accounts"; +import ApiTokensApi from "./resources/ApiTokens"; import BillingApi from "./resources/Billing"; import PermissionsApi from "./resources/Permissions"; @@ -18,6 +19,8 @@ export default class GeneralAPI { private billingInstance: BillingApi | null = null; + private apiTokensInstance: ApiTokensApi | null = null; + constructor(client: AxiosInstance, accountId?: number) { this.client = client; this.accountId = accountId ?? null; @@ -83,4 +86,16 @@ export default class GeneralAPI { return this.billingInstance; } + + /** + * Singleton getter for API Tokens API. + */ + public get apiTokens(): ApiTokensApi { + if (this.apiTokensInstance === null) { + const accountId = this.checkAccountIdPresence(); + this.apiTokensInstance = new ApiTokensApi(this.client, accountId); + } + + return this.apiTokensInstance; + } } diff --git a/src/lib/api/Organizations.ts b/src/lib/api/Organizations.ts new file mode 100644 index 0000000..b0a4884 --- /dev/null +++ b/src/lib/api/Organizations.ts @@ -0,0 +1,11 @@ +import { AxiosInstance } from "axios"; + +import SubAccountsApi from "./resources/SubAccounts"; + +export default class OrganizationsBaseAPI { + public subAccounts: SubAccountsApi; + + constructor(client: AxiosInstance, organizationId: number) { + this.subAccounts = new SubAccountsApi(client, organizationId); + } +} diff --git a/src/lib/api/Webhooks.ts b/src/lib/api/Webhooks.ts new file mode 100644 index 0000000..8a6be01 --- /dev/null +++ b/src/lib/api/Webhooks.ts @@ -0,0 +1,27 @@ +import { AxiosInstance } from "axios"; + +import WebhooksApi from "./resources/Webhooks"; + +export default class WebhooksBaseAPI { + private client: AxiosInstance; + + public getList: WebhooksApi["getList"]; + + public create: WebhooksApi["create"]; + + public get: WebhooksApi["get"]; + + public update: WebhooksApi["update"]; + + public delete: WebhooksApi["delete"]; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + const webhooks = new WebhooksApi(this.client, accountId); + this.getList = webhooks.getList.bind(webhooks); + this.create = webhooks.create.bind(webhooks); + this.get = webhooks.get.bind(webhooks); + this.update = webhooks.update.bind(webhooks); + this.delete = webhooks.delete.bind(webhooks); + } +} diff --git a/src/lib/api/resources/ApiTokens.ts b/src/lib/api/resources/ApiTokens.ts new file mode 100644 index 0000000..205c30e --- /dev/null +++ b/src/lib/api/resources/ApiTokens.ts @@ -0,0 +1,72 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + ApiToken, + ApiTokenWithToken, + CreateApiTokenRequest, +} from "../../../types/api/api-tokens"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ApiTokensApi { + private client: AxiosInstance; + + private apiTokensURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.apiTokensURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/api_tokens`; + } + + /** + * List all API tokens visible to the current API token. + * The full token value is never returned here — only `last_4_digits`. + */ + public async getList() { + const url = this.apiTokensURL; + + return this.client.get(url); + } + + /** + * Create a new API token for the account with the given name and resource permissions. + * The full token value is returned only in the response of this call — store it securely. + */ + public async create(params: CreateApiTokenRequest) { + const url = this.apiTokensURL; + + return this.client.post(url, params); + } + + /** + * Get a single API token by ID. The full token value is not returned — + * only `last_4_digits` is available outside of create/reset responses. + */ + public async get(id: number) { + const url = `${this.apiTokensURL}/${id}`; + + return this.client.get(url); + } + + /** + * Reset an API token: expires the existing token and returns a new one with + * the same permissions. The new token value is returned only in this response — + * store it securely. Only tokens that have not already been reset can be reset. + */ + public async reset(id: number) { + const url = `${this.apiTokensURL}/${id}/reset`; + + return this.client.post(url); + } + + /** + * Permanently delete an API token by ID. + */ + public async delete(id: number) { + const url = `${this.apiTokensURL}/${id}`; + + return this.client.delete(url); + } +} diff --git a/src/lib/api/resources/SubAccounts.ts b/src/lib/api/resources/SubAccounts.ts new file mode 100644 index 0000000..da5bb57 --- /dev/null +++ b/src/lib/api/resources/SubAccounts.ts @@ -0,0 +1,42 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + CreateSubAccountParams, + SubAccount, +} from "../../../types/api/sub-accounts"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class SubAccountsApi { + private client: AxiosInstance; + + private subAccountsURL: string; + + constructor(client: AxiosInstance, organizationId: number) { + this.client = client; + this.subAccountsURL = `${GENERAL_ENDPOINT}/api/organizations/${organizationId}/sub_accounts`; + } + + /** + * Get a list of sub accounts for the organization. Requires sub-account + * management permissions. + */ + public async getList() { + const url = this.subAccountsURL; + + return this.client.get(url); + } + + /** + * Create a new sub account under the organization. Requires sub-account + * management permissions. + */ + public async create(params: CreateSubAccountParams) { + const url = this.subAccountsURL; + const data = { account: params }; + + return this.client.post(url, data); + } +} diff --git a/src/lib/api/resources/Webhooks.ts b/src/lib/api/resources/Webhooks.ts new file mode 100644 index 0000000..d636a65 --- /dev/null +++ b/src/lib/api/resources/Webhooks.ts @@ -0,0 +1,87 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + CreateWebhookParams, + CreateWebhookResponse, + DeleteWebhookResponse, + GetWebhookResponse, + ListWebhooksResponse, + UpdateWebhookParams, + UpdateWebhookResponse, +} from "../../../types/api/webhooks"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class WebhooksApi { + private client: AxiosInstance; + + private webhooksURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.webhooksURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/webhooks`; + } + + /** + * Returns all webhooks for the account. + */ + public async getList() { + const url = this.webhooksURL; + + return this.client.get(url); + } + + /** + * Create a new webhook for the account. The response includes a + * `signing_secret` that is used to verify webhook payload signatures — + * it is only returned on creation, store it securely. + */ + public async create(params: CreateWebhookParams) { + const url = this.webhooksURL; + const data = { webhook: params }; + + return this.client.post( + url, + data + ); + } + + /** + * Get a single webhook by ID. The `signing_secret` is not returned here — + * it is only available in the create response. + */ + public async get(id: number) { + const url = `${this.webhooksURL}/${id}`; + + return this.client.get(url); + } + + /** + * Update an existing webhook. Only `url`, `active`, `payload_format`, and + * `event_types` can be changed; `webhook_type`, `sending_stream`, and + * `domain_id` are immutable after creation. + */ + public async update(id: number, params: UpdateWebhookParams) { + const url = `${this.webhooksURL}/${id}`; + const data = { webhook: params }; + + return this.client.patch( + url, + data + ); + } + + /** + * Permanently delete a webhook by ID. The deleted webhook is returned in + * the response. + */ + public async delete(id: number) { + const url = `${this.webhooksURL}/${id}`; + + return this.client.delete( + url + ); + } +} diff --git a/src/types/api/api-tokens.ts b/src/types/api/api-tokens.ts new file mode 100644 index 0000000..472b0f2 --- /dev/null +++ b/src/types/api/api-tokens.ts @@ -0,0 +1,33 @@ +export type ResourceType = "account" | "project" | "inbox" | "sending_domain"; + +export type AccessLevel = 10 | 100; + +export type ResourcePermissionInput = { + resource_type: ResourceType; + resource_id: number | string; + access_level: AccessLevel; +}; + +export type ResourcePermission = { + resource_type: ResourceType; + resource_id: number | string; + access_level: AccessLevel; +}; + +export type CreateApiTokenRequest = { + name: string; + resources?: ResourcePermissionInput[]; +}; + +export type ApiToken = { + id: number; + name: string; + last_4_digits: string; + created_by: string; + expires_at: string | null; + resources: ResourcePermission[]; +}; + +export type ApiTokenWithToken = ApiToken & { + token: string; +}; diff --git a/src/types/api/sub-accounts.ts b/src/types/api/sub-accounts.ts new file mode 100644 index 0000000..83e7a7d --- /dev/null +++ b/src/types/api/sub-accounts.ts @@ -0,0 +1,8 @@ +export type SubAccount = { + id: number; + name: string; +}; + +export type CreateSubAccountParams = { + name: string; +}; diff --git a/src/types/api/webhooks.ts b/src/types/api/webhooks.ts new file mode 100644 index 0000000..d8a1919 --- /dev/null +++ b/src/types/api/webhooks.ts @@ -0,0 +1,68 @@ +export type WebhookType = "email_sending" | "audit_log"; + +export type PayloadFormat = "json" | "jsonlines"; + +export type SendingStream = "transactional" | "bulk"; + +export type WebhookEventType = + | "delivery" + | "soft_bounce" + | "bounce" + | "suspension" + | "unsubscribe" + | "open" + | "spam_complaint" + | "click" + | "reject"; + +export type Webhook = { + id: number; + url: string; + active: boolean; + webhook_type: WebhookType; + payload_format: PayloadFormat; + sending_stream?: SendingStream | null; + domain_id?: number | null; + event_types?: WebhookEventType[]; +}; + +export type ListWebhooksResponse = { + data: Webhook[]; +}; + +export type GetWebhookResponse = { + data: Webhook; +}; + +export type CreateWebhookParams = { + url: string; + webhook_type: WebhookType; + active?: boolean; + payload_format?: PayloadFormat; + sending_stream?: SendingStream; + event_types?: WebhookEventType[]; + domain_id?: number; +}; + +export type WebhookWithSigningSecret = Webhook & { + signing_secret: string; +}; + +export type CreateWebhookResponse = { + data: WebhookWithSigningSecret; +}; + +export type UpdateWebhookParams = { + url?: string; + active?: boolean; + payload_format?: PayloadFormat; + event_types?: WebhookEventType[]; +}; + +export type UpdateWebhookResponse = { + data: Webhook; +}; + +export type DeleteWebhookResponse = { + data: Webhook; +}; diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 649c25d..ecce670 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -72,6 +72,7 @@ export type MailtrapClientConfig = { token: string; testInboxId?: number; accountId?: number; + organizationId?: number; bulk?: boolean; sandbox?: boolean; userAgent?: string;