0%

转型计划之初级后端(下)

参考

资源

前言

此文是对 转型计划之初级后端(中) 的整理与补充, 措辞尽量精简以方便后续查阅。

前置知识

初始化项目

使用 npm 全局安装一些命令行工具

1
2
3
npm install -g @nestjs/cli
npm install -g yarn
npm install -g yarn-upgrade-all

使用 nest-cli 初始化项目, 包管理器选 yarn

1
2
nest new desktop-deployment-management-tool-api
cd desktop-deployment-management-tool-api

使用 yarn-upgrade-all 更新依赖

1
yarn-upgrade-all

修改 prettier 配置文件 .prettierrc

1
2
3
4
5
{
"singleQuote": true,
"trailingComma": "all",
"semi": false
}

创建编辑器工作区配置

1
mkdir .vscode;touch .vscode/setting.json

配置 prettiervscode 插件: prettier-vscode

setting.json

1
2
3
4
5
6
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
}

使用 nest-cli 生成 Interceptors, Exception filters, Pipes, Guards

1
2
3
4
nest generate interceptor common/interceptors/logging
nest generate filter common/filters/all-exception
nest generate pipe common/pipes/body-validation
nest generate guard common/guard/auth

运行 jest 测试

1
yarn test

配置

使用 yarn 添加运行时依赖 configdotenv

1
yarn add config dotenv

创建并编辑 config 配置文件

1
2
3
4
5
6
mkdir config
touch config/default.json
touch config/production.json
touch config/custom-environment-variables.json
touch src/config.ts
touch .env

default.json

1
2
3
4
5
6
7
8
9
10
11
{
"env": "development",
"port": 3000,
"logLevel": "debug",
"db": {
"name": "ddmtdb",
"password": "rootpass",
"port": 3307,
"username": "root"
}
}

production.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"env": "development",
"port": 3000,
"logLevel": "debug",
"db": {
"host": "db",
"name": "ddmtdb",
"password": "rootpass",
"port": 3306,
"username": "root"
},
"jwt": {
"secret": "secret-key"
}
}

custom-environment-variables.json

1
2
3
4
5
6
7
8
9
10
11
{
"env": "NODE_ENV",
"port": "PORT",
"logLevel": "LOG_LEVEL",
"db": {
"name": "DB_NAME",
"password": "DB_PASSWORD",
"port": "DB_PORT",
"username": "DB_USERNAME"
}
}

config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as dotenv from "dotenv"
dotenv.config()

export interface Config {
env: string
port: number
logLevel: string
db: {
name: string
username: string
password: string
port: number
}
}

export const config: Config = require('config')

.env

1
2
3
4
5
6
7
8
9
10
NODE_ENV=development
PORT=3000
SECRET=secret-key
LOG_LEVEL=debug
DB_NAME=ddmtdb
DB_USERNAME=root
DB_PASSWORD=rootpass
DB_PORT=3307
ADMINER_THEME=pepa-linha
ADMINER_PORT=8081

.env 中的值映射到 custom-environment-variables.json 对应的值上,
而 custom-environment-variables.json 中的值映射到 default.json 对应的值上.
最终 .env 中的值覆盖 default.json 中对应的值.

加密敏感信息请参考: 使用 Git-crypt 加密生产配置文件

日志

使用 yarn 添加运行时依赖 nest-winstonwinston

1
yarn add nest-winston winston

config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import * as dotenv from 'dotenv'
import { utilities as nestWinstonModuleUtilities } from 'nest-winston'
import { format, transports } from 'winston'

dotenv.config()

export interface Config {
env: string
port: number
logLevel: string
dbName: string
dbPassword: string
dbPort: number
}

export const config: Config = require('config')
export const loggerConfig = {
// options here
level: config.logLevel,
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
nestWinstonModuleUtilities.format.nestLike(),
),
}),
new transports.File({
filename: 'combined.log',
level: config.logLevel,
}),
],
}

logging.interceptor.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
LoggerService,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'
import { Request, Response } from 'express'

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private readonly logger: LoggerService) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const method = request.method
const url = request.url

const requestTime = Date.now()

// Add request time to params to be used in exception filters
request.params.requestTime = Date.now().toString()

