From 70026cb11dcede5f9cfdf8ad986d910b0ece97be Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sat, 5 Apr 2025 06:48:03 +1100 Subject: [PATCH] domain apis (#146) * fix: create domain api * add domain apis * fix url --------- Co-authored-by: harshsbhat --- .../api-reference/domains/create-domain.mdx | 3 + .../api-reference/domains/verify-domain.mdx | 3 + apps/docs/api-reference/openapi.json | 168 ++++++++++++++++++ apps/docs/mint.json | 4 +- apps/web/src/lib/zod/domain-schema.ts | 14 +- .../public-api/api/domains/create-domain.ts | 45 +++++ .../public-api/api/domains/verify-domain.ts | 49 +++++ apps/web/src/server/public-api/index.ts | 4 + packages/sdk/package.json | 2 +- packages/sdk/src/domain.ts | 57 ++++++ packages/sdk/types/schema.d.ts | 78 ++++++++ 11 files changed, 415 insertions(+), 12 deletions(-) create mode 100644 apps/docs/api-reference/domains/create-domain.mdx create mode 100644 apps/docs/api-reference/domains/verify-domain.mdx create mode 100644 apps/web/src/server/public-api/api/domains/create-domain.ts create mode 100644 apps/web/src/server/public-api/api/domains/verify-domain.ts create mode 100644 packages/sdk/src/domain.ts diff --git a/apps/docs/api-reference/domains/create-domain.mdx b/apps/docs/api-reference/domains/create-domain.mdx new file mode 100644 index 0000000..81f2745 --- /dev/null +++ b/apps/docs/api-reference/domains/create-domain.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/domains +--- diff --git a/apps/docs/api-reference/domains/verify-domain.mdx b/apps/docs/api-reference/domains/verify-domain.mdx new file mode 100644 index 0000000..05b40dd --- /dev/null +++ b/apps/docs/api-reference/domains/verify-domain.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /v1/domains/{id}/verify +--- diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index a665b73..8632d2b 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -85,6 +85,22 @@ }, "updatedAt": { "type": "string" + }, + "dmarcAdded": { + "type": "boolean", + "default": false + }, + "isVerifying": { + "type": "boolean", + "default": false + }, + "errorMessage": { + "type": "string", + "nullable": true + }, + "subdomain": { + "type": "string", + "nullable": true } }, "required": [ @@ -102,6 +118,158 @@ } } } + }, + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "required": [ + "name", + "region" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Create a new domain", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the domain", + "example": 1 + }, + "name": { + "type": "string", + "description": "The name of the domain", + "example": "example.com" + }, + "teamId": { + "type": "number", + "description": "The ID of the team", + "example": 1 + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "region": { + "type": "string", + "default": "us-east-1" + }, + "clickTracking": { + "type": "boolean", + "default": false + }, + "openTracking": { + "type": "boolean", + "default": false + }, + "publicKey": { + "type": "string" + }, + "dkimStatus": { + "type": "string", + "nullable": true + }, + "spfDetails": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "dmarcAdded": { + "type": "boolean", + "default": false + }, + "isVerifying": { + "type": "boolean", + "default": false + }, + "errorMessage": { + "type": "string", + "nullable": true + }, + "subdomain": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "name", + "teamId", + "status", + "publicKey", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/domains/{id}/verify": { + "put": { + "parameters": [ + { + "schema": { + "type": "number", + "example": 1 + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Create a new domain", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + } + } + } + } } }, "/v1/emails/{emailId}": { diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 23f0dcc..9fcdd4d 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -96,7 +96,9 @@ { "group": "Domains", "pages": [ - "api-reference/domains/get-domain" + "api-reference/domains/get-domain", + "api-reference/domains/create-domain", + "api-reference/domains/verify-domain" ] } ], diff --git a/apps/web/src/lib/zod/domain-schema.ts b/apps/web/src/lib/zod/domain-schema.ts index e18c6dc..9a5c07b 100644 --- a/apps/web/src/lib/zod/domain-schema.ts +++ b/apps/web/src/lib/zod/domain-schema.ts @@ -18,14 +18,8 @@ export const DomainSchema = z.object({ spfDetails: z.string().optional().nullish(), createdAt: z.string(), updatedAt: z.string(), -}); - -export const CreateDomainSchema = DomainSchema.omit({ - id: true, - createdAt: true, - updatedAt: true, - teamId: true, - publicKey: true, - dkimStatus: true, - spfDetails: true, + dmarcAdded: z.boolean().default(false), + isVerifying: z.boolean().default(false), + errorMessage: z.string().optional().nullish(), + subdomain: z.string().optional().nullish(), }); diff --git a/apps/web/src/server/public-api/api/domains/create-domain.ts b/apps/web/src/server/public-api/api/domains/create-domain.ts new file mode 100644 index 0000000..1c5d5d6 --- /dev/null +++ b/apps/web/src/server/public-api/api/domains/create-domain.ts @@ -0,0 +1,45 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { DomainSchema } from "~/lib/zod/domain-schema"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { createDomain as createDomainService } from "~/server/service/domain-service"; +import { getTeamFromToken } from "~/server/public-api/auth"; + +const route = createRoute({ + method: "post", + path: "/v1/domains", + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + name: z.string(), + region: z.string(), + }), + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: DomainSchema, + }, + }, + description: "Create a new domain", + }, + }, +}); + +function createDomain(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = await getTeamFromToken(c); + const body = c.req.valid("json"); + const response = await createDomainService(team.id, body.name, body.region); + + return c.json(response); + }); +} + +export default createDomain; diff --git a/apps/web/src/server/public-api/api/domains/verify-domain.ts b/apps/web/src/server/public-api/api/domains/verify-domain.ts new file mode 100644 index 0000000..08a83c3 --- /dev/null +++ b/apps/web/src/server/public-api/api/domains/verify-domain.ts @@ -0,0 +1,49 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { getTeamFromToken } from "~/server/public-api/auth"; +import { db } from "~/server/db"; + +const route = createRoute({ + method: "put", + path: "/v1/domains/{id}/verify", + request: { + params: z.object({ + id: z.coerce.number().openapi({ + param: { + name: "id", + in: "path", + }, + example: 1, + }), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + message: z.string(), + }), + }, + }, + description: "Create a new domain", + }, + }, +}); + +function verifyDomain(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = await getTeamFromToken(c); + + await db.domain.update({ + where: { id: c.req.valid("param").id }, + data: { isVerifying: true }, + }); + + return c.json({ + message: "Domain verification started", + }); + }); +} + +export default verifyDomain; diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts index b982aa5..abe0bdb 100644 --- a/apps/web/src/server/public-api/index.ts +++ b/apps/web/src/server/public-api/index.ts @@ -9,12 +9,16 @@ import updateEmailScheduledAt from "./api/emails/update-email"; import cancelScheduledEmail from "./api/emails/cancel-email"; import getContacts from "./api/contacts/get-contacts"; import upsertContact from "./api/contacts/upsert-contact"; +import createDomain from "./api/domains/create-domain"; import deleteContact from "./api/contacts/delete-contact"; +import verifyDomain from "./api/domains/verify-domain"; export const app = getApp(); /**Domain related APIs */ getDomains(app); +createDomain(app); +verifyDomain(app); /**Email related APIs */ getEmail(app); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a5d1637..54f12e7 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "unsend", - "version": "1.4.0", + "version": "1.4.1", "description": "", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/domain.ts b/packages/sdk/src/domain.ts new file mode 100644 index 0000000..4a0187f --- /dev/null +++ b/packages/sdk/src/domain.ts @@ -0,0 +1,57 @@ +import { paths } from "../types/schema"; +import { ErrorResponse } from "../types"; +import { Unsend } from "./unsend"; + +type CreateDomainPayload = + paths["/v1/domains"]["post"]["requestBody"]["content"]["application/json"]; + +type CreateDomainResponse = { + data: CreateDomainResponseSuccess | null; + error: ErrorResponse | null; +}; + +type CreateDomainResponseSuccess = + paths["/v1/domains"]["post"]["responses"]["200"]["content"]["application/json"]; + +type GetDomainsResponse = { + data: GetDomainsResponseSuccess | null; + error: ErrorResponse | null; +}; + +type GetDomainsResponseSuccess = + paths["/v1/domains"]["get"]["responses"]["200"]["content"]["application/json"]; + +type VerifyDomainResponse = { + data: VerifyDomainResponseSuccess | null; + error: ErrorResponse | null; +}; + +type VerifyDomainResponseSuccess = + paths["/v1/domains/{id}/verify"]["put"]["responses"]["200"]["content"]["application/json"]; + +export class Domains { + constructor(private readonly unsend: Unsend) { + this.unsend = unsend; + } + + async list(): Promise { + const data = await this.unsend.get("/domains"); + return data; + } + + async create(payload: CreateDomainPayload): Promise { + const data = await this.unsend.post( + "/domains", + payload + ); + return data; + } + + async verify(id: number): Promise { + const data = await this.unsend.put( + `/domains/${id}/verify`, + {} + ); + return data; + } +} diff --git a/packages/sdk/types/schema.d.ts b/packages/sdk/types/schema.d.ts index 1912aef..46608af 100644 --- a/packages/sdk/types/schema.d.ts +++ b/packages/sdk/types/schema.d.ts @@ -40,11 +40,89 @@ export interface paths { spfDetails?: string | null; createdAt: string; updatedAt: string; + /** @default false */ + dmarcAdded?: boolean; + /** @default false */ + isVerifying?: boolean; + errorMessage?: string | null; + subdomain?: string | null; })[]; }; }; }; }; + post: { + requestBody: { + content: { + "application/json": { + name: string; + region: string; + }; + }; + }; + responses: { + /** @description Create a new domain */ + 200: { + content: { + "application/json": { + /** + * @description The ID of the domain + * @example 1 + */ + id: number; + /** + * @description The name of the domain + * @example example.com + */ + name: string; + /** + * @description The ID of the team + * @example 1 + */ + teamId: number; + /** @enum {string} */ + status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; + /** @default us-east-1 */ + region?: string; + /** @default false */ + clickTracking?: boolean; + /** @default false */ + openTracking?: boolean; + publicKey: string; + dkimStatus?: string | null; + spfDetails?: string | null; + createdAt: string; + updatedAt: string; + /** @default false */ + dmarcAdded?: boolean; + /** @default false */ + isVerifying?: boolean; + errorMessage?: string | null; + subdomain?: string | null; + }; + }; + }; + }; + }; + }; + "/v1/domains/{id}/verify": { + put: { + parameters: { + path: { + id: number; + }; + }; + responses: { + /** @description Create a new domain */ + 200: { + content: { + "application/json": { + message: string; + }; + }; + }; + }; + }; }; "/v1/emails/{emailId}": { get: {