diff --git a/src/domainrproxy/domainrproxy.controller.ts b/src/domainrproxy/domainrproxy.controller.ts index 92e00f0..7c9d512 100644 --- a/src/domainrproxy/domainrproxy.controller.ts +++ b/src/domainrproxy/domainrproxy.controller.ts @@ -1,19 +1,30 @@ import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { DomainrproxyService } from './domainrproxy.service'; import { AuthGuard } from 'src/auth/auth.guard'; +import { + ApiBearerAuth, + ApiOkResponse, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { DomainrParsedStatusResult } from './domainrproxy.service'; +@ApiTags('Domainr') @Controller('domainrproxy') export class DomainrproxyController { constructor(private readonly proxyService: DomainrproxyService) {} @UseGuards(AuthGuard) @Get(':domain') + @ApiOkResponse({ type: DomainrParsedStatusResult }) + @ApiBearerAuth() queryDomain(@Param('domain') domain: string) { return this.proxyService.queryForDomain(domain); } @UseGuards(AuthGuard) @Post('search') + @ApiBearerAuth() search(@Body() body: { query: string }) { return this.proxyService.search(body.query); } diff --git a/src/domainrproxy/domainrproxy.service.ts b/src/domainrproxy/domainrproxy.service.ts index 20330fa..4b5fa97 100644 --- a/src/domainrproxy/domainrproxy.service.ts +++ b/src/domainrproxy/domainrproxy.service.ts @@ -1,12 +1,205 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { ApiProperty } from '@nestjs/swagger'; import axios from 'axios'; +import { any } from 'ramda'; +export interface DomainrSearchResult { + domain: string; + host: string; + subdomain: string; + path: string; + registerURL: string; +} + +const statusStrings = [ + /** + * Unknown status, usually resulting from an error or + * misconfiguration. + * + * Example: foobar.llp + */ + 'unknown', + /** + * The domain is not present in DNS. + * + * Example: hello.edu + */ + 'undelgated', + /** + * Available for new registration. + * + * Example: nonexistent.xyz + */ + 'inactive', + /** + * TLD not yet in the root zone file + * + * Example: heartb.eat + */ + 'pending', + /** + * Disallowed by the registry, ICANN, or other (wrong + * script, etc.). + * + * Example: a.mobi + */ + 'disallowed', + /** + * Claimed or reserved by some party (not available for new + * registration). + * + * Example: fedex.name + */ + 'claimed', + /** + * Explicitly reserved by ICANN, the registry, or another + * party. + * + * Example: s.work + */ + 'reserved', + /** + * Domains Protected Marks List, reserved for trademark + * holders. + * + * Example: google.bargains + */ + 'dpml', + /** + * Technically invalid, e.g. too long or too short. + * Example: a domain longer than 64 characters + */ + 'invalid', + /** + * Registered, but possibly available via the aftermarket. + * + * Example: vertical.coffee + */ + 'active', + /** + * Active and parked, possibly available via the aftermarket. + * + * Example: wi.io + */ + 'parked', + /** + * Explicitly marketed as for sale via the aftermarket. + * + * Example: wi.io + */ + 'marketed', + /** + * e.g. in the Redemption Grace Period, and possibly available + * via a backorder service. Not guaranteed to be present for + * all expiring domains. + * + * Example: An expiring domain. + */ + 'expiring', + /** + * e.g. in the Pending Delete phase, and possibly available + * via a backorder service. Not guaranteed to be present for + * all deleting domains. + * + * Example: A expired domain pending removal + * from the registry. + */ + 'deleting', + /** + * e.g. via the BuyDomains service. + * + * Example: An aftermarket domain with an explicit price. + */ + 'priced', + /** + * e.g. in the Afternic inventory. + * + * Example: An aftermarket domain available for + * fast-transfer. + */ + 'transferable', + /** + * Premium domain name for sale by the registry. + * + * Example: ace.pizza + */ + 'premium', + /** + * A public suffix according to publicsuffix.org. + * + * Example: blogspot.com + */ + 'suffix', + /** + * A zone (domain extension) in the Domainr database. + * + * Example: .co.uk + */ + 'zone', + /** + * A top-level domain. + * + * Example: .com + */ + 'tld', +] as const; + +export type Status = (typeof statusStrings)[number]; + +export class DomainrStatusResult { + @ApiProperty() + public readonly domain: string; + @ApiProperty() + public readonly zone: string; + @ApiProperty() + public readonly status: string; + /** + * Deprecated + * @deprecated + */ + @ApiProperty() + public readonly summary: string; +} + +export class DomainrParsedStatusResult extends DomainrStatusResult { + @ApiProperty() + public readonly statuses: Status[]; +} + +export class DomainrStatusResponse { + status: DomainrStatusResult[]; +} @Injectable() export class DomainrproxyService { private readonly logger = new Logger(DomainrproxyService.name); constructor(private readonly configService: ConfigService) {} + canRegister = (status: Status) => (['inactive'] as Status[]).includes(status); + + canPurchase = (status: Status) => + ( + [ + 'inactive', + 'parked', + 'marketed', + 'priced', + 'transferable', + 'premium', + ] as Status[] + ).includes(status); + + parseStatusResult = (incoming: DomainrStatusResult) => { + const statuses: Status[] = incoming.status.split(' ') as Status[]; + + return { + ...incoming, + statuses, + canRegister: any(this.canRegister, statuses), + canPurchase: any(this.canPurchase, statuses), + }; + }; + queryForDomain = async (domain: string) => { this.logger.verbose(`Handling domainr query for domain ${domain}`); const options = { @@ -22,8 +215,8 @@ export class DomainrproxyService { }, }; - const result = await axios.request(options); - return result.data; + const result = await axios.request(options); + return result.data.status.map(this.parseStatusResult); }; search = async (query: string) => { @@ -34,6 +227,7 @@ export class DomainrproxyService { params: { query, defaults: 'com,net,org,io,sh,wtf', + registrar: 'namecheap.com', 'mashape-key': this.configService.get('rapidApiKey'), }, headers: {