return next
.handle()
.pipe(
tap(() =>
this.logger.log(
`${method} ${url} - ${response.statusCode} - ${Date.now() -
requestTime}ms`,
),
),
)
}
}

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import { config, loggerConfig } from './config'

const logger = WinstonModule.createLogger(loggerConfig)

async function bootstrap() {
const app = await NestFactory.create(AppModule)

app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER))
app.useGlobalInterceptors(new LoggingInterceptor(logger))

await app.listen(config.port)
logger.log(`Server started on port ${config.port}`)
}
bootstrap()

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { WinstonModule } from 'nest-winston'
import { loggerConfig } from './config'

@Module({
imports: [WinstonModule.forRoot(loggerConfig)],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

异常捕获

all-exception.filter.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
LoggerService,
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: LoggerService) {}
catch(exception: HttpException | Error, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR

// Get the location where the error was thrown from to use as a logging tag
const stackTop = exception.stack
.split('\n')[1]
.split('at ')[1]
.split(' ')[0]

const message = exception.message.message || exception.message
const meta = exception.message.meta || exception.message.errors
const logMessage = {
status,
message,
meta,
}

this.logger.error(JSON.stringify(logMessage), stackTop, 'TRACE')

const method = request.method
const url = request.url
const requestTime = request.params.requestTime

this.logger.log(
`${method} ${url} - ${status} - ${Date.now() - Number(requestTime)}ms`,
'Access',
)

response.status(status).send(logMessage)
}
}

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import { AllExceptionFilter } from './common/filters/all-exception.filter'
import { config, loggerConfig } from './config'

const logger = WinstonModule.createLogger(loggerConfig)

async function bootstrap() {
const app = await NestFactory.create(AppModule)

app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER))
app.useGlobalInterceptors(new LoggingInterceptor(logger))
app.useGlobalFilters(new AllExceptionFilter(logger))

await app.listen(config.port)
logger.log(`Server started on port ${config.port}`)
}
bootstrap()

请求体验证

使用 yarn 添加运行时依赖 class-validatorclass-transformer

1
yarn add class-validator class-transformer

body-validation.pipe.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, metadata: ArgumentMetadata) {
// Account for an empty request body
if (value == null) {
value = {}
}

const { metatype } = metadata
if (!metatype || !this.toValidate(metatype)) {
return value
}

const object = plainToClass(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
// Top-level errors
const topLevelErrors = errors
.filter(v => v.constraints)
.map(error => {
return {
property: error.property,
constraints: Object.values(error.constraints),
}
})

// Nested errors
const nestedErrors = errors
.filter(v => !v.constraints)
.map(error => {
const validationErrors = this.getValidationErrorsFromChildren(
error.property,
error.children,
)
return validationErrors
})

throw new BadRequestException({
message: 'Validation failed',
meta: topLevelErrors.concat(...nestedErrors),
})
}

return value
}

private toValidate(metatype: any): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}

private getValidationErrorsFromChildren(parent, children, errors = []) {
children.forEach(child => {
if (child.constraints) {
errors.push({
property: `${parent}.${child.property}`,
constraints: Object.values(child.constraints),
})
} else {
return this.getValidationErrorsFromChildren(
`${parent}.${child.property}`,
child.children,
errors,
)
}
})
return errors
}
}

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import { AllExceptionFilter } from './common/filters/all-exception.filter'
import { BodyValidationPipe } from './common/pipes/body-validation.pipe'
import { config, loggerConfig } from './config'

const logger = WinstonModule.createLogger(loggerConfig)

async function bootstrap() {
const app = await NestFactory.create(AppModule)

app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER))
app.useGlobalInterceptors(new LoggingInterceptor(logger))
app.useGlobalFilters(new AllExceptionFilter(logger))
app.useGlobalPipes(new BodyValidationPipe())

await app.listen(config.port)
logger.log(`Server started on port ${config.port}`)
}
bootstrap()

数据库

使用 image:mysqlimage:adminer 创建 docker 容器

创建文件

1
2
mkdir mysql
touch docker-compose.yml

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Use root/example as user/password credentials
version: '3.1'

services:

db:
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
ports:
- "${DB_PORT}:3306"
volumes:
- ./mysql:/var/lib/mysql

