add rbac module
This commit is contained in:
parent
6951ea1373
commit
16a08bf280
5
bun.lock
5
bun.lock
@ -21,6 +21,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"fastify": "^5.4.0",
|
||||||
"find-up": "^7.0.0",
|
"find-up": "^7.0.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@ -1086,7 +1087,7 @@
|
|||||||
|
|
||||||
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
||||||
|
|
||||||
"fastify": ["fastify@5.3.3", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-nCBiBCw9q6jPx+JJNVgO8JVnTXeUyrGcyTKPQikRkA/PanrFcOIo4R+ZnLeOLPZPGgzjomqfVarzE0kYx7qWiQ=="],
|
"fastify": ["fastify@5.4.0", "https://registry.npmmirror.com/fastify/-/fastify-5.4.0.tgz", { "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw=="],
|
||||||
|
|
||||||
"fastify-plugin": ["fastify-plugin@5.0.1", "", {}, "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ=="],
|
"fastify-plugin": ["fastify-plugin@5.0.1", "", {}, "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ=="],
|
||||||
|
|
||||||
@ -2142,6 +2143,8 @@
|
|||||||
|
|
||||||
"@nestjs/jwt/@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg=="],
|
"@nestjs/jwt/@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg=="],
|
||||||
|
|
||||||
|
"@nestjs/platform-fastify/fastify": ["fastify@5.3.3", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-nCBiBCw9q6jPx+JJNVgO8JVnTXeUyrGcyTKPQikRkA/PanrFcOIo4R+ZnLeOLPZPGgzjomqfVarzE0kYx7qWiQ=="],
|
||||||
|
|
||||||
"@nestjs/schematics/@angular-devkit/core": ["@angular-devkit/core@19.2.6", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ=="],
|
"@nestjs/schematics/@angular-devkit/core": ["@angular-devkit/core@19.2.6", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ=="],
|
||||||
|
|
||||||
"@nestjs/schematics/@angular-devkit/schematics": ["@angular-devkit/schematics@19.2.6", "", { "dependencies": { "@angular-devkit/core": "19.2.6", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" } }, "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ=="],
|
"@nestjs/schematics/@angular-devkit/schematics": ["@angular-devkit/schematics@19.2.6", "", { "dependencies": { "@angular-devkit/core": "19.2.6", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" } }, "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ=="],
|
||||||
|
492
pnpm-lock.yaml
492
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,16 @@ import { ContentFactory } from '@/modules/database/factories/content.factory';
|
|||||||
import ContentSeeder from '@/modules/database/seeders/content.seeder';
|
import ContentSeeder from '@/modules/database/seeders/content.seeder';
|
||||||
|
|
||||||
export const database = createDBConfig((configure) => ({
|
export const database = createDBConfig((configure) => ({
|
||||||
common: { synchronize: true },
|
common: {
|
||||||
|
synchronize: true,
|
||||||
|
// 启用详细日志以便调试 SQL 错误
|
||||||
|
logging:
|
||||||
|
configure.env.get('NODE_ENV') === 'development'
|
||||||
|
? ['query', 'error', 'schema', 'warn', 'info', 'log']
|
||||||
|
: ['error'],
|
||||||
|
// 启用最大日志记录
|
||||||
|
maxQueryExecutionTime: 1000,
|
||||||
|
},
|
||||||
connections: [
|
connections: [
|
||||||
{
|
{
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
|
@ -27,7 +27,7 @@ import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
|
|||||||
|
|
||||||
const permission: PermissionChecker = async (ab) => ab.can(PermissionAction.MANAGE, TagEntity.name);
|
const permission: PermissionChecker = async (ab) => ab.can(PermissionAction.MANAGE, TagEntity.name);
|
||||||
|
|
||||||
@ApiTags('标签查询')
|
@ApiTags('标签管理')
|
||||||
@Depends(ContentModule)
|
@Depends(ContentModule)
|
||||||
@Controller('tag')
|
@Controller('tag')
|
||||||
export class TagController {
|
export class TagController {
|
||||||
|
98
src/modules/core/filters/global-exception.filter.ts
Normal file
98
src/modules/core/filters/global-exception.filter.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
ArgumentsHost,
|
||||||
|
Catch,
|
||||||
|
ExceptionFilter,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
import { QueryFailedError } from 'typeorm';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger(GlobalExceptionFilter.name);
|
||||||
|
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const request = ctx.getRequest<FastifyRequest>();
|
||||||
|
const response = ctx.getResponse<FastifyReply>();
|
||||||
|
|
||||||
|
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
let message = 'Internal server error';
|
||||||
|
let details: any = null;
|
||||||
|
|
||||||
|
// 记录完整的错误信息
|
||||||
|
this.logError(exception, request);
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
status = exception.getStatus();
|
||||||
|
const exceptionResponse = exception.getResponse();
|
||||||
|
message =
|
||||||
|
typeof exceptionResponse === 'string'
|
||||||
|
? exceptionResponse
|
||||||
|
: (exceptionResponse as any).message || exception.message;
|
||||||
|
} else if (exception instanceof QueryFailedError) {
|
||||||
|
// 专门处理 TypeORM 查询错误
|
||||||
|
status = HttpStatus.BAD_REQUEST;
|
||||||
|
message = 'Database query failed';
|
||||||
|
details = {
|
||||||
|
query: exception.query,
|
||||||
|
parameters: exception.parameters,
|
||||||
|
driverError: exception.driverError?.message,
|
||||||
|
sqlMessage: (exception as any).sqlMessage,
|
||||||
|
code: (exception as any).code,
|
||||||
|
errno: (exception as any).errno,
|
||||||
|
};
|
||||||
|
} else if (exception instanceof Error) {
|
||||||
|
message = exception.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorResponse = {
|
||||||
|
statusCode: status,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
method: request.method,
|
||||||
|
message,
|
||||||
|
...(details && { details }),
|
||||||
|
};
|
||||||
|
|
||||||
|
response.status(status).send(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private logError(exception: unknown, request: FastifyRequest) {
|
||||||
|
const errorInfo = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
headers: request.headers,
|
||||||
|
body: request.body,
|
||||||
|
query: request.query,
|
||||||
|
params: request.params,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (exception instanceof QueryFailedError) {
|
||||||
|
this.logger.error(`Database Query Failed: ${exception.message}`, {
|
||||||
|
...errorInfo,
|
||||||
|
query: exception.query,
|
||||||
|
parameters: exception.parameters,
|
||||||
|
driverError: exception.driverError,
|
||||||
|
stack: exception.stack,
|
||||||
|
sqlMessage: (exception as any).sqlMessage,
|
||||||
|
code: (exception as any).code,
|
||||||
|
errno: (exception as any).errno,
|
||||||
|
});
|
||||||
|
} else if (exception instanceof Error) {
|
||||||
|
this.logger.error(`Unhandled Exception: ${exception.message}`, {
|
||||||
|
...errorInfo,
|
||||||
|
stack: exception.stack,
|
||||||
|
name: exception.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.logger.error('Unknown Exception', {
|
||||||
|
...errorInfo,
|
||||||
|
exception,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-tr
|
|||||||
const permission: PermissionChecker = async (ab) =>
|
const permission: PermissionChecker = async (ab) =>
|
||||||
ab.can(PermissionAction.MANAGE, PermissionEntity.name);
|
ab.can(PermissionAction.MANAGE, PermissionEntity.name);
|
||||||
|
|
||||||
@ApiTags('权限查询')
|
@ApiTags('权限管理')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Depends(RbacModule)
|
@Depends(RbacModule)
|
||||||
@Controller('permissions')
|
@Controller('permissions')
|
||||||
|
@ -243,7 +243,7 @@ export class RbacResolver<P extends AbilityTuple = AbilityTuple, T extends Mongo
|
|||||||
const superUsers = await manager
|
const superUsers = await manager
|
||||||
.createQueryBuilder(UserEntity, 'user')
|
.createQueryBuilder(UserEntity, 'user')
|
||||||
.leftJoinAndSelect('user.roles', 'roles')
|
.leftJoinAndSelect('user.roles', 'roles')
|
||||||
.where('roles.id IN (:...ids', { ids: [superRole.id] })
|
.where('roles.id IN (:...ids)', { ids: [superRole.id] })
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (superUsers.length < 1) {
|
if (superUsers.length < 1) {
|
||||||
|
@ -24,7 +24,7 @@ export const createRbacApi = () => {
|
|||||||
app: [{ name: '角色查询', description: '查询角色信息' }],
|
app: [{ name: '角色查询', description: '查询角色信息' }],
|
||||||
manager: [
|
manager: [
|
||||||
{ name: '角色管理', description: '管理角色信息' },
|
{ name: '角色管理', description: '管理角色信息' },
|
||||||
{ name: '权限信息', description: '查询权限信息' },
|
{ name: '权限管理', description: '管理权限信息' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
return { routes, tags };
|
return { routes, tags };
|
||||||
|
@ -34,7 +34,7 @@ export class PermissionService extends BaseService<
|
|||||||
) {
|
) {
|
||||||
const qb = await super.buildListQB(queryBuilder, options, callback);
|
const qb = await super.buildListQB(queryBuilder, options, callback);
|
||||||
if (!isNil(options.role)) {
|
if (!isNil(options.role)) {
|
||||||
qb.andWhere('role.id IN (:...roles', { roles: [options.role] });
|
qb.andWhere('role.id IN (:...roles)', { roles: [options.role] });
|
||||||
}
|
}
|
||||||
return qb;
|
return qb;
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,7 @@ export function createUserApi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tags: Record<'app' | 'manager', (string | TagOption)[]> = {
|
const tags: Record<'app' | 'manager', (string | TagOption)[]> = {
|
||||||
app: [
|
app: [{ name: '账户操作', description: '注册登录、查看修改账户信息、修改密码等' }],
|
||||||
{ name: '用户管理', description: '对用户进行CRUD操作' },
|
|
||||||
{ name: '账户操作', description: '注册登录、查看修改账户信息、修改密码等' },
|
|
||||||
],
|
|
||||||
manager: [{ name: '用户管理', description: '管理用户信息' }],
|
manager: [{ name: '用户管理', description: '管理用户信息' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -132,10 +132,10 @@ export class UserService extends BaseService<UserEntity, UserRepository> {
|
|||||||
const { orderBy } = options;
|
const { orderBy } = options;
|
||||||
const qb = await super.buildListQB(queryBuilder, options, callback);
|
const qb = await super.buildListQB(queryBuilder, options, callback);
|
||||||
if (!isNil(options.role)) {
|
if (!isNil(options.role)) {
|
||||||
qb.andWhere('roles.id IN (:...roles', { roles: [options.role] });
|
qb.andWhere('roles.id IN (:...roles)', { roles: [options.role] });
|
||||||
}
|
}
|
||||||
if (!isNil(options.permission)) {
|
if (!isNil(options.permission)) {
|
||||||
qb.andWhere('permissions.id IN (:...permissions', {
|
qb.andWhere('permissions.id IN (:...permissions)', {
|
||||||
permissions: [options.permission],
|
permissions: [options.permission],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { UserModule } from '@/modules/user/user.module';
|
|||||||
|
|
||||||
import * as configs from './config';
|
import * as configs from './config';
|
||||||
import { ContentModule } from './modules/content/content.module';
|
import { ContentModule } from './modules/content/content.module';
|
||||||
|
import { GlobalExceptionFilter } from './modules/core/filters/global-exception.filter';
|
||||||
import { CreateOptions } from './modules/core/types';
|
import { CreateOptions } from './modules/core/types';
|
||||||
import * as dbCommands from './modules/database/commands';
|
import * as dbCommands from './modules/database/commands';
|
||||||
import { DatabaseModule } from './modules/database/database.module';
|
import { DatabaseModule } from './modules/database/database.module';
|
||||||
@ -32,7 +33,10 @@ export const createOptions: CreateOptions = {
|
|||||||
await UserModule.forRoot(configure),
|
await UserModule.forRoot(configure),
|
||||||
await RbacModule.forRoot(configure),
|
await RbacModule.forRoot(configure),
|
||||||
],
|
],
|
||||||
globals: { guard: RbacGuard },
|
globals: {
|
||||||
|
guard: RbacGuard,
|
||||||
|
filter: GlobalExceptionFilter,
|
||||||
|
},
|
||||||
builder: async ({ configure, BootModule }) => {
|
builder: async ({ configure, BootModule }) => {
|
||||||
const container = await NestFactory.create<NestFastifyApplication>(
|
const container = await NestFactory.create<NestFastifyApplication>(
|
||||||
BootModule,
|
BootModule,
|
||||||
|
Loading…
Reference in New Issue
Block a user