71 changed files with 3421 additions and 736 deletions
@ -0,0 +1,18 @@ |
|||||
|
# @vben/backend-mock |
||||
|
|
||||
|
## Description |
||||
|
|
||||
|
Vben Admin Pro 数据mock服务 |
||||
|
|
||||
|
## Running the app |
||||
|
|
||||
|
```bash |
||||
|
# development |
||||
|
$ pnpm run start |
||||
|
|
||||
|
# watch mode |
||||
|
$ pnpm run start:dev |
||||
|
|
||||
|
# production mode |
||||
|
$ pnpm run start:prod |
||||
|
``` |
||||
@ -0,0 +1,23 @@ |
|||||
|
module.exports = { |
||||
|
apps: [ |
||||
|
{ |
||||
|
autorestart: true, |
||||
|
cwd: './', |
||||
|
env: { |
||||
|
NODE_ENV: 'production', |
||||
|
}, |
||||
|
env_development: { |
||||
|
NODE_ENV: 'development', |
||||
|
}, |
||||
|
env_production: { |
||||
|
NODE_ENV: 'production', |
||||
|
}, |
||||
|
ignore_watch: ['node_modules', '.logs', 'dist'], |
||||
|
instances: 1, |
||||
|
max_memory_restart: '1G', |
||||
|
name: '@vben/backend-mock', |
||||
|
script: 'node dist/main.js', |
||||
|
watch: false, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
@ -0,0 +1,20 @@ |
|||||
|
@port = 5320 |
||||
|
@type = application/json |
||||
|
@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwicm9sZXMiOlsiYWRtaW4iXSwidXNlcm5hbWUiOiJ2YmVuIiwiaWF0IjoxNzE5ODkwMTEwLCJleHAiOjE3MTk5NzY1MTB9.eyAFsQ2Jk_mAQGvrEL1jF9O6YmLZ_PSYj5aokL6fCuU |
||||
|
POST http://localhost:{{port}}/api/auth/login HTTP/1.1 |
||||
|
content-type: {{ type }} |
||||
|
|
||||
|
{ |
||||
|
"username": "vben", |
||||
|
"password": "123456" |
||||
|
} |
||||
|
|
||||
|
|
||||
|
### |
||||
|
GET http://localhost:{{port}}/api/auth/getUserInfo HTTP/1.1 |
||||
|
content-type: {{ type }} |
||||
|
Authorization: {{ token }} |
||||
|
|
||||
|
{ |
||||
|
"username": "vben" |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
@port = 5320 |
||||
|
GET http://localhost:{{port}}/api HTTP/1.1 |
||||
|
content-type: application/json |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/nest-cli", |
||||
|
"collection": "@nestjs/schematics", |
||||
|
"sourceRoot": "src", |
||||
|
"compilerOptions": { |
||||
|
"assets": ["**/*.yml"], |
||||
|
"watchAssets": true, |
||||
|
"deleteOutDir": true |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
{ |
||||
|
"name": "@vben/backend-mock", |
||||
|
"version": "0.0.1", |
||||
|
"description": "", |
||||
|
"private": true, |
||||
|
"license": "MIT", |
||||
|
"author": "", |
||||
|
"scripts": { |
||||
|
"build": "nest build", |
||||
|
"dev": "pnpm run start:dev", |
||||
|
"start:dev": "cross-env NODE_ENV=development DEBUG=true nest start --watch", |
||||
|
"start": "cross-env NODE_ENV=development node dist/main", |
||||
|
"start:prod": "cross-env NODE_ENV=production node dist/main" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@nestjs/common": "^10.3.10", |
||||
|
"@nestjs/config": "^3.2.3", |
||||
|
"@nestjs/core": "^10.3.10", |
||||
|
"@nestjs/jwt": "^10.2.0", |
||||
|
"@nestjs/passport": "^10.0.3", |
||||
|
"@nestjs/platform-express": "^10.3.10", |
||||
|
"@nestjs/typeorm": "^10.0.2", |
||||
|
"@types/js-yaml": "^4.0.9", |
||||
|
"bcryptjs": "^2.4.3", |
||||
|
"class-transformer": "^0.5.1", |
||||
|
"class-validator": "^0.14.1", |
||||
|
"cross-env": "^7.0.3", |
||||
|
"joi": "^17.13.3", |
||||
|
"js-yaml": "^4.1.0", |
||||
|
"passport": "^0.7.0", |
||||
|
"passport-jwt": "^4.0.1", |
||||
|
"passport-local": "^1.0.0", |
||||
|
"reflect-metadata": "^0.2.2", |
||||
|
"rxjs": "^7.8.1", |
||||
|
"sqlite3": "^5.1.7", |
||||
|
"typeorm": "^0.3.20" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@nestjs/cli": "^10.3.2", |
||||
|
"@nestjs/schematics": "^10.1.1", |
||||
|
"@types/express": "^4.17.21", |
||||
|
"@types/node": "^20.14.9", |
||||
|
"nodemon": "^3.1.4", |
||||
|
"ts-node": "^10.9.2", |
||||
|
"typescript": "^5.5.3" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
import configuration from '@/config/index'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { ConfigModule } from '@nestjs/config'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
import Joi from 'joi'; |
||||
|
|
||||
|
import { AuthModule } from './modules/auth/auth.module'; |
||||
|
import { DatabaseModule } from './modules/database/database.module'; |
||||
|
import { HealthModule } from './modules/health/health.module'; |
||||
|
import { UsersModule } from './modules/users/users.module'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [ |
||||
|
TypeOrmModule.forRoot({ |
||||
|
autoLoadEntities: true, |
||||
|
database: 'data/db.sqlite', |
||||
|
synchronize: true, |
||||
|
type: 'sqlite', |
||||
|
}), |
||||
|
ConfigModule.forRoot({ |
||||
|
cache: true, |
||||
|
isGlobal: true, |
||||
|
load: [configuration], |
||||
|
validationOptions: { |
||||
|
abortEarly: true, |
||||
|
allowUnknown: true, |
||||
|
}, |
||||
|
validationSchema: Joi.object({ |
||||
|
NODE_ENV: Joi.string().valid('development', 'production', 'test'), |
||||
|
port: Joi.number(), |
||||
|
}), |
||||
|
}), |
||||
|
HealthModule, |
||||
|
AuthModule, |
||||
|
UsersModule, |
||||
|
DatabaseModule, |
||||
|
], |
||||
|
}) |
||||
|
export class AppModule {} |
||||
@ -0,0 +1,8 @@ |
|||||
|
NODE_ENV: development |
||||
|
port: 5320 |
||||
|
apiPrefix: /api |
||||
|
jwt: |
||||
|
secret: plonmGN4aSuMVnucrHuhnUoo49Wy |
||||
|
expiresIn: 1d |
||||
|
refreshSecret: 1lonmGN4aSuMVnucrHuhnUoo49Wy |
||||
|
refreshexpiresIn: 7d |
||||
@ -0,0 +1,23 @@ |
|||||
|
import { readFileSync } from 'node:fs'; |
||||
|
import { join } from 'node:path'; |
||||
|
import process from 'node:process'; |
||||
|
|
||||
|
import * as yaml from 'js-yaml'; |
||||
|
|
||||
|
const configFileNameObj = { |
||||
|
development: 'dev', |
||||
|
production: 'prod', |
||||
|
}; |
||||
|
|
||||
|
const env = process.env.NODE_ENV; |
||||
|
|
||||
|
const configFactory = () => { |
||||
|
return yaml.load( |
||||
|
readFileSync( |
||||
|
join(process.cwd(), 'src', 'config', `${configFileNameObj[env]}.yml`), |
||||
|
'utf8', |
||||
|
), |
||||
|
) as Record<string, any>; |
||||
|
}; |
||||
|
|
||||
|
export default configFactory; |
||||
@ -0,0 +1,8 @@ |
|||||
|
NODE_ENV: production |
||||
|
port: 5320 |
||||
|
apiPrefix: /api |
||||
|
jwt: |
||||
|
secret: plonmGN4SuMVnucrHunUoo49Wy12 |
||||
|
expiresIn: 1d |
||||
|
refreshSecret: 2lonmGN4aSuMVnucrHuhnUoo49Wy |
||||
|
refreshexpiresIn: 7d |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './public'; |
||||
@ -0,0 +1,4 @@ |
|||||
|
import { SetMetadata } from '@nestjs/common'; |
||||
|
|
||||
|
export const IS_PUBLIC_KEY = 'isPublic'; |
||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); |
||||
@ -0,0 +1,40 @@ |
|||||
|
import { |
||||
|
ArgumentsHost, |
||||
|
Catch, |
||||
|
ExceptionFilter, |
||||
|
HttpException, |
||||
|
HttpStatus, |
||||
|
Logger, |
||||
|
} from '@nestjs/common'; |
||||
|
import { Request, Response } from 'express'; |
||||
|
|
||||
|
@Catch(HttpException) |
||||
|
export class HttpExceptionFilter implements ExceptionFilter { |
||||
|
catch(exception: HttpException, 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; |
||||
|
|
||||
|
const logFormat = `Request original url: ${request.originalUrl} Method: ${request.method} IP: ${request.ip} Status code: ${status} Response: ${exception.toString()}`; |
||||
|
Logger.error(logFormat); |
||||
|
|
||||
|
const resultMessage = exception.message as any; |
||||
|
const message = |
||||
|
resultMessage || `${status >= 500 ? 'Service Error' : 'Client Error'}`; |
||||
|
|
||||
|
const errorResponse = { |
||||
|
code: 1, |
||||
|
error: resultMessage, |
||||
|
message, |
||||
|
status, |
||||
|
url: request.originalUrl, |
||||
|
}; |
||||
|
response.status(status); |
||||
|
response.header('Content-Type', 'application/json; charset=utf-8'); |
||||
|
response.send(errorResponse); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './http-exception.filter'; |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './jwt-auth.guard'; |
||||
|
export * from './local-auth.guard'; |
||||
@ -0,0 +1,23 @@ |
|||||
|
import { ExecutionContext, Injectable } from '@nestjs/common'; |
||||
|
import { Reflector } from '@nestjs/core'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
|
||||
|
import { IS_PUBLIC_KEY } from '../decorator/index'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class JwtAuthGuard extends AuthGuard('jwt') { |
||||
|
constructor(private reflector: Reflector) { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
canActivate(context: ExecutionContext) { |
||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ |
||||
|
context.getHandler(), |
||||
|
context.getClass(), |
||||
|
]); |
||||
|
if (isPublic) { |
||||
|
return true; |
||||
|
} |
||||
|
return super.canActivate(context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class LocalAuthGuard extends AuthGuard('local') {} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './transform.interceptor'; |
||||
@ -0,0 +1,37 @@ |
|||||
|
import { |
||||
|
CallHandler, |
||||
|
ExecutionContext, |
||||
|
Injectable, |
||||
|
Logger, |
||||
|
NestInterceptor, |
||||
|
} from '@nestjs/common'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TransformInterceptor implements NestInterceptor { |
||||
|
public intercept( |
||||
|
context: ExecutionContext, |
||||
|
next: CallHandler, |
||||
|
): Observable<any> { |
||||
|
const req = context.getArgByIndex(1).req; |
||||
|
return next.handle().pipe( |
||||
|
map((data) => { |
||||
|
const logFormat = ` |
||||
|
Request original url: ${req.originalUrl} |
||||
|
Method: ${req.method} |
||||
|
IP: ${req.ip} |
||||
|
User: ${JSON.stringify(req.user)} |
||||
|
Response data: ${JSON.stringify(data)} |
||||
|
`;
|
||||
|
Logger.debug(logFormat); |
||||
|
return { |
||||
|
code: 0, |
||||
|
data, |
||||
|
error: null, |
||||
|
message: 'ok', |
||||
|
}; |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './params.pipe'; |
||||
@ -0,0 +1,27 @@ |
|||||
|
import { |
||||
|
BadRequestException, |
||||
|
HttpStatus, |
||||
|
ValidationPipe, |
||||
|
type ValidationPipeOptions, |
||||
|
} from '@nestjs/common'; |
||||
|
|
||||
|
class ParamsValidationPipe extends ValidationPipe { |
||||
|
constructor(options: ValidationPipeOptions = {}) { |
||||
|
super({ |
||||
|
errorHttpStatusCode: HttpStatus.BAD_REQUEST, |
||||
|
exceptionFactory: (errors) => { |
||||
|
const message = Object.values(errors[0].constraints)[0]; |
||||
|
return new BadRequestException({ |
||||
|
message, |
||||
|
status: HttpStatus.BAD_REQUEST, |
||||
|
}); |
||||
|
}, |
||||
|
forbidNonWhitelisted: true, |
||||
|
transform: true, |
||||
|
whitelist: true, |
||||
|
...options, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { ParamsValidationPipe }; |
||||
@ -0,0 +1,51 @@ |
|||||
|
import type { AppConfig } from '@/types'; |
||||
|
|
||||
|
import process from 'node:process'; |
||||
|
|
||||
|
import { HttpExceptionFilter } from '@/core/filter'; |
||||
|
import { TransformInterceptor } from '@/core/interceptor'; |
||||
|
import { ParamsValidationPipe } from '@/core/pipe'; |
||||
|
import { type LogLevel } from '@nestjs/common'; |
||||
|
import { ConfigService } from '@nestjs/config'; |
||||
|
import { NestFactory, Reflector } from '@nestjs/core'; |
||||
|
|
||||
|
import { AppModule } from './app.module'; |
||||
|
import { JwtAuthGuard } from './core/guard'; |
||||
|
|
||||
|
async function bootstrap() { |
||||
|
const debug: LogLevel[] = process.env.DEBUG ? ['debug'] : []; |
||||
|
const loggerLevel: LogLevel[] = ['log', 'error', 'warn', ...debug]; |
||||
|
|
||||
|
const app = await NestFactory.create(AppModule, { |
||||
|
cors: true, |
||||
|
logger: loggerLevel, |
||||
|
}); |
||||
|
|
||||
|
// 获取 ConfigService 实例
|
||||
|
const configService = app.get(ConfigService); |
||||
|
|
||||
|
// 使用 ConfigService 获取配置值
|
||||
|
const port = configService.get<AppConfig['port']>('port') || 3000; |
||||
|
const apiPrefix = configService.get<AppConfig['apiPrefix']>('apiPrefix'); |
||||
|
|
||||
|
// 全局注册拦截器
|
||||
|
app.useGlobalInterceptors(new TransformInterceptor()); |
||||
|
|
||||
|
const reflector = app.get(Reflector); |
||||
|
app.useGlobalGuards(new JwtAuthGuard(reflector)); |
||||
|
|
||||
|
// 全局注册错误的过滤器
|
||||
|
app.useGlobalFilters(new HttpExceptionFilter()); |
||||
|
|
||||
|
// 设置全局接口数据校验
|
||||
|
app.useGlobalPipes(new ParamsValidationPipe()); |
||||
|
|
||||
|
app.setGlobalPrefix(apiPrefix); |
||||
|
|
||||
|
await app.listen(port); |
||||
|
|
||||
|
console.log( |
||||
|
`Application is running on: http://localhost:${port}${apiPrefix}`, |
||||
|
); |
||||
|
} |
||||
|
bootstrap(); |
||||
@ -0,0 +1,5 @@ |
|||||
|
class RefreshTokenDto { |
||||
|
refreshToken: string; |
||||
|
} |
||||
|
|
||||
|
export { RefreshTokenDto }; |
||||
@ -0,0 +1,34 @@ |
|||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; |
||||
|
|
||||
|
@Entity() |
||||
|
class UserEntity { |
||||
|
@PrimaryGeneratedColumn() |
||||
|
id: number; |
||||
|
/** |
||||
|
* 密码 |
||||
|
*/ |
||||
|
@Column() |
||||
|
password: string; |
||||
|
/** |
||||
|
* 真实姓名 |
||||
|
*/ |
||||
|
@Column() |
||||
|
realName: string; |
||||
|
/** |
||||
|
* 角色 |
||||
|
*/ |
||||
|
@Column('text', { |
||||
|
transformer: { |
||||
|
from: (value: string) => JSON.parse(value), |
||||
|
to: (value: string[]) => JSON.stringify(value), |
||||
|
}, |
||||
|
}) |
||||
|
roles: string[]; |
||||
|
/** |
||||
|
* 用户名 |
||||
|
*/ |
||||
|
@Column({ unique: true }) |
||||
|
username: string; |
||||
|
} |
||||
|
|
||||
|
export { UserEntity }; |
||||
@ -0,0 +1,49 @@ |
|||||
|
import type { RefreshTokenDto } from '@/models/dto/auth.dto'; |
||||
|
|
||||
|
import { Public } from '@/core/decorator'; |
||||
|
import { LocalAuthGuard } from '@/core/guard'; |
||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Get, |
||||
|
HttpCode, |
||||
|
HttpStatus, |
||||
|
Post, |
||||
|
Request, |
||||
|
UseGuards, |
||||
|
} from '@nestjs/common'; |
||||
|
|
||||
|
import { AuthService } from './auth.service'; |
||||
|
|
||||
|
@Controller('auth') |
||||
|
export class AuthController { |
||||
|
constructor(private authService: AuthService) {} |
||||
|
|
||||
|
/** |
||||
|
* 获取用户信息 |
||||
|
* @param req |
||||
|
*/ |
||||
|
@Get('getUserInfo') |
||||
|
@HttpCode(HttpStatus.OK) |
||||
|
async getProfile(@Request() req: Request) { |
||||
|
return await this.authService.getUserInfo(req.user.username); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 用户登录 |
||||
|
* @param req |
||||
|
*/ |
||||
|
@Public() |
||||
|
@UseGuards(LocalAuthGuard) |
||||
|
@Post('login') |
||||
|
@HttpCode(HttpStatus.OK) |
||||
|
async login(@Request() req: Request) { |
||||
|
return await this.authService.login(req.user); |
||||
|
} |
||||
|
|
||||
|
@Post('refreshToken') |
||||
|
@HttpCode(HttpStatus.OK) |
||||
|
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) { |
||||
|
return this.authService.refresh(refreshTokenDto.refreshToken); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import type { JwtConfig } from '@/types'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { ConfigService } from '@nestjs/config'; |
||||
|
import { JwtModule } from '@nestjs/jwt'; |
||||
|
|
||||
|
import { UsersModule } from '../users/users.module'; |
||||
|
import { AuthController } from './auth.controller'; |
||||
|
import { AuthService } from './auth.service'; |
||||
|
import { JwtStrategy } from './jwt.strategy'; |
||||
|
import { LocalStrategy } from './local.strategy'; |
||||
|
import { JwtRefreshStrategy } from './refresh-token.strategy'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [AuthController], |
||||
|
exports: [AuthService], |
||||
|
imports: [ |
||||
|
UsersModule, |
||||
|
JwtModule.registerAsync({ |
||||
|
global: true, |
||||
|
inject: [ConfigService], |
||||
|
useFactory: async (configService: ConfigService) => { |
||||
|
const { expiresIn, secret } = configService.get<JwtConfig>('jwt'); |
||||
|
return { |
||||
|
secret, |
||||
|
signOptions: { expiresIn }, |
||||
|
}; |
||||
|
}, |
||||
|
}), |
||||
|
], |
||||
|
providers: [AuthService, JwtStrategy, JwtRefreshStrategy, LocalStrategy], |
||||
|
}) |
||||
|
export class AuthModule {} |
||||
@ -0,0 +1,70 @@ |
|||||
|
import type { UserEntity } from '@/models/entity/user.entity'; |
||||
|
import type { JwtConfig } from '@/types'; |
||||
|
|
||||
|
import { UsersService } from '@/modules/users/users.service'; |
||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common'; |
||||
|
import { ConfigService } from '@nestjs/config'; |
||||
|
import { JwtService } from '@nestjs/jwt'; |
||||
|
import bcrypt from 'bcryptjs'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AuthService { |
||||
|
constructor( |
||||
|
private usersService: UsersService, |
||||
|
private jwtService: JwtService, |
||||
|
private configService: ConfigService, |
||||
|
) {} |
||||
|
|
||||
|
/** |
||||
|
* get user info |
||||
|
* @param username |
||||
|
*/ |
||||
|
async getUserInfo(username: string): Promise<Omit<UserEntity, 'password'>> { |
||||
|
const user = await this.usersService.findOne(username); |
||||
|
const { password: _pass, ...userInfo } = user; |
||||
|
return userInfo; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* user login |
||||
|
*/ |
||||
|
async login(userEntity: UserEntity): Promise<any> { |
||||
|
const { id, roles, username } = userEntity; |
||||
|
|
||||
|
const payload = { id, roles, username }; |
||||
|
const { refreshSecret, refreshexpiresIn } = |
||||
|
this.configService.get<JwtConfig>('jwt'); |
||||
|
return { |
||||
|
accessToken: await this.jwtService.signAsync(payload), |
||||
|
refreshToken: this.jwtService.sign(payload, { |
||||
|
expiresIn: refreshexpiresIn, |
||||
|
secret: refreshSecret, |
||||
|
}), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async refresh(refreshToken: string) { |
||||
|
try { |
||||
|
const payload = this.jwtService.verify(refreshToken, { |
||||
|
secret: this.configService.get<JwtConfig>('jwt').refreshSecret, |
||||
|
}); |
||||
|
const user = await this.usersService.findOne(payload.username); |
||||
|
if (!user) { |
||||
|
throw new UnauthorizedException(); |
||||
|
} |
||||
|
return this.login(user); |
||||
|
} catch { |
||||
|
throw new UnauthorizedException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async validateUser(username: string, password: string): Promise<any> { |
||||
|
const user = await this.usersService.findOne(username); |
||||
|
if (user && (await bcrypt.compare(password, user.password))) { |
||||
|
// 使用 bcrypt.compare 验证密码
|
||||
|
const { password: _pass, ...result } = user; |
||||
|
return result; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
import type { JwtConfig, JwtPayload } from '@/types'; |
||||
|
|
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { ConfigService } from '@nestjs/config'; |
||||
|
import { PassportStrategy } from '@nestjs/passport'; |
||||
|
import { ExtractJwt, Strategy } from 'passport-jwt'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class JwtStrategy extends PassportStrategy(Strategy) { |
||||
|
constructor(configService: ConfigService) { |
||||
|
super({ |
||||
|
ignoreExpiration: false, |
||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), |
||||
|
secretOrKey: configService.get<JwtConfig>('jwt').secret, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async validate(payload: JwtPayload) { |
||||
|
console.log('jwt strategy validate payload', payload); |
||||
|
return { |
||||
|
id: payload.id, |
||||
|
roles: payload.roles, |
||||
|
username: payload.username, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common'; |
||||
|
import { PassportStrategy } from '@nestjs/passport'; |
||||
|
import { Strategy } from 'passport-local'; |
||||
|
|
||||
|
import { AuthService } from './auth.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class LocalStrategy extends PassportStrategy(Strategy) { |
||||
|
constructor(private authService: AuthService) { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
async validate(username: string, password: string): Promise<any> { |
||||
|
const user = await this.authService.validateUser(username, password); |
||||
|
if (!user) { |
||||
|
throw new UnauthorizedException(); |
||||
|
} |
||||
|
return user; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import type { JwtConfig, JwtPayload } from '@/types'; |
||||
|
|
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { ConfigService } from '@nestjs/config'; |
||||
|
import { PassportStrategy } from '@nestjs/passport'; |
||||
|
import { ExtractJwt, Strategy } from 'passport-jwt'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class JwtRefreshStrategy extends PassportStrategy( |
||||
|
Strategy, |
||||
|
'jwt-refresh', |
||||
|
) { |
||||
|
constructor(configService: ConfigService) { |
||||
|
super({ |
||||
|
ignoreExpiration: false, |
||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), |
||||
|
secretOrKey: configService.get<JwtConfig>('jwt').refreshSecret, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async validate(payload: JwtPayload) { |
||||
|
console.log('jwt refresh strategy validate payload', payload); |
||||
|
return { |
||||
|
id: payload.id, |
||||
|
roles: payload.roles, |
||||
|
username: payload.username, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { UserEntity } from '@/models/entity/user.entity'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
|
||||
|
import { UsersModule } from '../users/users.module'; |
||||
|
import { DatabaseService } from './database.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [UsersModule, TypeOrmModule.forFeature([UserEntity])], |
||||
|
providers: [DatabaseService], |
||||
|
}) |
||||
|
export class DatabaseModule {} |
||||
@ -0,0 +1,19 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
|
||||
|
import { DatabaseService } from './database.service'; |
||||
|
|
||||
|
describe('databaseService', () => { |
||||
|
let service: DatabaseService; |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [DatabaseService], |
||||
|
}).compile(); |
||||
|
|
||||
|
service = module.get<DatabaseService>(DatabaseService); |
||||
|
}); |
||||
|
|
||||
|
it('should be defined', () => { |
||||
|
expect(service).toBeDefined(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,40 @@ |
|||||
|
import type { Repository } from 'typeorm'; |
||||
|
|
||||
|
import { UserEntity } from '@/models/entity/user.entity'; |
||||
|
import { Injectable, type OnModuleInit } from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
|
||||
|
import { UsersService } from '../users/users.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class DatabaseService implements OnModuleInit { |
||||
|
constructor( |
||||
|
@InjectRepository(UserEntity) |
||||
|
private usersRepository: Repository<UserEntity>, |
||||
|
private userService: UsersService, |
||||
|
) {} |
||||
|
async onModuleInit() { |
||||
|
// data/db.sqlite会被git忽略,方式数据库文件被提交到git
|
||||
|
// 清空表,并初始化两条数据
|
||||
|
await this.usersRepository.clear(); |
||||
|
|
||||
|
await this.userService.create({ |
||||
|
id: 0, |
||||
|
password: '123456', |
||||
|
realName: 'Administrator', |
||||
|
roles: ['admin'], |
||||
|
username: 'vben', |
||||
|
}); |
||||
|
|
||||
|
await this.userService.create({ |
||||
|
id: 1, |
||||
|
password: '123456', |
||||
|
realName: 'Jack', |
||||
|
roles: ['user'], |
||||
|
username: 'jack', |
||||
|
}); |
||||
|
|
||||
|
const count = await this.usersRepository.count(); |
||||
|
console.log('Database has been initialized with seed data, count:', count); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { Public } from '@/core/decorator'; |
||||
|
import { Controller, Get } from '@nestjs/common'; |
||||
|
|
||||
|
@Controller() |
||||
|
export class HealthController { |
||||
|
@Public() |
||||
|
@Get() |
||||
|
getHeart(): string { |
||||
|
return 'ok'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { HealthController } from './health.controller'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [HealthController], |
||||
|
}) |
||||
|
export class HealthModule {} |
||||
@ -0,0 +1,12 @@ |
|||||
|
import { UserEntity } from '@/models/entity/user.entity'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
|
|
||||
|
import { UsersService } from './users.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
exports: [UsersService], |
||||
|
imports: [TypeOrmModule.forFeature([UserEntity])], |
||||
|
providers: [UsersService], |
||||
|
}) |
||||
|
export class UsersModule {} |
||||
@ -0,0 +1,27 @@ |
|||||
|
import type { Repository } from 'typeorm'; |
||||
|
|
||||
|
import { UserEntity } from '@/models/entity/user.entity'; |
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { InjectRepository } from '@nestjs/typeorm'; |
||||
|
import bcrypt from 'bcryptjs'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class UsersService { |
||||
|
constructor( |
||||
|
@InjectRepository(UserEntity) |
||||
|
private usersRepository: Repository<UserEntity>, |
||||
|
) {} |
||||
|
|
||||
|
async create(user: UserEntity): Promise<UserEntity> { |
||||
|
user.password = await bcrypt.hash(user.password, 10); // 密码哈希
|
||||
|
return this.usersRepository.save(user); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Find user by username |
||||
|
* @param username |
||||
|
*/ |
||||
|
async findOne(username: string): Promise<UserEntity | undefined> { |
||||
|
return await this.usersRepository.findOne({ where: { username } }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
interface AppConfig { |
||||
|
NODE_ENV: string; |
||||
|
apiPrefix: string; |
||||
|
port: number; |
||||
|
} |
||||
|
|
||||
|
interface JwtConfig { |
||||
|
expiresIn: string; |
||||
|
refreshSecret: string; |
||||
|
refreshexpiresIn: string; |
||||
|
secret: string; |
||||
|
} |
||||
|
export type { AppConfig, JwtConfig }; |
||||
@ -0,0 +1,7 @@ |
|||||
|
import { UserEntity } from '@/models/entity/user.entity'; |
||||
|
|
||||
|
declare global { |
||||
|
interface Request { |
||||
|
user?: UserEntity; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './config'; |
||||
|
export * from './jwt'; |
||||
@ -0,0 +1,7 @@ |
|||||
|
interface JwtPayload { |
||||
|
id: number; |
||||
|
roles: string[]; |
||||
|
username: string; |
||||
|
} |
||||
|
|
||||
|
export { JwtPayload }; |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"extends": "./tsconfig.json", |
||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"incremental": true, |
||||
|
"target": "ES2021", |
||||
|
"emitDecoratorMetadata": true, |
||||
|
"experimentalDecorators": true, |
||||
|
"baseUrl": "./", |
||||
|
"module": "commonjs", |
||||
|
"paths": { |
||||
|
"@/*": ["./src/*"] |
||||
|
}, |
||||
|
"strictBindCallApply": false, |
||||
|
"strictNullChecks": false, |
||||
|
"noFallthroughCasesInSwitch": false, |
||||
|
"noImplicitAny": false, |
||||
|
"declaration": true, |
||||
|
"outDir": "./dist", |
||||
|
"removeComments": true, |
||||
|
"sourceMap": true, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
"esModuleInterop": true, |
||||
|
"forceConsistentCasingInFileNames": false, |
||||
|
"skipLibCheck": true |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,3 @@ |
|||||
VITE_PUBLIC_PATH = / |
VITE_PUBLIC_PATH = / |
||||
|
|
||||
VITE_GLOB_API_URL=/vben-api |
VITE_GLOB_API_URL=/api |
||||
|
|||||
@ -1,33 +0,0 @@ |
|||||
function resultSuccess<T = Record<string, any>>( |
|
||||
result: T, |
|
||||
{ message = 'ok' } = {}, |
|
||||
) { |
|
||||
return { |
|
||||
code: 0, |
|
||||
message, |
|
||||
result, |
|
||||
type: 'success', |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
function resultError( |
|
||||
message = 'Request failed', |
|
||||
{ code = -1, result = null } = {}, |
|
||||
) { |
|
||||
return { |
|
||||
code, |
|
||||
message, |
|
||||
result, |
|
||||
type: 'error', |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @zh_CN 本函数用于从request数据中获取token,请根据项目的实际情况修改 |
|
||||
* |
|
||||
*/ |
|
||||
function getRequestToken({ headers }: any): string | undefined { |
|
||||
return headers?.authorization; |
|
||||
} |
|
||||
|
|
||||
export { getRequestToken, resultError, resultSuccess }; |
|
||||
@ -1,101 +0,0 @@ |
|||||
import { getRequestToken, resultError, resultSuccess } from './_util'; |
|
||||
|
|
||||
const fakeUserList = [ |
|
||||
{ |
|
||||
accessToken: 'fakeAdminToken', |
|
||||
avatar: '', |
|
||||
desc: 'manager', |
|
||||
homePath: '/', |
|
||||
password: '123456', |
|
||||
realName: 'Vben Admin', |
|
||||
roles: [ |
|
||||
{ |
|
||||
roleName: 'Super Admin', |
|
||||
value: 'super', |
|
||||
}, |
|
||||
], |
|
||||
userId: '1', |
|
||||
username: 'vben', |
|
||||
}, |
|
||||
{ |
|
||||
accessToken: 'fakeTestToken', |
|
||||
avatar: '', |
|
||||
desc: 'tester', |
|
||||
homePath: '/', |
|
||||
password: '123456', |
|
||||
realName: 'test user', |
|
||||
roles: [ |
|
||||
{ |
|
||||
roleName: 'Tester', |
|
||||
value: 'test', |
|
||||
}, |
|
||||
], |
|
||||
userId: '2', |
|
||||
username: 'test', |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
export default [ |
|
||||
{ |
|
||||
method: 'post', |
|
||||
response: ({ body }: any) => { |
|
||||
const { password, username } = body; |
|
||||
const checkUser = fakeUserList.find( |
|
||||
(item) => item.username === username && password === item.password, |
|
||||
); |
|
||||
if (!checkUser) { |
|
||||
return resultError('Incorrect account or password!'); |
|
||||
} |
|
||||
const { |
|
||||
accessToken, |
|
||||
desc, |
|
||||
realName, |
|
||||
roles, |
|
||||
userId, |
|
||||
username: _username, |
|
||||
} = checkUser; |
|
||||
return resultSuccess({ |
|
||||
accessToken, |
|
||||
desc, |
|
||||
realName, |
|
||||
roles, |
|
||||
userId, |
|
||||
username: _username, |
|
||||
}); |
|
||||
}, |
|
||||
timeout: 200, |
|
||||
url: '/vben-api/login', |
|
||||
}, |
|
||||
{ |
|
||||
method: 'get', |
|
||||
response: (request: any) => { |
|
||||
const token = getRequestToken(request); |
|
||||
if (!token) return resultError('Invalid token'); |
|
||||
const checkUser = fakeUserList.find((item) => item.accessToken === token); |
|
||||
if (!checkUser) { |
|
||||
return resultError( |
|
||||
'The corresponding user information was not obtained!', |
|
||||
); |
|
||||
} |
|
||||
const { accessToken: _token, password: _pwd, ...rest } = checkUser; |
|
||||
return resultSuccess(rest); |
|
||||
}, |
|
||||
url: '/vben-api/getUserInfo', |
|
||||
}, |
|
||||
{ |
|
||||
method: 'get', |
|
||||
response: (request: any) => { |
|
||||
const token = getRequestToken(request); |
|
||||
if (!token) return resultError('Invalid token'); |
|
||||
const checkUser = fakeUserList.find((item) => item.accessToken === token); |
|
||||
if (!checkUser) { |
|
||||
return resultError('Invalid token!'); |
|
||||
} |
|
||||
return resultSuccess(undefined, { |
|
||||
message: 'Token has been destroyed', |
|
||||
}); |
|
||||
}, |
|
||||
timeout: 200, |
|
||||
url: '/vben-api/logout', |
|
||||
}, |
|
||||
]; |
|
||||
@ -1,10 +0,0 @@ |
|||||
import { createProdMockServer } from 'vite-plugin-mock/client'; |
|
||||
|
|
||||
// 逐一导入您的mock.ts文件
|
|
||||
// 如果使用vite.mock.config.ts,只需直接导入文件
|
|
||||
// 可以使用 import.meta.glob功能来进行全部导入
|
|
||||
import userModule from '../mock/user'; |
|
||||
|
|
||||
export function setupProdMockServer() { |
|
||||
createProdMockServer([...userModule]); |
|
||||
} |
|
||||
File diff suppressed because it is too large
Loading…
Reference in new issue