-
-
Notifications
You must be signed in to change notification settings - Fork 685
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(server): implement website hosting module (#763)
- Loading branch information
Showing
12 changed files
with
329 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ | |
"apiextensions", | ||
"appid", | ||
"automount", | ||
"binded", | ||
"bodyparser", | ||
"bson", | ||
"buildah", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
import { PartialType } from '@nestjs/mapped-types' | ||
import { CreateWebsiteDto } from './create-website.dto' | ||
import { ApiProperty } from '@nestjs/swagger' | ||
import { IsNotEmpty, IsString } from 'class-validator' | ||
|
||
export class UpdateWebsiteDto extends PartialType(CreateWebsiteDto) {} | ||
export class BindCustomDomainDto { | ||
@ApiProperty() | ||
@IsNotEmpty() | ||
@IsString() | ||
domain: string | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { | ||
Controller, | ||
Get, | ||
Post, | ||
Body, | ||
Patch, | ||
Param, | ||
Delete, | ||
UseGuards, | ||
} from '@nestjs/common' | ||
import { WebsiteService } from './website.service' | ||
import { CreateWebsiteDto } from './dto/create-website.dto' | ||
import { BindCustomDomainDto } from './dto/update-website.dto' | ||
import { | ||
ApiBearerAuth, | ||
ApiOperation, | ||
ApiResponse, | ||
ApiTags, | ||
} from '@nestjs/swagger' | ||
import { JwtAuthGuard } from 'src/auth/jwt.auth.guard' | ||
import { ApplicationAuthGuard } from 'src/auth/application.auth.guard' | ||
import { ResponseUtil } from 'src/utils/response' | ||
|
||
@ApiTags('WebsiteHosting') | ||
@ApiBearerAuth('Authorization') | ||
@Controller('apps/:appid/websites') | ||
export class WebsiteController { | ||
constructor(private readonly websiteService: WebsiteService) {} | ||
|
||
/** | ||
* Create a new website | ||
* @param appid | ||
* @param dto | ||
* @param req | ||
* @returns | ||
*/ | ||
@ApiResponse({ type: ResponseUtil }) | ||
@ApiOperation({ summary: 'Create a new website' }) | ||
@UseGuards(JwtAuthGuard, ApplicationAuthGuard) | ||
@Post() | ||
async create(@Param('appid') appid: string, @Body() dto: CreateWebsiteDto) { | ||
const site = await this.websiteService.create(appid, dto) | ||
|
||
if (!site) { | ||
return ResponseUtil.error('failed to create website') | ||
} | ||
|
||
return ResponseUtil.ok(site) | ||
} | ||
|
||
/** | ||
* Get all websites of an app | ||
* @param req | ||
* @returns | ||
*/ | ||
@ApiResponse({ type: ResponseUtil }) | ||
@ApiOperation({ summary: 'Get all websites of an app' }) | ||
@UseGuards(JwtAuthGuard, ApplicationAuthGuard) | ||
@Get() | ||
async findAll(@Param('appid') appid: string) { | ||
const sites = await this.websiteService.findAll(appid) | ||
return ResponseUtil.ok(sites) | ||
} | ||
|
||
/** | ||
* Get a website hosting of an app | ||
* @param id | ||
* @returns | ||
*/ | ||
@ApiResponse({ type: ResponseUtil }) | ||
@ApiOperation({ summary: 'Get a website hosting of an app' }) | ||
@UseGuards(JwtAuthGuard, ApplicationAuthGuard) | ||
@Get(':id') | ||
async findOne(@Param('appid') _appid: string, @Param('id') id: string) { | ||
const site = await this.websiteService.findOne(id) | ||
if (!site) { | ||
return ResponseUtil.error('website hosting not found') | ||
} | ||
|
||
return ResponseUtil.ok(site) | ||
} | ||
|
||
/** | ||
* Bind custom domain to website | ||
* @param id | ||
* @param dto | ||
* @param req | ||
* @returns | ||
*/ | ||
@ApiResponse({ type: ResponseUtil }) | ||
@ApiOperation({ summary: 'Bind custom domain to website' }) | ||
@UseGuards(JwtAuthGuard, ApplicationAuthGuard) | ||
@Patch(':id') | ||
async bindDomain( | ||
@Param('appid') _appid: string, | ||
@Param('id') id: string, | ||
@Body() dto: BindCustomDomainDto, | ||
) { | ||
// get website | ||
const site = await this.websiteService.findOne(id) | ||
if (!site) { | ||
return ResponseUtil.error('website hosting not found') | ||
} | ||
|
||
// check if domain resolved | ||
const resolved = await this.websiteService.checkResolved(site, dto.domain) | ||
if (!resolved) { | ||
return ResponseUtil.error('domain not resolved') | ||
} | ||
|
||
// TODO: check if domain is already binded, remove old domain | ||
|
||
// bind domain | ||
const binded = await this.websiteService.bindCustomDomain( | ||
site.id, | ||
dto.domain, | ||
) | ||
if (!binded) { | ||
return ResponseUtil.error('failed to bind domain') | ||
} | ||
|
||
return ResponseUtil.ok(binded) | ||
} | ||
|
||
/** | ||
* Check if domain is resolved | ||
* @param id | ||
* @param dto | ||
* @returns | ||
*/ | ||
@ApiResponse({ type: ResponseUtil }) | ||
@ApiOperation({ summary: 'Check if domain is resolved' }) | ||
@UseGuards(JwtAuthGuard, ApplicationAuthGuard) | ||
@Post(':id/resolved') | ||
async checkResolved( | ||
@Param('appid') _appid: string, | ||
@Param('id') id: string, | ||
@Body() dto: BindCustomDomainDto, | ||
) { | ||
// get website | ||
const site = await this.websiteService.findOne(id) | ||
if (!site) { | ||
return ResponseUtil.error('website hosting not found') | ||
} | ||
|
||
// check if domain resolved | ||
const resolved = await this.websiteService.checkResolved(site, dto.domain) | ||
if (!resolved) { | ||
return ResponseUtil.error('domain not resolved') | ||
} | ||
|
||
return ResponseUtil.ok(resolved) | ||
} | ||
|
||
/** | ||
* Delete a website hosting | ||
* @param id | ||
* @returns | ||
*/ | ||
@ApiResponse({ type: ResponseUtil }) | ||
@ApiOperation({ summary: 'Delete a website hosting' }) | ||
@UseGuards(JwtAuthGuard, ApplicationAuthGuard) | ||
@Delete(':id') | ||
async remove(@Param('appid') _appid: string, @Param('id') id: string) { | ||
const site = await this.websiteService.findOne(id) | ||
if (!site) { | ||
return ResponseUtil.error('website hosting not found') | ||
} | ||
|
||
const deleted = await this.websiteService.remove(site.id) | ||
if (!deleted) { | ||
return ResponseUtil.error('failed to delete website hosting') | ||
} | ||
|
||
return ResponseUtil.ok(deleted) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Module } from '@nestjs/common' | ||
import { WebsiteService } from './website.service' | ||
import { WebsiteController } from './website.controller' | ||
import { PrismaService } from 'src/prisma.service' | ||
import { RegionModule } from 'src/region/region.module' | ||
import { ApplicationService } from 'src/application/application.service' | ||
|
||
@Module({ | ||
imports: [RegionModule], | ||
controllers: [WebsiteController], | ||
providers: [WebsiteService, PrismaService, ApplicationService], | ||
}) | ||
export class WebsiteModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { Injectable, Logger } from '@nestjs/common' | ||
import { DomainPhase, DomainState, WebsiteHosting } from '@prisma/client' | ||
import { TASK_LOCK_INIT_TIME } from 'src/constants' | ||
import { PrismaService } from 'src/prisma.service' | ||
import { RegionService } from 'src/region/region.service' | ||
import { CreateWebsiteDto } from './dto/create-website.dto' | ||
import * as assert from 'node:assert' | ||
import * as dns from 'node:dns' | ||
|
||
@Injectable() | ||
export class WebsiteService { | ||
private readonly logger = new Logger(WebsiteService.name) | ||
|
||
constructor( | ||
private readonly prisma: PrismaService, | ||
private readonly regionService: RegionService, | ||
) {} | ||
|
||
async create(appid: string, dto: CreateWebsiteDto) { | ||
const region = await this.regionService.findByAppId(appid) | ||
assert(region, 'region not found') | ||
|
||
// generate default website domain | ||
const domain = `${dto.bucketName}.${region.gatewayConf.websiteDomain}` | ||
|
||
const website = await this.prisma.websiteHosting.create({ | ||
data: { | ||
appid: appid, | ||
domain: domain, | ||
isCustom: false, | ||
state: DomainState.Active, | ||
phase: DomainPhase.Creating, | ||
lockedAt: TASK_LOCK_INIT_TIME, | ||
bucket: { | ||
connect: { | ||
name: dto.bucketName, | ||
}, | ||
}, | ||
}, | ||
}) | ||
|
||
return website | ||
} | ||
|
||
async findAll(appid: string) { | ||
const websites = await this.prisma.websiteHosting.findMany({ | ||
where: { | ||
appid: appid, | ||
}, | ||
include: { | ||
bucket: true, | ||
}, | ||
}) | ||
|
||
return websites | ||
} | ||
|
||
async findOne(id: string) { | ||
const website = await this.prisma.websiteHosting.findFirst({ | ||
where: { | ||
id, | ||
}, | ||
include: { | ||
bucket: true, | ||
}, | ||
}) | ||
|
||
return website | ||
} | ||
|
||
async checkResolved(website: WebsiteHosting, customDomain: string) { | ||
// get bucket domain | ||
const bucketDomain = await this.prisma.bucketDomain.findFirst({ | ||
where: { | ||
appid: website.appid, | ||
bucketName: website.bucketName, | ||
}, | ||
}) | ||
|
||
const cnameTarget = bucketDomain.domain | ||
|
||
// check domain is available | ||
const resolver = new dns.promises.Resolver({ timeout: 3000, tries: 1 }) | ||
const result = await resolver | ||
.resolveCname(customDomain as string) | ||
.catch(() => { | ||
return | ||
}) | ||
|
||
if (!result) { | ||
return false | ||
} | ||
|
||
if (false === (result || []).includes(cnameTarget)) { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
async bindCustomDomain(id: string, domain: string) { | ||
const website = await this.prisma.websiteHosting.update({ | ||
where: { | ||
id, | ||
}, | ||
data: { | ||
domain: domain, | ||
isCustom: true, | ||
}, | ||
}) | ||
|
||
return website | ||
} | ||
|
||
async remove(id: string) { | ||
const website = await this.prisma.websiteHosting.update({ | ||
where: { | ||
id, | ||
}, | ||
data: { | ||
state: DomainState.Deleted, | ||
}, | ||
}) | ||
|
||
return website | ||
} | ||
} |
Oops, something went wrong.