vben
2 months ago
122 changed files with 2575 additions and 2986 deletions
-
2.gitignore
-
2.prettierignore
-
3.vscode/settings.json
-
1apps/backend-mock/.env
-
5apps/backend-mock/README.md
-
15apps/backend-mock/api/auth/codes.ts
-
20apps/backend-mock/api/auth/login.post.ts
-
14apps/backend-mock/api/menu/all.ts
-
5apps/backend-mock/api/status.ts
-
1apps/backend-mock/api/test.get.ts
-
1apps/backend-mock/api/test.post.ts
-
14apps/backend-mock/api/user/info.ts
-
23apps/backend-mock/ecosystem.config.cjs
-
7apps/backend-mock/error.ts
-
20apps/backend-mock/http/auth.http
-
3apps/backend-mock/http/health.http
-
6apps/backend-mock/http/menu.http
-
10apps/backend-mock/nest-cli.json
-
6apps/backend-mock/nitro.config.ts
-
37apps/backend-mock/package.json
-
12apps/backend-mock/routes/[...].ts
-
34apps/backend-mock/src/app.module.ts
-
8apps/backend-mock/src/config/dev.yml
-
23apps/backend-mock/src/config/index.ts
-
8apps/backend-mock/src/config/prod.yml
-
1apps/backend-mock/src/core/decorator/index.ts
-
4apps/backend-mock/src/core/decorator/public.ts
-
40apps/backend-mock/src/core/filter/http-exception.filter.ts
-
1apps/backend-mock/src/core/filter/index.ts
-
2apps/backend-mock/src/core/guard/index.ts
-
23apps/backend-mock/src/core/guard/jwt-auth.guard.ts
-
5apps/backend-mock/src/core/guard/local-auth.guard.ts
-
1apps/backend-mock/src/core/interceptor/index.ts
-
37apps/backend-mock/src/core/interceptor/transform.interceptor.ts
-
1apps/backend-mock/src/core/pipe/index.ts
-
27apps/backend-mock/src/core/pipe/params.pipe.ts
-
51apps/backend-mock/src/main.ts
-
5apps/backend-mock/src/models/dto/auth.dto.ts
-
9apps/backend-mock/src/models/dto/user.dto.ts
-
21apps/backend-mock/src/models/entity/user.entity.ts
-
59apps/backend-mock/src/modules/auth/auth.controller.ts
-
33apps/backend-mock/src/modules/auth/auth.module.ts
-
94apps/backend-mock/src/modules/auth/auth.service.ts
-
26apps/backend-mock/src/modules/auth/jwt.strategy.ts
-
20apps/backend-mock/src/modules/auth/local.strategy.ts
-
29apps/backend-mock/src/modules/auth/refresh-token.strategy.ts
-
11apps/backend-mock/src/modules/health/health.controller.ts
-
8apps/backend-mock/src/modules/health/health.module.ts
-
157apps/backend-mock/src/modules/menu/menu.controller.ts
-
10apps/backend-mock/src/modules/menu/menu.module.ts
-
4apps/backend-mock/src/modules/menu/menu.service.ts
-
1apps/backend-mock/src/modules/mock/mock-db.json
-
23apps/backend-mock/src/modules/mock/mock.controller.ts
-
13apps/backend-mock/src/modules/mock/mock.interface.ts
-
11apps/backend-mock/src/modules/mock/mock.module.ts
-
80apps/backend-mock/src/modules/mock/mock.service.ts
-
11apps/backend-mock/src/modules/users/users.module.ts
-
18apps/backend-mock/src/modules/users/users.service.ts
-
13apps/backend-mock/src/types/config.ts
-
7apps/backend-mock/src/types/express.d.ts
-
2apps/backend-mock/src/types/index.ts
-
7apps/backend-mock/src/types/jwt.ts
-
5apps/backend-mock/src/utils/index.ts
-
24apps/backend-mock/tsconfig.json
-
178apps/backend-mock/utils/mock-data.ts
-
17apps/backend-mock/utils/response.ts
-
2apps/web-antd/.env.development
-
4apps/web-antd/package.json
-
17apps/web-antd/src/apis/core/auth.ts
-
2apps/web-antd/src/apis/core/index.ts
-
6apps/web-antd/src/apis/core/menu.ts
-
10apps/web-antd/src/apis/core/user.ts
-
1apps/web-antd/src/apis/demos/index.ts
-
2apps/web-antd/src/apis/demos/status.ts
-
3apps/web-antd/src/apis/index.ts
-
28apps/web-antd/src/apis/modules/user.ts
-
4apps/web-antd/src/forward/request.ts
-
13apps/web-antd/src/locales/langs/en-US.json
-
13apps/web-antd/src/locales/langs/zh-CN.json
-
99apps/web-antd/src/router/routes/modules/demos.ts
-
6apps/web-antd/src/router/routes/modules/vben.ts
-
4apps/web-antd/src/store/modules/access.ts
-
35apps/web-antd/src/views/demos/access/button-control.vue
-
8apps/web-antd/src/views/demos/access/index.vue
-
0apps/web-antd/src/views/demos/breadcrumb/lateral-detail.vue
-
0apps/web-antd/src/views/demos/breadcrumb/lateral.vue
-
0apps/web-antd/src/views/demos/breadcrumb/level-detail.vue
-
14apps/web-antd/src/views/demos/features/login-expired/index.vue
-
86apps/web-antd/src/views/demos/features/tabs/index.vue
-
4internal/lint-configs/commitlint-config/package.json
-
2internal/lint-configs/eslint-config/src/configs/ignores.ts
-
7internal/lint-configs/eslint-config/src/custom-config.ts
-
1internal/vite-config/package.json
-
6internal/vite-config/src/config/application.ts
-
2internal/vite-config/src/plugins/extra-app-config.ts
-
1internal/vite-config/src/plugins/importmap.ts
-
18internal/vite-config/src/plugins/index.ts
-
1internal/vite-config/src/plugins/inject-metadata.ts
-
89internal/vite-config/src/plugins/nitor-mock.ts
-
28internal/vite-config/src/plugins/print.ts
@ -0,0 +1 @@ |
|||||
|
PORT=5320 |
@ -0,0 +1,15 @@ |
|||||
|
export default eventHandler((event) => { |
||||
|
const token = getHeader(event, 'Authorization'); |
||||
|
|
||||
|
if (!token) { |
||||
|
setResponseStatus(event, 401); |
||||
|
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
||||
|
} |
||||
|
|
||||
|
const username = Buffer.from(token, 'base64').toString('utf8'); |
||||
|
|
||||
|
const codes = |
||||
|
MOCK_CODES.find((item) => item.username === username)?.codes ?? []; |
||||
|
|
||||
|
return useResponseSuccess(codes); |
||||
|
}); |
@ -0,0 +1,20 @@ |
|||||
|
export default defineEventHandler(async (event) => { |
||||
|
const { password, username } = await readBody(event); |
||||
|
|
||||
|
const findUser = MOCK_USERS.find( |
||||
|
(item) => item.username === username && item.password === password, |
||||
|
); |
||||
|
|
||||
|
if (!findUser) { |
||||
|
setResponseStatus(event, 403); |
||||
|
return useResponseError('UnauthorizedException', '用户名或密码错误'); |
||||
|
} |
||||
|
|
||||
|
const accessToken = Buffer.from(username).toString('base64'); |
||||
|
|
||||
|
return useResponseSuccess({ |
||||
|
accessToken, |
||||
|
// TODO: refresh token
|
||||
|
refreshToken: accessToken, |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,14 @@ |
|||||
|
export default eventHandler((event) => { |
||||
|
const token = getHeader(event, 'Authorization'); |
||||
|
|
||||
|
if (!token) { |
||||
|
setResponseStatus(event, 401); |
||||
|
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
||||
|
} |
||||
|
|
||||
|
const username = Buffer.from(token, 'base64').toString('utf8'); |
||||
|
|
||||
|
const menus = |
||||
|
MOCK_MENUS.find((item) => item.username === username)?.menus ?? []; |
||||
|
return useResponseSuccess(menus); |
||||
|
}); |
@ -0,0 +1,5 @@ |
|||||
|
export default eventHandler((event) => { |
||||
|
const { status } = getQuery(event); |
||||
|
setResponseStatus(event, Number(status)); |
||||
|
return useResponseError(`${status}`); |
||||
|
}); |
@ -0,0 +1 @@ |
|||||
|
export default defineEventHandler(() => 'Test get handler'); |
@ -0,0 +1 @@ |
|||||
|
export default defineEventHandler(() => 'Test post handler'); |
@ -0,0 +1,14 @@ |
|||||
|
export default eventHandler((event) => { |
||||
|
const token = getHeader(event, 'Authorization'); |
||||
|
if (!token) { |
||||
|
setResponseStatus(event, 401); |
||||
|
return useResponseError('UnauthorizedException', 'Unauthorized Exception'); |
||||
|
} |
||||
|
|
||||
|
const username = Buffer.from(token, 'base64').toString('utf8'); |
||||
|
|
||||
|
const user = MOCK_USERS.find((item) => item.username === username); |
||||
|
|
||||
|
const { password: _pwd, ...userInfo } = user; |
||||
|
return useResponseSuccess(userInfo); |
||||
|
}); |
@ -1,23 +0,0 @@ |
|||||
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,7 @@ |
|||||
|
import type { NitroErrorHandler } from 'nitropack'; |
||||
|
|
||||
|
const errorHandler: NitroErrorHandler = function (error, event) { |
||||
|
event.res.end(`[error handler] ${error.stack}`); |
||||
|
}; |
||||
|
|
||||
|
export default errorHandler; |
@ -1,20 +0,0 @@ |
|||||
@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" |
|
||||
} |
|
@ -1,3 +0,0 @@ |
|||||
@port = 5320 |
|
||||
GET http://localhost:{{port}}/api HTTP/1.1 |
|
||||
content-type: application/json |
|
@ -1,6 +0,0 @@ |
|||||
@port = 5320 |
|
||||
@type = application/json |
|
||||
@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwicm9sZXMiOlsiYWRtaW4iXSwidXNlcm5hbWUiOiJ2YmVuIiwiaWF0IjoxNzE5ODkwMTEwLCJleHAiOjE3MTk5NzY1MTB9.eyAFsQ2Jk_mAQGvrEL1jF9O6YmLZ_PSYj5aokL6fCuU |
|
||||
GET http://localhost:{{port}}/api/menu/getAll HTTP/1.1 |
|
||||
content-type: {{ type }} |
|
||||
Authorization: {{ token }} |
|
@ -1,10 +0,0 @@ |
|||||
{ |
|
||||
"$schema": "https://json.schemastore.org/nest-cli", |
|
||||
"collection": "@nestjs/schematics", |
|
||||
"sourceRoot": "src", |
|
||||
"compilerOptions": { |
|
||||
"assets": ["**/*.yml", "**/*.json"], |
|
||||
"watchAssets": true, |
|
||||
"deleteOutDir": true |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,6 @@ |
|||||
|
import errorHandler from './error'; |
||||
|
|
||||
|
export default defineNitroConfig({ |
||||
|
devErrorHandler: errorHandler, |
||||
|
errorHandler: '~/error', |
||||
|
}); |
@ -0,0 +1,12 @@ |
|||||
|
export default defineEventHandler(() => { |
||||
|
return `
|
||||
|
<h1>Hello Vben Admin</h1> |
||||
|
<h2>Mock service is starting</h2> |
||||
|
<ul> |
||||
|
<li><a href="/api/user">/api/user/info</a></li> |
||||
|
<li><a href="/api/menu">/api/menu/all</a></li> |
||||
|
<li><a href="/api/auth/codes">/api/auth/codes</a></li> |
||||
|
<li><a href="/api/auth/login">/api/auth/login</a></li> |
||||
|
</ul> |
||||
|
`;
|
||||
|
}); |
@ -1,34 +0,0 @@ |
|||||
import configuration from '@/config/index'; |
|
||||
import { Module } from '@nestjs/common'; |
|
||||
import { ConfigModule } from '@nestjs/config'; |
|
||||
import Joi from 'joi'; |
|
||||
|
|
||||
import { AuthModule } from './modules/auth/auth.module'; |
|
||||
import { HealthModule } from './modules/health/health.module'; |
|
||||
import { MenuModule } from './modules/menu/menu.module'; |
|
||||
import { MockModule } from './modules/mock/mock.module'; |
|
||||
import { UsersModule } from './modules/users/users.module'; |
|
||||
|
|
||||
@Module({ |
|
||||
imports: [ |
|
||||
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, |
|
||||
MenuModule, |
|
||||
MockModule, |
|
||||
], |
|
||||
}) |
|
||||
export class AppModule {} |
|
@ -1,8 +0,0 @@ |
|||||
NODE_ENV: development |
|
||||
port: 5320 |
|
||||
apiPrefix: /api |
|
||||
jwt: |
|
||||
secret: plonmGN4aSuMVnucrHuhnUoo49Wy |
|
||||
expiresIn: 1d |
|
||||
refreshSecret: 1lonmGN4aSuMVnucrHuhnUoo49Wy |
|
||||
refreshexpiresIn: 7d |
|
@ -1,23 +0,0 @@ |
|||||
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; |
|
@ -1,8 +0,0 @@ |
|||||
NODE_ENV: production |
|
||||
port: 5320 |
|
||||
apiPrefix: /api |
|
||||
jwt: |
|
||||
secret: plonmGN4SuMVnucrHunUoo49Wy12 |
|
||||
expiresIn: 1d |
|
||||
refreshSecret: 2lonmGN4aSuMVnucrHuhnUoo49Wy |
|
||||
refreshexpiresIn: 7d |
|
@ -1 +0,0 @@ |
|||||
export * from './public'; |
|
@ -1,4 +0,0 @@ |
|||||
import { SetMetadata } from '@nestjs/common'; |
|
||||
|
|
||||
export const IS_PUBLIC_KEY = 'isPublic'; |
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); |
|
@ -1,40 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export * from './http-exception.filter'; |
|
@ -1,2 +0,0 @@ |
|||||
export * from './jwt-auth.guard'; |
|
||||
export * from './local-auth.guard'; |
|
@ -1,23 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
} |
|
@ -1,5 +0,0 @@ |
|||||
import { Injectable } from '@nestjs/common'; |
|
||||
import { AuthGuard } from '@nestjs/passport'; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class LocalAuthGuard extends AuthGuard('local') {} |
|
@ -1 +0,0 @@ |
|||||
export * from './transform.interceptor'; |
|
@ -1,37 +0,0 @@ |
|||||
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', |
|
||||
}; |
|
||||
}), |
|
||||
); |
|
||||
} |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export * from './params.pipe'; |
|
@ -1,27 +0,0 @@ |
|||||
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 }; |
|
@ -1,51 +0,0 @@ |
|||||
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(); |
|
@ -1,5 +0,0 @@ |
|||||
class RefreshTokenDto { |
|
||||
refreshToken: string; |
|
||||
} |
|
||||
|
|
||||
export { RefreshTokenDto }; |
|
@ -1,9 +0,0 @@ |
|||||
class CreateUserDto { |
|
||||
id: number; |
|
||||
password: string; |
|
||||
realName: string; |
|
||||
roles: string[]; |
|
||||
username: string; |
|
||||
} |
|
||||
|
|
||||
export { CreateUserDto }; |
|
@ -1,21 +0,0 @@ |
|||||
class UserEntity { |
|
||||
id: number; |
|
||||
/** |
|
||||
* 密码 |
|
||||
*/ |
|
||||
password: string; |
|
||||
/** |
|
||||
* 真实姓名 |
|
||||
*/ |
|
||||
realName: string; |
|
||||
/** |
|
||||
* 角色 |
|
||||
*/ |
|
||||
roles: string[]; |
|
||||
/** |
|
||||
* 用户名 |
|
||||
*/ |
|
||||
username: string; |
|
||||
} |
|
||||
|
|
||||
export { UserEntity }; |
|
@ -1,59 +0,0 @@ |
|||||
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('getAccessCodes') |
|
||||
@HttpCode(HttpStatus.OK) |
|
||||
async getAccessCodes(@Request() req: Request) { |
|
||||
return await this.authService.getAccessCodes(req.user.username); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取用户信息 |
|
||||
* @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); |
|
||||
} |
|
||||
} |
|
@ -1,33 +0,0 @@ |
|||||
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 {} |
|
@ -1,94 +0,0 @@ |
|||||
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 getAccessCodes(username: string): Promise<string[]> { |
|
||||
const user = await this.usersService.findOne(username); |
|
||||
|
|
||||
const mockCodes = [ |
|
||||
// super
|
|
||||
{ |
|
||||
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'], |
|
||||
userId: 0, |
|
||||
}, |
|
||||
{ |
|
||||
// admin
|
|
||||
codes: ['AC_100010', 'AC_100020', 'AC_100030'], |
|
||||
userId: 1, |
|
||||
}, |
|
||||
{ |
|
||||
// user
|
|
||||
codes: ['AC_1000001', 'AC_1000002'], |
|
||||
userId: 2, |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
return mockCodes.find((item) => item.userId === user.id)?.codes ?? []; |
|
||||
} |
|
||||
|
|
||||
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; |
|
||||
} |
|
||||
} |
|
@ -1,26 +0,0 @@ |
|||||
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, |
|
||||
}; |
|
||||
} |
|
||||
} |
|
@ -1,20 +0,0 @@ |
|||||
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; |
|
||||
} |
|
||||
} |
|
@ -1,29 +0,0 @@ |
|||||
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, |
|
||||
}; |
|
||||
} |
|
||||
} |
|
@ -1,11 +0,0 @@ |
|||||
import { Public } from '@/core/decorator'; |
|
||||
import { Controller, Get } from '@nestjs/common'; |
|
||||
|
|
||||
@Controller() |
|
||||
export class HealthController { |
|
||||
@Public() |
|
||||
@Get() |
|
||||
getHeart(): string { |
|
||||
return 'ok'; |
|
||||
} |
|
||||
} |
|
@ -1,8 +0,0 @@ |
|||||
import { Module } from '@nestjs/common'; |
|
||||
|
|
||||
import { HealthController } from './health.controller'; |
|
||||
|
|
||||
@Module({ |
|
||||
controllers: [HealthController], |
|
||||
}) |
|
||||
export class HealthModule {} |
|
@ -1,157 +0,0 @@ |
|||||
import { sleep } from '@/utils'; |
|
||||
import { Controller, Get, HttpCode, HttpStatus, Request } from '@nestjs/common'; |
|
||||
|
|
||||
@Controller('menu') |
|
||||
export class MenuController { |
|
||||
/** |
|
||||
* 获取用户所有菜单 |
|
||||
*/ |
|
||||
@Get('getAll') |
|
||||
@HttpCode(HttpStatus.OK) |
|
||||
async getAll(@Request() req: Request) { |
|
||||
// 模拟请求延迟
|
|
||||
await sleep(500); |
|
||||
// 请求用户的id
|
|
||||
const userId = req.user.id; |
|
||||
|
|
||||
// TODO: 改为表方式获取
|
|
||||
const dashboardMenus = [ |
|
||||
{ |
|
||||
component: 'BasicLayout', |
|
||||
meta: { |
|
||||
order: -1, |
|
||||
title: 'page.dashboard.title', |
|
||||
}, |
|
||||
name: 'Dashboard', |
|
||||
path: '/', |
|
||||
redirect: '/analytics', |
|
||||
children: [ |
|
||||
{ |
|
||||
name: 'Analytics', |
|
||||
path: '/analytics', |
|
||||
component: '/dashboard/analytics/index', |
|
||||
meta: { |
|
||||
affixTab: true, |
|
||||
title: 'page.dashboard.analytics', |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
name: 'Workspace', |
|
||||
path: '/workspace', |
|
||||
component: '/dashboard/workspace/index', |
|
||||
meta: { |
|
||||
title: 'page.dashboard.workspace', |
|
||||
}, |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
const createDemosMenus = (role: 'admin' | 'super' | 'user') => { |
|
||||
const roleWithMenus = { |
|
||||
admin: { |
|
||||
component: '/demos/access/admin-visible', |
|
||||
meta: { |
|
||||
icon: 'mdi:button-cursor', |
|
||||
title: 'page.demos.access.adminVisible', |
|
||||
}, |
|
||||
name: 'AccessAdminVisible', |
|
||||
path: 'admin-visible', |
|
||||
}, |
|
||||
super: { |
|
||||
component: '/demos/access/super-visible', |
|
||||
meta: { |
|
||||
icon: 'mdi:button-cursor', |
|
||||
title: 'page.demos.access.superVisible', |
|
||||
}, |
|
||||
name: 'AccessSuperVisible', |
|
||||
path: 'super-visible', |
|
||||
}, |
|
||||
user: { |
|
||||
component: '/demos/access/user-visible', |
|
||||
meta: { |
|
||||
icon: 'mdi:button-cursor', |
|
||||
title: 'page.demos.access.userVisible', |
|
||||
}, |
|
||||
name: 'AccessUserVisible', |
|
||||
path: 'user-visible', |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
return [ |
|
||||
{ |
|
||||
component: 'BasicLayout', |
|
||||
meta: { |
|
||||
icon: 'ic:baseline-view-in-ar', |
|
||||
keepAlive: true, |
|
||||
order: 1000, |
|
||||
title: 'page.demos.title', |
|
||||
}, |
|
||||
name: 'Demos', |
|
||||
path: '/demos', |
|
||||
redirect: '/access', |
|
||||
children: [ |
|
||||
{ |
|
||||
name: 'Access', |
|
||||
path: '/access', |
|
||||
meta: { |
|
||||
icon: 'mdi:cloud-key-outline', |
|
||||
title: 'page.demos.access.backendPermissions', |
|
||||
}, |
|
||||
redirect: '/access/page-control', |
|
||||
children: [ |
|
||||
{ |
|
||||
name: 'AccessPageControl', |
|
||||
path: 'page-control', |
|
||||
component: '/demos/access/index', |
|
||||
meta: { |
|
||||
icon: 'mdi:page-previous-outline', |
|
||||
title: 'page.demos.access.pageAccess', |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
name: 'AccessButtonControl', |
|
||||
path: 'button-control', |
|
||||
component: '/demos/access/button-control', |
|
||||
meta: { |
|
||||
icon: 'mdi:button-cursor', |
|
||||
title: 'page.demos.access.buttonControl', |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
name: 'AccessMenuVisible403', |
|
||||
path: 'menu-visible-403', |
|
||||
component: '/demos/access/menu-visible-403', |
|
||||
meta: { |
|
||||
authority: ['no-body'], |
|
||||
icon: 'mdi:button-cursor', |
|
||||
menuVisibleWithForbidden: true, |
|
||||
title: 'page.demos.access.menuVisible403', |
|
||||
}, |
|
||||
}, |
|
||||
roleWithMenus[role], |
|
||||
], |
|
||||
}, |
|
||||
], |
|
||||
}, |
|
||||
]; |
|
||||
}; |
|
||||
|
|
||||
const MOCK_MENUS = [ |
|
||||
{ |
|
||||
menus: [...dashboardMenus, ...createDemosMenus('super')], |
|
||||
userId: 0, |
|
||||
}, |
|
||||
{ |
|
||||
menus: [...dashboardMenus, ...createDemosMenus('admin')], |
|
||||
userId: 1, |
|
||||
}, |
|
||||
{ |
|
||||
menus: [...dashboardMenus, ...createDemosMenus('user')], |
|
||||
userId: 2, |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
return MOCK_MENUS.find((item) => item.userId === userId)?.menus ?? []; |
|
||||
} |
|
||||
} |
|
@ -1,10 +0,0 @@ |
|||||
import { Module } from '@nestjs/common'; |
|
||||
|
|
||||
import { MenuController } from './menu.controller'; |
|
||||
import { MenuService } from './menu.service'; |
|
||||
|
|
||||
@Module({ |
|
||||
controllers: [MenuController], |
|
||||
providers: [MenuService], |
|
||||
}) |
|
||||
export class MenuModule {} |
|
@ -1,4 +0,0 @@ |
|||||
import { Injectable } from '@nestjs/common'; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class MenuService {} |
|
@ -1 +0,0 @@ |
|||||
{} |
|
@ -1,23 +0,0 @@ |
|||||
import type { Response } from 'express'; |
|
||||
|
|
||||
import { Controller, Get, Query, Res } from '@nestjs/common'; |
|
||||
|
|
||||
@Controller('mock') |
|
||||
export class MockController { |
|
||||
/** |
|
||||
* 用于模拟任意的状态码 |
|
||||
* @param res |
|
||||
*/ |
|
||||
@Get('status') |
|
||||
async mockAnyStatus( |
|
||||
@Res() res: Response, |
|
||||
@Query() { status }: { status: string }, |
|
||||
) { |
|
||||
res.status(Number.parseInt(status, 10)).send({ |
|
||||
code: 1, |
|
||||
data: null, |
|
||||
error: null, |
|
||||
message: `code is ${status}`, |
|
||||
}); |
|
||||
} |
|
||||
} |
|
@ -1,13 +0,0 @@ |
|||||
interface User { |
|
||||
id: number; |
|
||||
password: string; |
|
||||
realName: string; |
|
||||
roles: string[]; |
|
||||
username: string; |
|
||||
} |
|
||||
|
|
||||
interface MockDatabaseData { |
|
||||
users: User[]; |
|
||||
} |
|
||||
|
|
||||
export type { MockDatabaseData, User }; |
|
@ -1,11 +0,0 @@ |
|||||
import { Module } from '@nestjs/common'; |
|
||||
|
|
||||
import { MockController } from './mock.controller'; |
|
||||
import { MockService } from './mock.service'; |
|
||||
|
|
||||
@Module({ |
|
||||
controllers: [MockController], |
|
||||
exports: [MockService], |
|
||||
providers: [MockService], |
|
||||
}) |
|
||||
export class MockModule {} |
|
@ -1,80 +0,0 @@ |
|||||
import type { MockDatabaseData } from './mock.interface'; |
|
||||
|
|
||||
import fs from 'node:fs'; |
|
||||
import path from 'node:path'; |
|
||||
|
|
||||
import { Injectable, type OnModuleInit } from '@nestjs/common'; |
|
||||
import bcrypt from 'bcryptjs'; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class MockService implements OnModuleInit { |
|
||||
private data: MockDatabaseData; |
|
||||
private readonly filePath: string; |
|
||||
|
|
||||
constructor() { |
|
||||
this.filePath = path.join(__dirname, '.', 'mock-db.json'); |
|
||||
this.loadData(); |
|
||||
} |
|
||||
|
|
||||
private loadData() { |
|
||||
const fileData = fs.readFileSync(this.filePath, 'utf8'); |
|
||||
this.data = JSON.parse(fileData); |
|
||||
} |
|
||||
|
|
||||
private saveData() { |
|
||||
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2)); |
|
||||
} |
|
||||
|
|
||||
addItem(collection: string, item: any) { |
|
||||
this.data[collection].push(item); |
|
||||
this.saveData(); |
|
||||
return item; |
|
||||
} |
|
||||
|
|
||||
clearCollection(collection: string) { |
|
||||
this.data[collection] = []; |
|
||||
this.saveData(); |
|
||||
return this.data[collection]; |
|
||||
} |
|
||||
|
|
||||
findAll(collection: string) { |
|
||||
return this.data[collection]; |
|
||||
} |
|
||||
|
|
||||
findOneById(collection: string, id: number) { |
|
||||
return this.data[collection].find((item) => item.id === id); |
|
||||
} |
|
||||
|
|
||||
async onModuleInit() { |
|
||||
// 清空表,并初始化两条数据
|
|
||||
await this.clearCollection('users'); |
|
||||
|
|
||||
// 密码哈希
|
|
||||
const hashPassword = await bcrypt.hash('123456', 10); |
|
||||
|
|
||||
await this.addItem('users', { |
|
||||
id: 0, |
|
||||
password: hashPassword, |
|
||||
realName: 'Vben', |
|
||||
roles: ['super'], |
|
||||
username: 'vben', |
|
||||
}); |
|
||||
|
|
||||
await this.addItem('users', { |
|
||||
id: 1, |
|
||||
password: hashPassword, |
|
||||
realName: 'Admin', |
|
||||
roles: ['admin'], |
|
||||
username: 'admin', |
|
||||
}); |
|
||||
await this.addItem('users', { |
|
||||
id: 2, |
|
||||
password: hashPassword, |
|
||||
realName: 'Jack', |
|
||||
roles: ['user'], |
|
||||
username: 'jack', |
|
||||
}); |
|
||||
const count = await this.findAll('users').length; |
|
||||
console.log('Database has been initialized with seed data, count:', count); |
|
||||
} |
|
||||
} |
|
@ -1,11 +0,0 @@ |
|||||
import { Module } from '@nestjs/common'; |
|
||||
|
|
||||
import { MockModule } from '../mock/mock.module'; |
|
||||
import { UsersService } from './users.service'; |
|
||||
|
|
||||
@Module({ |
|
||||
exports: [UsersService], |
|
||||
imports: [MockModule], |
|
||||
providers: [UsersService], |
|
||||
}) |
|
||||
export class UsersModule {} |
|
@ -1,18 +0,0 @@ |
|||||
import { UserEntity } from '@/models/entity/user.entity'; |
|
||||
import { Injectable } from '@nestjs/common'; |
|
||||
|
|
||||
import { MockService } from '../mock/mock.service'; |
|
||||
|
|
||||
@Injectable() |
|
||||
export class UsersService { |
|
||||
constructor(private mockService: MockService) {} |
|
||||
|
|
||||
/** |
|
||||
* Find user by username |
|
||||
* @param username |
|
||||
*/ |
|
||||
async findOne(username: string): Promise<UserEntity | undefined> { |
|
||||
const allUsers = await this.mockService.findAll('users'); |
|
||||
return allUsers.find((user) => user.username === username); |
|
||||
} |
|
||||
} |
|
@ -1,13 +0,0 @@ |
|||||
interface AppConfig { |
|
||||
NODE_ENV: string; |
|
||||
apiPrefix: string; |
|
||||
port: number; |
|
||||
} |
|
||||
|
|
||||
interface JwtConfig { |
|
||||
expiresIn: string; |
|
||||
refreshSecret: string; |
|
||||
refreshexpiresIn: string; |
|
||||
secret: string; |
|
||||
} |
|
||||
export type { AppConfig, JwtConfig }; |
|
@ -1,7 +0,0 @@ |
|||||
import { UserEntity } from '@/models/entity/user.entity'; |
|
||||
|
|
||||
declare global { |
|
||||
interface Request { |
|
||||
user?: UserEntity; |
|
||||
} |
|
||||
} |
|
@ -1,2 +0,0 @@ |
|||||
export * from './config'; |
|
||||
export * from './jwt'; |
|
@ -1,7 +0,0 @@ |
|||||
interface JwtPayload { |
|
||||
id: number; |
|
||||
roles: string[]; |
|
||||
username: string; |
|
||||
} |
|
||||
|
|
||||
export { JwtPayload }; |
|
@ -1,5 +0,0 @@ |
|||||
function sleep(ms: number) { |
|
||||
return new Promise((resolve) => setTimeout(resolve, ms)); |
|
||||
} |
|
||||
|
|
||||
export { sleep }; |
|
@ -1,25 +1,3 @@ |
|||||
{ |
{ |
||||
"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 |
|
||||
} |
|
||||
|
"extends": "./.nitro/types/tsconfig.json" |
||||
} |
} |
@ -0,0 +1,178 @@ |
|||||
|
export const MOCK_USERS = [ |
||||
|
{ |
||||
|
id: 0, |
||||
|
password: '123456', |
||||
|
realName: 'Vben', |
||||
|
roles: ['super'], |
||||
|
username: 'vben', |
||||
|
}, |
||||
|
{ |
||||
|
id: 1, |
||||
|
password: '123456', |
||||
|
realName: 'Admin', |
||||
|
roles: ['admin'], |
||||
|
username: 'admin', |
||||
|
}, |
||||
|
{ |
||||
|
id: 2, |
||||
|
password: '123456', |
||||
|
realName: 'Jack', |
||||
|
roles: ['user'], |
||||
|
username: 'jack', |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
export const MOCK_CODES = [ |
||||
|
// super
|
||||
|
{ |
||||
|
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'], |
||||
|
username: 'vben', |
||||
|
}, |
||||
|
{ |
||||
|
// admin
|
||||
|
codes: ['AC_100010', 'AC_100020', 'AC_100030'], |
||||
|
username: 'admin', |
||||
|
}, |
||||
|
{ |
||||
|
// user
|
||||
|
codes: ['AC_1000001', 'AC_1000002'], |
||||
|
username: 'jack', |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const dashboardMenus = [ |
||||
|
{ |
||||
|
component: 'BasicLayout', |
||||
|
meta: { |
||||
|
order: -1, |
||||
|
title: 'page.dashboard.title', |
||||
|
}, |
||||
|
name: 'Dashboard', |
||||
|
path: '/', |
||||
|
redirect: '/analytics', |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'Analytics', |
||||
|
path: '/analytics', |
||||
|
component: '/dashboard/analytics/index', |
||||
|
meta: { |
||||
|
affixTab: true, |
||||
|
title: 'page.dashboard.analytics', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Workspace', |
||||
|
path: '/workspace', |
||||
|
component: '/dashboard/workspace/index', |
||||
|
meta: { |
||||
|
title: 'page.dashboard.workspace', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const createDemosMenus = (role: 'admin' | 'super' | 'user') => { |
||||
|
const roleWithMenus = { |
||||
|
admin: { |
||||
|
component: '/demos/access/admin-visible', |
||||
|
meta: { |
||||
|
icon: 'mdi:button-cursor', |
||||
|
title: 'page.demos.access.adminVisible', |
||||
|
}, |
||||
|
name: 'AccessAdminVisible', |
||||
|
path: 'admin-visible', |
||||
|
}, |
||||
|
super: { |
||||
|
component: '/demos/access/super-visible', |
||||
|
meta: { |
||||
|
icon: 'mdi:button-cursor', |
||||
|
title: 'page.demos.access.superVisible', |
||||
|
}, |
||||
|
name: 'AccessSuperVisible', |
||||
|
path: 'super-visible', |
||||
|
}, |
||||
|
user: { |
||||
|
component: '/demos/access/user-visible', |
||||
|
meta: { |
||||
|
icon: 'mdi:button-cursor', |
||||
|
title: 'page.demos.access.userVisible', |
||||
|
}, |
||||
|
name: 'AccessUserVisible', |
||||
|
path: 'user-visible', |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
return [ |
||||
|
{ |
||||
|
component: 'BasicLayout', |
||||
|
meta: { |
||||
|
icon: 'ic:baseline-view-in-ar', |
||||
|
keepAlive: true, |
||||
|
order: 1000, |
||||
|
title: 'page.demos.title', |
||||
|
}, |
||||
|
name: 'Demos', |
||||
|
path: '/demos', |
||||
|
redirect: '/access', |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'Access', |
||||
|
path: 'access', |
||||
|
meta: { |
||||
|
icon: 'mdi:cloud-key-outline', |
||||
|
title: 'page.demos.access.backendPermissions', |
||||
|
}, |
||||
|
redirect: '/demos/access/page-control', |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'AccessPageControl', |
||||
|
path: 'page-control', |
||||
|
component: '/demos/access/index', |
||||
|
meta: { |
||||
|
icon: 'mdi:page-previous-outline', |
||||
|
title: 'page.demos.access.pageAccess', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
name: 'AccessButtonControl', |
||||
|
path: 'button-control', |
||||
|
component: '/demos/access/button-control', |
||||
|
meta: { |
||||
|
icon: 'mdi:button-cursor', |
||||
|
title: 'page.demos.access.buttonControl', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
name: 'AccessMenuVisible403', |
||||
|
path: 'menu-visible-403', |
||||
|
component: '/demos/access/menu-visible-403', |
||||
|
meta: { |
||||
|
authority: ['no-body'], |
||||
|
icon: 'mdi:button-cursor', |
||||
|
menuVisibleWithForbidden: true, |
||||
|
title: 'page.demos.access.menuVisible403', |
||||
|
}, |
||||
|
}, |
||||
|
roleWithMenus[role], |
||||
|
], |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
}; |
||||
|
|
||||
|
export const MOCK_MENUS = [ |
||||
|
{ |
||||
|
menus: [...dashboardMenus, ...createDemosMenus('super')], |
||||
|
username: 'vben', |
||||
|
}, |
||||
|
{ |
||||
|
menus: [...dashboardMenus, ...createDemosMenus('admin')], |
||||
|
username: 'admin', |
||||
|
}, |
||||
|
{ |
||||
|
menus: [...dashboardMenus, ...createDemosMenus('user')], |
||||
|
username: 'user', |
||||
|
}, |
||||
|
]; |
@ -0,0 +1,17 @@ |
|||||
|
export function useResponseSuccess<T = any>(data: T) { |
||||
|
return { |
||||
|
code: 0, |
||||
|
data, |
||||
|
error: null, |
||||
|
message: 'ok', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function useResponseError(message: string, error: any = null) { |
||||
|
return { |
||||
|
code: -1, |
||||
|
data: null, |
||||
|
error, |
||||
|
message, |
||||
|
}; |
||||
|
} |
@ -1,3 +1,5 @@ |
|||||
VITE_PUBLIC_PATH = / |
VITE_PUBLIC_PATH = / |
||||
|
|
||||
VITE_GLOB_API_URL=/api |
VITE_GLOB_API_URL=/api |
||||
|
|
||||
|
VITE_NITRO_MOCK = true |
@ -0,0 +1,17 @@ |
|||||
|
import type { UserApi } from '../types'; |
||||
|
|
||||
|
import { requestClient } from '#/forward'; |
||||
|
|
||||
|
/** |
||||
|
* 登录 |
||||
|
*/ |
||||
|
export async function login(data: UserApi.LoginParams) { |
||||
|
return requestClient.post<UserApi.LoginResult>('/auth/login', data); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取用户权限码 |
||||
|
*/ |
||||
|
export async function getAccessCodes() { |
||||
|
return requestClient.get<string[]>('/auth/codes'); |
||||
|
} |
@ -1,3 +1,3 @@ |
|||||
|
export * from './auth'; |
||||
export * from './menu'; |
export * from './menu'; |
||||
export * from './mock'; |
|
||||
export * from './user'; |
export * from './user'; |
@ -0,0 +1,10 @@ |
|||||
|
import type { UserInfo } from '@vben/types'; |
||||
|
|
||||
|
import { requestClient } from '#/forward'; |
||||
|
|
||||
|
/** |
||||
|
* 获取用户信息 |
||||
|
*/ |
||||
|
export async function getUserInfo() { |
||||
|
return requestClient.get<UserInfo>('/user/info'); |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export * from './status'; |
@ -1,2 +1,3 @@ |
|||||
export * from './modules'; |
|
||||
|
export * from './core'; |
||||
|
export * from './demos'; |
||||
export type * from './types'; |
export type * from './types'; |
@ -1,28 +0,0 @@ |
|||||
import type { UserInfo } from '@vben/types'; |
|
||||
|
|
||||
import type { UserApi } from '../types'; |
|
||||
|
|
||||
import { requestClient } from '#/forward'; |
|
||||
|
|
||||
/** |
|
||||
* 登录 |
|
||||
*/ |
|
||||
async function userLogin(data: UserApi.LoginParams) { |
|
||||
return requestClient.post<UserApi.LoginResult>('/auth/login', data); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取用户信息 |
|
||||
*/ |
|
||||
async function getUserInfo() { |
|
||||
return requestClient.get<UserInfo>('/auth/getUserInfo'); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取用户权限码 |
|
||||
*/ |
|
||||
async function getAccessCodes() { |
|
||||
return requestClient.get<string[]>('/auth/getAccessCodes'); |
|
||||
} |
|
||||
|
|
||||
export { getAccessCodes, getUserInfo, userLogin }; |
|
@ -0,0 +1,86 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useRouter } from 'vue-router'; |
||||
|
|
||||
|
import { useTabs } from '@vben/hooks'; |
||||
|
|
||||
|
import { Button } from 'ant-design-vue'; |
||||
|
|
||||
|
defineOptions({ name: 'FeatureTabsDemo' }); |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
// const newTabTitle = ref(''); |
||||
|
const { |
||||
|
closeAllTabs, |
||||
|
closeCurrentTab, |
||||
|
closeLeftTabs, |
||||
|
closeOtherTabs, |
||||
|
closeRightTabs, |
||||
|
closeTabByKey, |
||||
|
refreshTab, |
||||
|
} = useTabs(); |
||||
|
|
||||
|
function openTab() { |
||||
|
// 这里就是路由跳转,也可以用path |
||||
|
router.push({ name: 'VbenAbout' }); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="p-5"> |
||||
|
<div class="card-box p-5"> |
||||
|
<h1 class="text-xl font-semibold">标签页</h1> |
||||
|
<div class="text-foreground/80 mt-2">用于需要操作标签页的场景</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-box mt-5 p-5"> |
||||
|
<div class="text-lg font-semibold">打开/关闭标签页</div> |
||||
|
<div class="text-foreground/80 my-3"> |
||||
|
如果标签页存在,直接跳转切换。如果标签页不存在,则打开新的标签页。 |
||||
|
</div> |
||||
|
<div class="flex flex-wrap gap-3"> |
||||
|
<Button type="primary" @click="openTab"> 打开 "关于" 标签页 </Button> |
||||
|
<Button type="primary" @click="closeTabByKey('/vben-admin/about')"> |
||||
|
关闭 "关于" 标签页 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-box mt-5 p-5"> |
||||
|
<div class="text-lg font-semibold">标签页操作</div> |
||||
|
<div class="text-foreground/80 my-3">用于动态控制标签页的各种操作</div> |
||||
|
<div class="flex flex-wrap gap-3"> |
||||
|
<Button type="primary" @click="closeCurrentTab()"> |
||||
|
关闭当前标签页 |
||||
|
</Button> |
||||
|
<Button type="primary" @click="closeLeftTabs()"> |
||||
|
关闭左侧标签页 |
||||
|
</Button> |
||||
|
<Button type="primary" @click="closeRightTabs()"> |
||||
|
关闭右侧标签页 |
||||
|
</Button> |
||||
|
<Button type="primary" @click="closeAllTabs()"> 打开所有标签页 </Button> |
||||
|
<Button type="primary" @click="closeOtherTabs()"> |
||||
|
关闭其他标签页 |
||||
|
</Button> |
||||
|
<Button type="primary" @click="refreshTab()"> 刷新当前标签页 </Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-box mt-5 p-5"> |
||||
|
<div class="text-lg font-semibold">动态标题</div> |
||||
|
<div class="text-foreground/80 my-3"> |
||||
|
该操作不会影响页面标题,仅修改Tab标题 |
||||
|
</div> |
||||
|
<!-- <div class="flex flex-wrap items-center gap-3"> |
||||
|
<Input |
||||
|
v-model="newTabTitle" |
||||
|
class="w-30" |
||||
|
placeholder="请输入新的标题" |
||||
|
/> |
||||
|
<Button type="primary" @click="closeCurrentTab()"> |
||||
|
关闭当前标签页 {{ newTabTitle }} |
||||
|
</Button> |
||||
|
</div> --> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
@ -0,0 +1,89 @@ |
|||||
|
import type { PluginOption } from 'vite'; |
||||
|
|
||||
|
import type { NitroMockPluginOptions } from '../typing'; |
||||
|
|
||||
|
import { colors, consola, getPackage } from '@vben/node-utils'; |
||||
|
|
||||
|
import { build, createDevServer, createNitro, prepare } from 'nitropack'; |
||||
|
|
||||
|
const hmrKeyRe = /^runtimeConfig\.|routeRules\./; |
||||
|
|
||||
|
export const viteNitroMockPlugin = ({ |
||||
|
mockServerPackage = '@vben/backend-mock', |
||||
|
port = 5320, |
||||
|
verbose = true, |
||||
|
}: NitroMockPluginOptions = {}): PluginOption => { |
||||
|
return { |
||||
|
async configureServer(server) { |
||||
|
const pkg = await getPackage(mockServerPackage); |
||||
|
if (!pkg) { |
||||
|
consola.error(`Package ${mockServerPackage} not found.`); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
runNitroServer(pkg.dir, port, verbose); |
||||
|
|
||||
|
const _printUrls = server.printUrls; |
||||
|
server.printUrls = () => { |
||||
|
_printUrls(); |
||||
|
|
||||
|
consola.log( |
||||
|
` ${colors.green('➜')} ${colors.bold('Nitro Mock Server')}: ${colors.cyan(`http://localhost:${port}/api`)}`, |
||||
|
); |
||||
|
}; |
||||
|
}, |
||||
|
enforce: 'pre', |
||||
|
name: 'vite:mock-server', |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
async function runNitroServer(rootDir: string, port: number, verbose: boolean) { |
||||
|
let nitro: any; |
||||
|
const reload = async () => { |
||||
|
if (nitro) { |
||||
|
consola.info('Restarting dev server...'); |
||||
|
if ('unwatch' in nitro.options._c12) { |
||||
|
await nitro.options._c12.unwatch(); |
||||
|
} |
||||
|
await nitro.close(); |
||||
|
} |
||||
|
nitro = await createNitro( |
||||
|
{ |
||||
|
dev: true, |
||||
|
preset: 'nitro-dev', |
||||
|
rootDir, |
||||
|
}, |
||||
|
{ |
||||
|
c12: { |
||||
|
async onUpdate({ getDiff, newConfig }) { |
||||
|
const diff = getDiff(); |
||||
|
if (diff.length === 0) { |
||||
|
return; |
||||
|
} |
||||
|
verbose && |
||||
|
consola.info( |
||||
|
`Nitro config updated:\n${diff |
||||
|
.map((entry) => ` ${entry.toString()}`) |
||||
|
.join('\n')}`,
|
||||
|
); |
||||
|
await (diff.every((e) => hmrKeyRe.test(e.key)) |
||||
|
? nitro.updateConfig(newConfig.config) |
||||
|
: reload()); |
||||
|
}, |
||||
|
}, |
||||
|
watch: true, |
||||
|
}, |
||||
|
); |
||||
|
nitro.hooks.hookOnce('restart', reload); |
||||
|
const server = createDevServer(nitro); |
||||
|
await server.listen(port, { showURL: false }); |
||||
|
await prepare(nitro); |
||||
|
await build(nitro); |
||||
|
|
||||
|
if (verbose) { |
||||
|
console.log(''); |
||||
|
consola.success(colors.bold(colors.green('Nitro Mock Server started.'))); |
||||
|
} |
||||
|
}; |
||||
|
await reload(); |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
import type { PluginOption } from 'vite'; |
||||
|
|
||||
|
import type { PrintPluginOptions } from '../typing'; |
||||
|
|
||||
|
import { colors } from '@vben/node-utils'; |
||||
|
|
||||
|
export const vitePrintPlugin = ( |
||||
|
options: PrintPluginOptions = {}, |
||||
|
): PluginOption => { |
||||
|
const { infoMap = {} } = options; |
||||
|
|
||||
|
return { |
||||
|
configureServer(server) { |
||||
|
const _printUrls = server.printUrls; |
||||
|
server.printUrls = () => { |
||||
|
_printUrls(); |
||||
|
|
||||
|
for (const [key, value] of Object.entries(infoMap)) { |
||||
|
console.log( |
||||
|
` ${colors.green('➜')} ${colors.bold(key)}: ${colors.cyan(value)}`, |
||||
|
); |
||||
|
} |
||||
|
}; |
||||
|
}, |
||||
|
enforce: 'pre', |
||||
|
name: 'vite:print-info', |
||||
|
}; |
||||
|
}; |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue