feat: add backend-mock app

pull/48/MERGE
vben 2024-06-30 14:09:44 +08:00
parent c58aa26dbf
commit ca1cad0cd3
71 changed files with 3420 additions and 735 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ dev-dist
yarn.lock
package-lock.json
.VSCodeCounter
**/backend-mock/data
# local env files
.env.local

View File

@ -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
```

View File

@ -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,
},
],
};

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
@port = 5320
GET http://localhost:{{port}}/api HTTP/1.1
content-type: application/json

View File

@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": ["**/*.yml"],
"watchAssets": true,
"deleteOutDir": true
}
}

View File

@ -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"
}
}

View File

@ -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 {}

View File

@ -0,0 +1,8 @@
NODE_ENV: development
port: 5320
apiPrefix: /api
jwt:
secret: plonmGN4aSuMVnucrHuhnUoo49Wy
expiresIn: 1d
refreshSecret: 1lonmGN4aSuMVnucrHuhnUoo49Wy
refreshexpiresIn: 7d

View File

@ -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;

View File

@ -0,0 +1,8 @@
NODE_ENV: production
port: 5320
apiPrefix: /api
jwt:
secret: plonmGN4SuMVnucrHunUoo49Wy12
expiresIn: 1d
refreshSecret: 2lonmGN4aSuMVnucrHuhnUoo49Wy
refreshexpiresIn: 7d

View File

@ -0,0 +1 @@
export * from './public';

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
export * from './http-exception.filter';

View File

@ -0,0 +1,2 @@
export * from './jwt-auth.guard';
export * from './local-auth.guard';

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

View File

@ -0,0 +1 @@
export * from './transform.interceptor';

View File

@ -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',
};
}),
);
}
}

View File

@ -0,0 +1 @@
export * from './params.pipe';

View File

@ -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 };

View File

@ -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();

View File

@ -0,0 +1,5 @@
class RefreshTokenDto {
refreshToken: string;
}
export { RefreshTokenDto };

View File

@ -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 };

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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,
};
}
}

View File

@ -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;
}
}

View File

@ -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,
};
}
}

View File

@ -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 {}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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';
}
}

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View File

@ -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 {}

View File

@ -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 } });
}
}

View File

@ -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 };

View File

@ -0,0 +1,7 @@
import { UserEntity } from '@/models/entity/user.entity';
declare global {
interface Request {
user?: UserEntity;
}
}

View File

@ -0,0 +1,2 @@
export * from './config';
export * from './jwt';

View File

@ -0,0 +1,7 @@
interface JwtPayload {
id: number;
roles: string[];
username: string;
}
export { JwtPayload };

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -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
}
}

View File

@ -3,4 +3,4 @@
VITE_PUBLIC_PATH = /
# Basic interface address SPA
VITE_GLOB_API_URL=/vben-api
VITE_GLOB_API_URL=/api

View File

@ -1,3 +1,3 @@
VITE_PUBLIC_PATH = /
VITE_GLOB_API_URL=/vben-api
VITE_GLOB_API_URL=/api

View File

@ -2,4 +2,4 @@
VITE_PUBLIC_PATH = /
# Basic interface address SPA
VITE_GLOB_API_URL=/vben-api
VITE_GLOB_API_URL=/api

View File

@ -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 requesttoken
*
*/
function getRequestToken({ headers }: any): string | undefined {
return headers?.authorization;
}
export { getRequestToken, resultError, resultSuccess };

View File

@ -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',
},
];

View File

@ -46,8 +46,5 @@
"pinia": "2.1.7",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
},
"devDependencies": {
"vite-plugin-mock": "^3.0.2"
}
}

View File

@ -2,20 +2,20 @@ import type { UserInfo } from '@vben/types';
import type { UserApiType } from '../types';
import { get, post } from '#/forward';
import { requestClient } from '#/forward';
/**
*
*/
async function userLogin(data: UserApiType.LoginParams) {
return post<UserApiType.LoginResult>('/login', data);
return requestClient.post<UserApiType.LoginResult>('/auth/login', data);
}
/**
*
*/
async function getUserInfo() {
return get<UserInfo>('/getUserInfo');
return requestClient.get<UserInfo>('/auth/getUserInfo');
}
export { getUserInfo, userLogin };

View File

@ -10,6 +10,7 @@ namespace UserApiType {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}

View File

@ -22,13 +22,6 @@ async function bootstrap(namespace: string) {
app.use(router);
app.mount('#app');
// production mock server
if (import.meta.env.PROD) {
import('./mock-prod-server').then(({ setupProdMockServer }) => {
setupProdMockServer();
});
}
}
export { bootstrap };

View File

@ -15,8 +15,8 @@ interface HttpResponse<T = any> {
* 0 means success, others means fail
*/
code: number;
data: T;
message: string;
result: T;
}
/**
@ -31,7 +31,10 @@ function createRequestClient() {
return {
handler: () => {
const accessStore = useAccessStore();
return accessStore.getAccessToken;
return {
refreshToken: `Bearer ${accessStore.getRefreshToken}`,
token: `Bearer ${accessStore.getAccessToken}`,
};
},
// 默认
key: 'Authorization',
@ -39,23 +42,18 @@ function createRequestClient() {
},
});
setupRequestInterceptors(client);
const request = client.request.bind(client);
const get = client.get.bind(client);
const post = client.post.bind(client);
return {
get,
post,
request,
};
return client;
}
function setupRequestInterceptors(client: RequestClient) {
client.addResponseInterceptor(
(response: AxiosResponse<HttpResponse>) => {
const { data: responseData, status } = response;
const { code, message: msg, result } = responseData;
if (status === 200 && code === 0) {
return result;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
} else {
message.error(msg);
throw new Error(msg);
@ -73,17 +71,19 @@ function setupRequestInterceptors(client: RequestClient) {
} else if (error?.message?.includes?.('timeout')) {
errMsg = '请求超时。';
} else {
errMsg = error?.response?.data?.error?.message ?? '';
const data = error?.response?.data;
errMsg = (data?.message || data?.error?.message) ?? '';
}
message.error(errMsg);
return Promise.reject(error);
},
);
}
const { get, post, request } = createRequestClient();
const requestClient = createRequestClient();
// 其他配置的请求方法
// const { request: xxxRequest } = createRequest();
export { get, post, request };
export { requestClient };

View File

@ -28,8 +28,9 @@ async function initApplication() {
/**
* loading
* index.html app
* index.html app
* cssloading
*
*/
function destroyAppLoading() {
// 查找全局 loading 元素

View File

@ -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]);
}

View File

@ -4,6 +4,7 @@ import type { LoginAndRegisterParams } from '@vben/universal-ui';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH } from '@vben/constants';
import { $t } from '@vben/locales';
import { AuthenticationLogin } from '@vben/universal-ui';
import { useRequest } from '@vben-core/request';
@ -39,7 +40,7 @@ async function handleLogin(values: LoginAndRegisterParams) {
// accessToken
// Asynchronously handle the user login operation and obtain the accessToken
const { accessToken } = await runUserLogin(values);
const { accessToken, refreshToken } = await runUserLogin(values);
// accessToken
// If accessToken is successfully obtained
@ -47,15 +48,17 @@ async function handleLogin(values: LoginAndRegisterParams) {
// accessToken accessStore
// Store the accessToken in accessStore
accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// accessStore
// Get user information and store it in accessStore
const userInfo = await runGetUserInfo();
accessStore.setUserInfo(userInfo);
// homePath
// Redirect to the homePath defined in the user information
await router.push(userInfo.homePath);
await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
notification.success({
description: `${$t('authentication.login-success-desc')}:${userInfo.realName}`,
duration: 3,

View File

@ -46,10 +46,11 @@ export default defineConfig({
vite: {
server: {
proxy: {
'/vben-api': {
'/api': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/vben-api/, ''),
target: 'http://localhost:3000',
rewrite: (path) => path.replace(/^\/api/, ''),
// 代理目标地址 - backend-mock 项目
target: 'http://localhost:5320/api',
ws: true,
},
},

View File

@ -5,6 +5,7 @@
"words": [
"clsx",
"esno",
"typeorm",
"unref",
"taze",
"acmr",

View File

@ -32,8 +32,8 @@
"devDependencies": {
"@eslint/js": "^9.6.0",
"@types/eslint": "^8.56.10",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-eslint-comments": "^3.2.0",

View File

@ -50,7 +50,6 @@
"vite": "^5.3.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "^3.9.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-mock": "^3.0.2"
"vite-plugin-html": "^3.2.2"
}
}

View File

@ -27,7 +27,6 @@ function defineApplicationConfig(options: DefineApplicationOptions = {}) {
injectMetadata: true,
isBuild,
license: true,
mock: true,
mode,
pwa: true,
turboConsole: false,

View File

@ -20,7 +20,6 @@ import viteCompressPlugin from 'vite-plugin-compression';
import viteDtsPlugin from 'vite-plugin-dts';
import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
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 viteVueDevTools from 'vite-plugin-vue-devtools';
@ -107,7 +106,6 @@ async function getApplicationConditionPlugins(
importmapOptions,
injectAppLoading,
license,
mock,
pwa,
pwaOptions,
turboConsole,
@ -200,16 +198,6 @@ async function getApplicationConditionPlugins(
condition: !isBuild && !!turboConsole,
plugins: () => [viteTurboConsolePlugin()],
},
{
condition: !!mock,
plugins: () => [
viteMockPlugin({
enable: true,
ignore: /^_/,
mockPath: 'mock',
}),
],
},
]);
}
@ -242,7 +230,6 @@ export {
viteCompressPlugin,
viteDtsPlugin,
viteHtmlPlugin,
viteMockPlugin,
viteTurboConsolePlugin,
viteVisualizerPlugin,
};

View File

@ -71,8 +71,6 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
injectAppLoading?: boolean;
/** 是否注入版权信息 */
license?: boolean;
/** mock 插件配置 */
mock?: boolean;
/** 是否开启pwa */
pwa?: boolean;
/** pwa 插件配置 */

View File

@ -70,10 +70,10 @@
"rimraf": "^5.0.7",
"taze": "^0.14.0",
"turbo": "^2.0.6",
"typescript": "^5.5.2",
"typescript": "^5.5.3",
"unbuild": "^2.0.0",
"vite": "^5.3.2",
"vitest": "^2.0.0-beta.10",
"vitest": "^2.0.0-beta.12",
"vue-tsc": "^2.0.24"
},
"engines": {

View File

@ -30,6 +30,7 @@ class RequestClient {
* @param options - Axios
*/
constructor(options: RequestClientOptions = {}) {
this.bindMethods();
// 合并默认配置和传入的配置
const defaultConfig: CreateAxiosDefaults = {
headers: {
@ -63,6 +64,21 @@ class RequestClient {
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) {
return Promise.reject(error);
}
@ -71,8 +87,8 @@ class RequestClient {
this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
config.headers[authorization.key || 'Authorization'] =
authorization.handler?.();
const { token } = authorization.handler?.() ?? {};
config.headers[authorization.key || 'Authorization'] = token;
}
return config;
}, this.errorHandler);

View File

@ -7,7 +7,7 @@ type RequestContentType =
| 'multipart/form-data;charset=utf-8';
interface MakeAuthorization {
handler: () => null | string;
handler: () => { refreshToken: string; token: string } | null;
key?: string;
}

View File

@ -6,7 +6,6 @@ import { acceptHMRUpdate, defineStore } from 'pinia';
type AccessToken = null | string;
interface BasicUserInfo {
[key: string]: any;
/**
*
*/
@ -15,12 +14,14 @@ interface BasicUserInfo {
*
*/
realName: string;
/**
*
*/
roles?: string[];
/**
* id
*/
userId: string;
/**
*
*/
@ -40,6 +41,10 @@ interface AccessState {
* accessToken
*/
accessToken: AccessToken;
/**
* accessToken
*/
refreshToken: AccessToken;
/**
*
*/
@ -64,16 +69,15 @@ const useAccessStore = defineStore('access', {
setAccessToken(token: AccessToken) {
this.accessToken = token;
},
setRefreshToken(token: AccessToken) {
this.refreshToken = token;
},
setUserInfo(userInfo: BasicUserInfo) {
// 设置用户信息
this.userInfo = userInfo;
// 设置角色信息
const roles = userInfo?.roles ?? [];
const roleValues =
typeof roles[0] === 'string'
? roles
: roles.map((item: Record<string, any>) => item.value);
this.setUserRoles(roleValues);
this.setUserRoles(roles);
},
setUserRoles(roles: string[]) {
this.userRoles = roles;
@ -89,6 +93,9 @@ const useAccessStore = defineStore('access', {
getAccessToken(): AccessToken {
return this.accessToken;
},
getRefreshToken(): AccessToken {
return this.refreshToken;
},
getUserInfo(): BasicUserInfo | null {
return this.userInfo;
},
@ -98,13 +105,13 @@ const useAccessStore = defineStore('access', {
},
persist: {
// 持久化
// TODO: accessToken 过期时间
paths: ['accessToken', 'userRoles', 'userInfo'],
paths: ['accessToken', 'refreshToken', 'userRoles', 'userInfo'],
},
state: (): AccessState => ({
accessMenus: [],
accessRoutes: [],
accessToken: null,
refreshToken: null,
userInfo: null,
userRoles: [],
}),

View File

@ -1,10 +1,3 @@
interface RoleInfo {
/** 角色名 */
roleName: string;
/** 角色值 */
value: string;
}
/** 用户信息 */
interface UserInfo {
/**
@ -26,7 +19,7 @@ interface UserInfo {
/**
*
*/
roles: RoleInfo[];
roles: string[];
/**
* accessToken
*/
@ -41,4 +34,4 @@ interface UserInfo {
username: string;
}
export type { RoleInfo, UserInfo };
export type { UserInfo };

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,9 @@
{
"folders": [
{
"name": "@vben/backend-mock",
"path": "apps/backend-mock",
},
{
"name": "@vben/antd-view",
"path": "apps/web-antd",