adminer:
image: adminer
restart: always
environment:
ADMINER_DESIGN: ${ADMINER_THEME}
ports:
- ${ADMINER_PORT}:8080

package.json

1
2
3
4
5
{
"scripit": {
"db": "docker-compose up -d"
}
}

生成并运行容器

1
yarn db

访问 image:adminer 以管理数据库

打开 http://127.0.0.1:8081/ 登录即可

使用 typeorm 连接数据库

参考: typeorm docs 以及 https://docs.nestjs.com/techniques/database

添加运行时依赖 typeorm@nestjs/typeormmysql

1
yarn add typeorm @nestjs/typeorm mysql

config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import * as dotenv from 'dotenv'
import { utilities as nestWinstonModuleUtilities } from 'nest-winston'
import { format, transports } from 'winston'
import { TypeOrmModuleOptions } from '@nestjs/typeorm'

dotenv.config()

export const isDev = process.env.NODE_ENV === 'development'

export interface Config {
env: string
port: number
logLevel: string
db: {
host: string
name: string
username: string
password: string
port: number
}
jwt: {
secret: string
}
}

export const config: Config = require('config')
export const loggerConfig = {
// options here
level: config.logLevel,
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.timestamp(),
nestWinstonModuleUtilities.format.nestLike(),
),
}),
new transports.File({
filename: 'combined.log',
level: config.logLevel,
}),
],
}

export const dbConfig: TypeOrmModuleOptions = {
type: 'mysql',
name: 'default',
host: config.db.host,
port: config.db.port,
username: config.db.username,
password: config.db.password,
database: config.db.name,
entities: ['dist/**/**.entity{.ts,.js}'],
synchronize: isDev ? true : false,
}

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { WinstonModule } from 'nest-winston'
import { TypeOrmModule } from '@nestjs/typeorm'
import { loggerConfig, dbConfig } from './config'

@Module({
imports: [
TypeOrmModule.forRoot(dbConfig),
WinstonModule.forRoot(loggerConfig),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

swagger 文档

参考: https://docs.nestjs.com/recipes/swagger#openapi-swagger
添加运行时依赖 @nestjs/swagger swagger-ui-express

1
yarn add @nestjs/swagger swagger-ui-express

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import { AllExceptionFilter } from './common/filters/all-exception.filter'
import { BodyValidationPipe } from './common/pipes/body-validation.pipe'
import { config, loggerConfig } from './config'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'

const logger = WinstonModule.createLogger(loggerConfig)

async function bootstrap() {
const app = await NestFactory.create(AppModule)

app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER))
app.useGlobalInterceptors(new LoggingInterceptor(logger))
app.useGlobalFilters(new AllExceptionFilter(logger))
app.useGlobalPipes(new BodyValidationPipe())

// Initialize Swagger UI
const options = new DocumentBuilder()
.setTitle('NestJS Realworld Example App')
.setDescription('The Realworld API description')
.setVersion('1.0')
.addBearerAuth()
.build()

const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('/docs', app, document)

await app.listen(config.port)
logger.log(`Server started on port ${config.port}`)
}
bootstrap()

认证

参考: passport docs 以及 https://docs.nestjs.com/techniques/authentication
添加运行时依赖 [@nestjs/passport][] 和 passport 和 [passport-local][] 和 [@nestjs/jwt][] 和 [passport-jwt][]
添加开发时依赖 [@types/passport-local][] 和 [@types/passport-jwt][]

1
2
yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
yarn add @types/passport-local @types/passport-jwt --save-dev

生成 auth 与 users 模块文件

1
2
3
4
5
6
7
nest g module auth
nest g service auth
nest g module user
nest g service user
nest g controller user
touch src/auth/local.strategy.ts
touch src/auth/jwt.strategy.ts

user

user.entity.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert } from 'typeorm'
import { IsEmail } from 'class-validator'
import * as crypto from 'crypto'

@Entity('user')
export class UserEntity {
@PrimaryGeneratedColumn()
id: number

@Column()
username: string

@Column()
email: string

@Column({ default: '' })
bio: string

@Column({ default: '' })
image: string

@Column()
password: string

@BeforeInsert()
hashPassword() {
this.password = crypto.createHmac('sha256', this.password).digest('hex')
}
}

