feat: add backend-mock app
parent
c58aa26dbf
commit
ca1cad0cd3
|
@ -12,6 +12,7 @@ dev-dist
|
||||||
yarn.lock
|
yarn.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
|
**/backend-mock/data
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,4 +3,4 @@
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# Basic interface address SPA
|
# Basic interface address SPA
|
||||||
VITE_GLOB_API_URL=/vben-api
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
VITE_GLOB_API_URL=/vben-api
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# Basic interface address SPA
|
# Basic interface address SPA
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -46,8 +46,5 @@
|
||||||
"pinia": "2.1.7",
|
"pinia": "2.1.7",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite-plugin-mock": "^3.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,20 @@ import type { UserInfo } from '@vben/types';
|
||||||
|
|
||||||
import type { UserApiType } from '../types';
|
import type { UserApiType } from '../types';
|
||||||
|
|
||||||
import { get, post } from '#/forward';
|
import { requestClient } from '#/forward';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
async function userLogin(data: UserApiType.LoginParams) {
|
async function userLogin(data: UserApiType.LoginParams) {
|
||||||
return post<UserApiType.LoginResult>('/login', data);
|
return requestClient.post<UserApiType.LoginResult>('/auth/login', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户信息
|
* 获取用户信息
|
||||||
*/
|
*/
|
||||||
async function getUserInfo() {
|
async function getUserInfo() {
|
||||||
return get<UserInfo>('/getUserInfo');
|
return requestClient.get<UserInfo>('/auth/getUserInfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getUserInfo, userLogin };
|
export { getUserInfo, userLogin };
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace UserApiType {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
realName: string;
|
realName: string;
|
||||||
|
refreshToken: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,6 @@ async function bootstrap(namespace: string) {
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|
||||||
// production mock server
|
|
||||||
if (import.meta.env.PROD) {
|
|
||||||
import('./mock-prod-server').then(({ setupProdMockServer }) => {
|
|
||||||
setupProdMockServer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { bootstrap };
|
export { bootstrap };
|
||||||
|
|
|
@ -15,8 +15,8 @@ interface HttpResponse<T = any> {
|
||||||
* 0 means success, others means fail
|
* 0 means success, others means fail
|
||||||
*/
|
*/
|
||||||
code: number;
|
code: number;
|
||||||
|
data: T;
|
||||||
message: string;
|
message: string;
|
||||||
result: T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,10 @@ function createRequestClient() {
|
||||||
return {
|
return {
|
||||||
handler: () => {
|
handler: () => {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
return accessStore.getAccessToken;
|
return {
|
||||||
|
refreshToken: `Bearer ${accessStore.getRefreshToken}`,
|
||||||
|
token: `Bearer ${accessStore.getAccessToken}`,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
// 默认
|
// 默认
|
||||||
key: 'Authorization',
|
key: 'Authorization',
|
||||||
|
@ -39,23 +42,18 @@ function createRequestClient() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setupRequestInterceptors(client);
|
setupRequestInterceptors(client);
|
||||||
const request = client.request.bind(client);
|
return client;
|
||||||
const get = client.get.bind(client);
|
|
||||||
const post = client.post.bind(client);
|
|
||||||
return {
|
|
||||||
get,
|
|
||||||
post,
|
|
||||||
request,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupRequestInterceptors(client: RequestClient) {
|
function setupRequestInterceptors(client: RequestClient) {
|
||||||
client.addResponseInterceptor(
|
client.addResponseInterceptor(
|
||||||
(response: AxiosResponse<HttpResponse>) => {
|
(response: AxiosResponse<HttpResponse>) => {
|
||||||
const { data: responseData, status } = response;
|
const { data: responseData, status } = response;
|
||||||
const { code, message: msg, result } = responseData;
|
|
||||||
if (status === 200 && code === 0) {
|
const { code, data, message: msg } = responseData;
|
||||||
return result;
|
|
||||||
|
if (status >= 200 && status < 400 && code === 0) {
|
||||||
|
return data;
|
||||||
} else {
|
} else {
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
|
@ -73,17 +71,19 @@ function setupRequestInterceptors(client: RequestClient) {
|
||||||
} else if (error?.message?.includes?.('timeout')) {
|
} else if (error?.message?.includes?.('timeout')) {
|
||||||
errMsg = '请求超时。';
|
errMsg = '请求超时。';
|
||||||
} else {
|
} else {
|
||||||
errMsg = error?.response?.data?.error?.message ?? '';
|
const data = error?.response?.data;
|
||||||
|
errMsg = (data?.message || data?.error?.message) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
message.error(errMsg);
|
message.error(errMsg);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { get, post, request } = createRequestClient();
|
const requestClient = createRequestClient();
|
||||||
|
|
||||||
// 其他配置的请求方法
|
// 其他配置的请求方法
|
||||||
// const { request: xxxRequest } = createRequest();
|
// const { request: xxxRequest } = createRequest();
|
||||||
|
|
||||||
export { get, post, request };
|
export { requestClient };
|
||||||
|
|
|
@ -28,8 +28,9 @@ async function initApplication() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除并销毁loading
|
* 移除并销毁loading
|
||||||
* 放在这里是而不是放在 index.html 的app标签内,主要是因为这样比较不会生硬,渲染过快可能会有闪烁
|
* 放在这里是而不是放在 index.html 的app标签内,是因为这样比较不会生硬,渲染过快可能会有闪烁
|
||||||
* 通过先添加css动画隐藏,在动画结束后在移除loading节点来改善体验
|
* 通过先添加css动画隐藏,在动画结束后在移除loading节点来改善体验
|
||||||
|
* 不好的地方是会增加一些代码量
|
||||||
*/
|
*/
|
||||||
function destroyAppLoading() {
|
function destroyAppLoading() {
|
||||||
// 查找全局 loading 元素
|
// 查找全局 loading 元素
|
||||||
|
|
|
@ -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]);
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { AuthenticationLogin } from '@vben/universal-ui';
|
import { AuthenticationLogin } from '@vben/universal-ui';
|
||||||
import { useRequest } from '@vben-core/request';
|
import { useRequest } from '@vben-core/request';
|
||||||
|
@ -39,7 +40,7 @@ async function handleLogin(values: LoginAndRegisterParams) {
|
||||||
// 异步处理用户登录操作并获取 accessToken
|
// 异步处理用户登录操作并获取 accessToken
|
||||||
// Asynchronously handle the user login operation and obtain the accessToken
|
// Asynchronously handle the user login operation and obtain the accessToken
|
||||||
|
|
||||||
const { accessToken } = await runUserLogin(values);
|
const { accessToken, refreshToken } = await runUserLogin(values);
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
// 如果成功获取到 accessToken
|
||||||
// If accessToken is successfully obtained
|
// If accessToken is successfully obtained
|
||||||
|
@ -47,15 +48,17 @@ async function handleLogin(values: LoginAndRegisterParams) {
|
||||||
// 将 accessToken 存储到 accessStore 中
|
// 将 accessToken 存储到 accessStore 中
|
||||||
// Store the accessToken in accessStore
|
// Store the accessToken in accessStore
|
||||||
accessStore.setAccessToken(accessToken);
|
accessStore.setAccessToken(accessToken);
|
||||||
|
accessStore.setRefreshToken(refreshToken);
|
||||||
|
|
||||||
// 获取用户信息并存储到 accessStore 中
|
// 获取用户信息并存储到 accessStore 中
|
||||||
// Get user information and store it in accessStore
|
// Get user information and store it in accessStore
|
||||||
const userInfo = await runGetUserInfo();
|
const userInfo = await runGetUserInfo();
|
||||||
|
|
||||||
accessStore.setUserInfo(userInfo);
|
accessStore.setUserInfo(userInfo);
|
||||||
|
|
||||||
// 跳转到用户信息中定义的 homePath 路径
|
// 跳转到用户信息中定义的 homePath 路径
|
||||||
// Redirect to the homePath defined in the user information
|
// Redirect to the homePath defined in the user information
|
||||||
await router.push(userInfo.homePath);
|
await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
|
||||||
notification.success({
|
notification.success({
|
||||||
description: `${$t('authentication.login-success-desc')}:${userInfo.realName}`,
|
description: `${$t('authentication.login-success-desc')}:${userInfo.realName}`,
|
||||||
duration: 3,
|
duration: 3,
|
||||||
|
|
|
@ -46,10 +46,11 @@ export default defineConfig({
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/vben-api': {
|
'/api': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/vben-api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
target: 'http://localhost:3000',
|
// 代理目标地址 - backend-mock 项目
|
||||||
|
target: 'http://localhost:5320/api',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"words": [
|
"words": [
|
||||||
"clsx",
|
"clsx",
|
||||||
"esno",
|
"esno",
|
||||||
|
"typeorm",
|
||||||
"unref",
|
"unref",
|
||||||
"taze",
|
"taze",
|
||||||
"acmr",
|
"acmr",
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.6.0",
|
"@eslint/js": "^9.6.0",
|
||||||
"@types/eslint": "^8.56.10",
|
"@types/eslint": "^8.56.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||||
"@typescript-eslint/parser": "^7.14.1",
|
"@typescript-eslint/parser": "^7.15.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
|
|
|
@ -50,7 +50,6 @@
|
||||||
"vite": "^5.3.2",
|
"vite": "^5.3.2",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dts": "^3.9.1",
|
"vite-plugin-dts": "^3.9.1",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2"
|
||||||
"vite-plugin-mock": "^3.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ function defineApplicationConfig(options: DefineApplicationOptions = {}) {
|
||||||
injectMetadata: true,
|
injectMetadata: true,
|
||||||
isBuild,
|
isBuild,
|
||||||
license: true,
|
license: true,
|
||||||
mock: true,
|
|
||||||
mode,
|
mode,
|
||||||
pwa: true,
|
pwa: true,
|
||||||
turboConsole: false,
|
turboConsole: false,
|
||||||
|
|
|
@ -20,7 +20,6 @@ import viteCompressPlugin from 'vite-plugin-compression';
|
||||||
import viteDtsPlugin from 'vite-plugin-dts';
|
import viteDtsPlugin from 'vite-plugin-dts';
|
||||||
import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
|
import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
|
||||||
import { libInjectCss as viteLibInjectCss } from 'vite-plugin-lib-inject-css';
|
import { libInjectCss as viteLibInjectCss } from 'vite-plugin-lib-inject-css';
|
||||||
import { viteMockServe as viteMockPlugin } from 'vite-plugin-mock';
|
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import viteVueDevTools from 'vite-plugin-vue-devtools';
|
import viteVueDevTools from 'vite-plugin-vue-devtools';
|
||||||
|
|
||||||
|
@ -107,7 +106,6 @@ async function getApplicationConditionPlugins(
|
||||||
importmapOptions,
|
importmapOptions,
|
||||||
injectAppLoading,
|
injectAppLoading,
|
||||||
license,
|
license,
|
||||||
mock,
|
|
||||||
pwa,
|
pwa,
|
||||||
pwaOptions,
|
pwaOptions,
|
||||||
turboConsole,
|
turboConsole,
|
||||||
|
@ -200,16 +198,6 @@ async function getApplicationConditionPlugins(
|
||||||
condition: !isBuild && !!turboConsole,
|
condition: !isBuild && !!turboConsole,
|
||||||
plugins: () => [viteTurboConsolePlugin()],
|
plugins: () => [viteTurboConsolePlugin()],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
condition: !!mock,
|
|
||||||
plugins: () => [
|
|
||||||
viteMockPlugin({
|
|
||||||
enable: true,
|
|
||||||
ignore: /^_/,
|
|
||||||
mockPath: 'mock',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +230,6 @@ export {
|
||||||
viteCompressPlugin,
|
viteCompressPlugin,
|
||||||
viteDtsPlugin,
|
viteDtsPlugin,
|
||||||
viteHtmlPlugin,
|
viteHtmlPlugin,
|
||||||
viteMockPlugin,
|
|
||||||
viteTurboConsolePlugin,
|
viteTurboConsolePlugin,
|
||||||
viteVisualizerPlugin,
|
viteVisualizerPlugin,
|
||||||
};
|
};
|
||||||
|
|
|
@ -71,8 +71,6 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
|
||||||
injectAppLoading?: boolean;
|
injectAppLoading?: boolean;
|
||||||
/** 是否注入版权信息 */
|
/** 是否注入版权信息 */
|
||||||
license?: boolean;
|
license?: boolean;
|
||||||
/** mock 插件配置 */
|
|
||||||
mock?: boolean;
|
|
||||||
/** 是否开启pwa */
|
/** 是否开启pwa */
|
||||||
pwa?: boolean;
|
pwa?: boolean;
|
||||||
/** pwa 插件配置 */
|
/** pwa 插件配置 */
|
||||||
|
|
|
@ -70,10 +70,10 @@
|
||||||
"rimraf": "^5.0.7",
|
"rimraf": "^5.0.7",
|
||||||
"taze": "^0.14.0",
|
"taze": "^0.14.0",
|
||||||
"turbo": "^2.0.6",
|
"turbo": "^2.0.6",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.3",
|
||||||
"unbuild": "^2.0.0",
|
"unbuild": "^2.0.0",
|
||||||
"vite": "^5.3.2",
|
"vite": "^5.3.2",
|
||||||
"vitest": "^2.0.0-beta.10",
|
"vitest": "^2.0.0-beta.12",
|
||||||
"vue-tsc": "^2.0.24"
|
"vue-tsc": "^2.0.24"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -30,6 +30,7 @@ class RequestClient {
|
||||||
* @param options - Axios请求配置,可选
|
* @param options - Axios请求配置,可选
|
||||||
*/
|
*/
|
||||||
constructor(options: RequestClientOptions = {}) {
|
constructor(options: RequestClientOptions = {}) {
|
||||||
|
this.bindMethods();
|
||||||
// 合并默认配置和传入的配置
|
// 合并默认配置和传入的配置
|
||||||
const defaultConfig: CreateAxiosDefaults = {
|
const defaultConfig: CreateAxiosDefaults = {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -63,6 +64,21 @@ class RequestClient {
|
||||||
this.setupInterceptors();
|
this.setupInterceptors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bindMethods() {
|
||||||
|
const propertyNames = Object.getOwnPropertyNames(
|
||||||
|
Object.getPrototypeOf(this),
|
||||||
|
);
|
||||||
|
propertyNames.forEach((propertyName) => {
|
||||||
|
const propertyValue = (this as any)[propertyName];
|
||||||
|
if (
|
||||||
|
typeof propertyValue === 'function' &&
|
||||||
|
propertyName !== 'constructor'
|
||||||
|
) {
|
||||||
|
(this as any)[propertyName] = propertyValue.bind(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private errorHandler(error: any) {
|
private errorHandler(error: any) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
@ -71,8 +87,8 @@ class RequestClient {
|
||||||
this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
|
this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
|
||||||
const authorization = this.makeAuthorization?.(config);
|
const authorization = this.makeAuthorization?.(config);
|
||||||
if (authorization) {
|
if (authorization) {
|
||||||
config.headers[authorization.key || 'Authorization'] =
|
const { token } = authorization.handler?.() ?? {};
|
||||||
authorization.handler?.();
|
config.headers[authorization.key || 'Authorization'] = token;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}, this.errorHandler);
|
}, this.errorHandler);
|
||||||
|
|
|
@ -7,7 +7,7 @@ type RequestContentType =
|
||||||
| 'multipart/form-data;charset=utf-8';
|
| 'multipart/form-data;charset=utf-8';
|
||||||
|
|
||||||
interface MakeAuthorization {
|
interface MakeAuthorization {
|
||||||
handler: () => null | string;
|
handler: () => { refreshToken: string; token: string } | null;
|
||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||||
type AccessToken = null | string;
|
type AccessToken = null | string;
|
||||||
|
|
||||||
interface BasicUserInfo {
|
interface BasicUserInfo {
|
||||||
[key: string]: any;
|
|
||||||
/**
|
/**
|
||||||
* 头像
|
* 头像
|
||||||
*/
|
*/
|
||||||
|
@ -15,12 +14,14 @@ interface BasicUserInfo {
|
||||||
* 用户昵称
|
* 用户昵称
|
||||||
*/
|
*/
|
||||||
realName: string;
|
realName: string;
|
||||||
|
/**
|
||||||
|
* 用户角色
|
||||||
|
*/
|
||||||
|
roles?: string[];
|
||||||
/**
|
/**
|
||||||
* 用户id
|
* 用户id
|
||||||
*/
|
*/
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户名
|
* 用户名
|
||||||
*/
|
*/
|
||||||
|
@ -40,6 +41,10 @@ interface AccessState {
|
||||||
* 登录 accessToken
|
* 登录 accessToken
|
||||||
*/
|
*/
|
||||||
accessToken: AccessToken;
|
accessToken: AccessToken;
|
||||||
|
/**
|
||||||
|
* 登录 accessToken
|
||||||
|
*/
|
||||||
|
refreshToken: AccessToken;
|
||||||
/**
|
/**
|
||||||
* 用户信息
|
* 用户信息
|
||||||
*/
|
*/
|
||||||
|
@ -64,16 +69,15 @@ const useAccessStore = defineStore('access', {
|
||||||
setAccessToken(token: AccessToken) {
|
setAccessToken(token: AccessToken) {
|
||||||
this.accessToken = token;
|
this.accessToken = token;
|
||||||
},
|
},
|
||||||
|
setRefreshToken(token: AccessToken) {
|
||||||
|
this.refreshToken = token;
|
||||||
|
},
|
||||||
setUserInfo(userInfo: BasicUserInfo) {
|
setUserInfo(userInfo: BasicUserInfo) {
|
||||||
// 设置用户信息
|
// 设置用户信息
|
||||||
this.userInfo = userInfo;
|
this.userInfo = userInfo;
|
||||||
// 设置角色信息
|
// 设置角色信息
|
||||||
const roles = userInfo?.roles ?? [];
|
const roles = userInfo?.roles ?? [];
|
||||||
const roleValues =
|
this.setUserRoles(roles);
|
||||||
typeof roles[0] === 'string'
|
|
||||||
? roles
|
|
||||||
: roles.map((item: Record<string, any>) => item.value);
|
|
||||||
this.setUserRoles(roleValues);
|
|
||||||
},
|
},
|
||||||
setUserRoles(roles: string[]) {
|
setUserRoles(roles: string[]) {
|
||||||
this.userRoles = roles;
|
this.userRoles = roles;
|
||||||
|
@ -89,6 +93,9 @@ const useAccessStore = defineStore('access', {
|
||||||
getAccessToken(): AccessToken {
|
getAccessToken(): AccessToken {
|
||||||
return this.accessToken;
|
return this.accessToken;
|
||||||
},
|
},
|
||||||
|
getRefreshToken(): AccessToken {
|
||||||
|
return this.refreshToken;
|
||||||
|
},
|
||||||
getUserInfo(): BasicUserInfo | null {
|
getUserInfo(): BasicUserInfo | null {
|
||||||
return this.userInfo;
|
return this.userInfo;
|
||||||
},
|
},
|
||||||
|
@ -98,13 +105,13 @@ const useAccessStore = defineStore('access', {
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
// 持久化
|
// 持久化
|
||||||
// TODO: accessToken 过期时间
|
paths: ['accessToken', 'refreshToken', 'userRoles', 'userInfo'],
|
||||||
paths: ['accessToken', 'userRoles', 'userInfo'],
|
|
||||||
},
|
},
|
||||||
state: (): AccessState => ({
|
state: (): AccessState => ({
|
||||||
accessMenus: [],
|
accessMenus: [],
|
||||||
accessRoutes: [],
|
accessRoutes: [],
|
||||||
accessToken: null,
|
accessToken: null,
|
||||||
|
refreshToken: null,
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
userRoles: [],
|
userRoles: [],
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
interface RoleInfo {
|
|
||||||
/** 角色名 */
|
|
||||||
roleName: string;
|
|
||||||
/** 角色值 */
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 用户信息 */
|
/** 用户信息 */
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
/**
|
/**
|
||||||
|
@ -26,7 +19,7 @@ interface UserInfo {
|
||||||
/**
|
/**
|
||||||
* 用户角色信息
|
* 用户角色信息
|
||||||
*/
|
*/
|
||||||
roles: RoleInfo[];
|
roles: string[];
|
||||||
/**
|
/**
|
||||||
* accessToken
|
* accessToken
|
||||||
*/
|
*/
|
||||||
|
@ -41,4 +34,4 @@ interface UserInfo {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { RoleInfo, UserInfo };
|
export type { UserInfo };
|
||||||
|
|
3001
pnpm-lock.yaml
3001
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"folders": [
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "@vben/backend-mock",
|
||||||
|
"path": "apps/backend-mock",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "@vben/antd-view",
|
"name": "@vben/antd-view",
|
||||||
"path": "apps/web-antd",
|
"path": "apps/web-antd",
|
||||||
|
|
Loading…
Reference in New Issue