user.decorator.ts

1
2
3
4
5
6
7
8
import { createParamDecorator } from '@nestjs/common'

export const User = createParamDecorator((data, req) => {
// if route is protected, there is a user set in auth.middleware
if (req.user) {
return data ? req.user[data] : req.user
}
})

user.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import {
Get,
Post,
Body,
Put,
Delete,
Param,
Controller,
UseGuards,
Request,
} from '@nestjs/common'
import { UserService } from './user.service'
import { UserRO } from './user.interface'
import { CreateUserDto, UpdateUserDto } from './dto'
import { User } from './user.decorator'
import { AuthGuard } from '@nestjs/passport'
import { AuthService } from '../auth/auth.service'

import { ApiTags, ApiBearerAuth, ApiBody } from '@nestjs/swagger'

@ApiBearerAuth()
@ApiTags('user')
@Controller()
export class UserController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}

@UseGuards(AuthGuard('jwt'))
@Put('user/profile')
async update(@User('email') email: string, @Body() userData: UpdateUserDto) {
const newUser = await this.userService.update(email, userData)
return this.authService.buildUserRO(newUser)
}

// @UsePipes(new ValidationPipe())
@Post('users/sign_up')
@ApiBody({ type: CreateUserDto })
async create(@Body() userData: CreateUserDto) {
const newUser = await this.userService.create(userData)
return this.authService.buildUserRO(newUser)
}

@UseGuards(AuthGuard('jwt'))
@Delete('user/cancel')
async delete(@User('email') email: string) {
return await this.userService.delete(email)
}

@UseGuards(AuthGuard('local'))
@Post('user/sign_in')
async login(@Request() req): Promise<any> {
return this.authService.buildUserRO(req.user)
}
}

user.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { Injectable, HttpStatus } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository, getRepository, DeleteResult } from 'typeorm'
import { UserEntity } from './user.entity'
import { CreateUserDto, LoginUserDto, UpdateUserDto } from './dto'
import { config } from '../config'
import { UserRO } from './user.interface'
import { validate } from 'class-validator'
import { HttpException } from '@nestjs/common/exceptions/http.exception'

import * as crypto from 'crypto'
import * as jwt from 'jsonwebtoken'

@Injectable()
export class UserService {
constructor(
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
) {}

async findAll(): Promise<UserEntity[]> {
return await this.userRepository.find()
}

async findOne(loginUserDto: LoginUserDto): Promise<UserEntity> {
const findOneOptions = {
email: loginUserDto.email,
password: crypto
.createHmac('sha256', loginUserDto.password)
.digest('hex'),
}

const user = await this.userRepository.findOne(findOneOptions)
return user
}

async create(dto: CreateUserDto): Promise<any> {
// check uniqueness of username/email
const { username, email, password } = dto
const qb = await getRepository(UserEntity)
.createQueryBuilder('user')
.where('user.username = :username', { username })
.orWhere('user.email = :email', { email })

const user = await qb.getOne()

if (user) {
const errors = { username: 'Username and email must be unique.' }
throw new HttpException(
{ message: 'Input data validation failed', errors },
HttpStatus.BAD_REQUEST,
)
}

// create new user
const newUser = new UserEntity()
newUser.username = username
newUser.email = email
newUser.password = password

const errors = await validate(newUser)
if (errors.length > 0) {
const _errors = { username: 'Userinput is not valid.' }
throw new HttpException(
{ message: 'Input data validation failed', meta: _errors },
HttpStatus.BAD_REQUEST,
)
} else {
const savedUser = await this.userRepository.save(newUser)
return savedUser
}
}

async update(email: string, dto: UpdateUserDto): Promise<UserEntity> {
const toUpdate = await this.userRepository.findOne(email)
delete toUpdate.password

const updated = Object.assign(toUpdate, dto)
return await this.userRepository.save(updated)
}

async delete(email: string): Promise<DeleteResult> {
return await this.userRepository.delete({ email: email })
}
}

user.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Module, forwardRef } from '@nestjs/common'
import { UserController } from './user.controller'
import { TypeOrmModule } from '@nestjs/typeorm'
import { UserEntity } from './user.entity'
import { UserService } from './user.service'
import { AuthModule } from '../auth/auth.module'

@Module({
imports: [
TypeOrmModule.forFeature([UserEntity]),
forwardRef(() => AuthModule),
],
providers: [UserService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}

auth

auth.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Module, forwardRef } from '@nestjs/common'
import { AuthService } from './auth.service'
import { LocalStrategy } from './local.strategy'
import { JwtStrategy } from './jwt.strategy'
import { UserModule } from '../user/user.module'
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt'
import { config } from '../config'

@Module({
imports: [
forwardRef(() => UserModule),
PassportModule,
JwtModule.register({
secret: config.jwt.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

auth.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Injectable, Inject } from '@nestjs/common'
import { UserService } from '../user/user.service'
import { JwtService } from '@nestjs/jwt'
import { LoginUserDto, UserRO } from '../user/dto'

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}

async validateUser({ email, password }: LoginUserDto): Promise<any> {
const user = await this.userService.findOne({ email, password })

if (user) {
const { id, email, username, bio, image } = user
return { id, email, username, bio, image }
}

return null
}

async buildUserRO(user): Promise<UserRO> {
const { id, email, username, bio, image } = user
const payload = { id, email, username }

return {
username,
email,
bio,
image,
access_token: this.jwtService.sign(payload),
}
}
}

local.strategy

local.strategy.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Strategy } from 'passport-local'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { AuthService } from './auth.service'

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
usernameField: 'email',
passwordField: 'password',
session: false,
})
}

async validate(email, password): Promise<any> {
const user = await this.authService.validateUser({
email,
password,
})
if (!user) {
throw new UnauthorizedException({
error: 'Incorrect username or password',
})
}
return user
}
}

jwt.strategy

jwt.strategy.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
import { config } from '../config'
import { JwtPayload } from '../user/dto'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.jwt.secret,
})
}

validate(payload: JwtPayload): JwtPayload {
return payload
}
}

Dockerizing

参考: 把一个 Node.js web 应用程序给 Docker 化
新建文件

1
2
3
touch Dockerfile
touch .dockerignore
touch docker-compose.prod.yml

.env

1
2
3
4
5
6
7
8
9
10
11
NODE_ENV=production
PORT=3000
SECRET=secret-key
LOG_LEVEL=debug
# DB_HOST=db
DB_NAME=ddmtdb
DB_USERNAME=root
DB_PASSWORD=rootpass
DB_PORT=3306
ADMINER_THEME=pepa-linha
ADMINER_PORT=8080

.dockerignore

1
2
3
node_modules
npm-debug.log
yarn-error.log

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM node:10

# Create app directory
WORKDIR /usr/src/app

# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./

# Install necessary tools for bcrypt to run in docker before npm install
RUN apt-get update && apt-get install -y build-essential && apt-get install -y python

# Install app dependencies
RUN npm install

FROM node:10-alpine

# Create app directory
WORKDIR /usr/src/app

COPY --from=0 /usr/src/app .

COPY . .

EXPOSE 3000
CMD [ "npm", "run", "start:prod" ]

docker-compose.prod.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Use root/example as user/password credentials
version: '3.7'

services:
nest-web-api:
build: .
restart: always
ports:
- "3000:3000"
depends_on:
- db
- adminer

db:
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
ports:
- "${DB_PORT}:3306"
volumes:
- ./mysql:/var/lib/mysql

adminer:
image: adminer
restart: always
environment:
ADMINER_DESIGN: ${ADMINER_THEME}
ports:
- ${ADMINER_PORT}:8080

package.json script

1
2
3
4
5
{
"start:prod": "npm run build && node dist/main",
"db": "docker-compose up -d",
"deploy": "docker-compose -f docker-compose.prod.yml up"
}

README.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## Dockerizing

### development
.env
```
NODE_ENV=development
```

```bash
yarn db
```

### deploy
.env
```
NODE_ENV=production
```

```bash
yarn deploy
```

扩展阅读

结语

到这种程度就足够支撑基本的单体架构应用的开发了, 更多的应用优化(如缓存, 日志监控分析)与开发概念(如 DDD, microservices, serverless),则需要到业务中去沉淀一番才能有所总结。
再进一步: [DDD and Microservices][]