Compare commits

...

2 Commits

Author SHA1 Message Date
16a08bf280 add rbac module 2025-07-01 10:38:38 +08:00
6951ea1373 add rbac module 2025-06-30 21:51:17 +08:00
15 changed files with 390 additions and 278 deletions

View File

@ -21,6 +21,7 @@
"dayjs": "^1.11.13",
"deepmerge": "^4.3.1",
"dotenv": "^16.5.0",
"fastify": "^5.4.0",
"find-up": "^7.0.0",
"fs-extra": "^11.3.0",
"jsonwebtoken": "^9.0.2",
@ -1086,7 +1087,7 @@
"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=="],
@ -2142,6 +2143,8 @@
"@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/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=="],

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,16 @@ import { ContentFactory } from '@/modules/database/factories/content.factory';
import ContentSeeder from '@/modules/database/seeders/content.seeder';
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: [
{
type: 'mysql',

View File

@ -27,7 +27,7 @@ import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
const permission: PermissionChecker = async (ab) => ab.can(PermissionAction.MANAGE, TagEntity.name);
@ApiTags('标签查询')
@ApiTags('标签管理')
@Depends(ContentModule)
@Controller('tag')
export class TagController {

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

View File

@ -13,7 +13,7 @@ import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-tr
const permission: PermissionChecker = async (ab) =>
ab.can(PermissionAction.MANAGE, PermissionEntity.name);
@ApiTags('权限查询')
@ApiTags('权限管理')
@ApiBearerAuth()
@Depends(RbacModule)
@Controller('permissions')

View File

@ -1,6 +1,7 @@
import { AbilityTuple, MongoQuery, RawRuleFrom } from '@casl/ability';
import { Exclude, Expose } from 'class-transformer';
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, Relation } from 'typeorm';
import type { Relation } from 'typeorm';
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { UserEntity } from '@/modules/user/entities';
@ -53,7 +54,7 @@ export class PermissionEntity<
*
*/
@Expose({ groups: ['permission-list', 'permission-detail'] })
@ManyToMany(() => RoleEntity, (role) => role.permissions, { cascade: true })
@ManyToMany(() => RoleEntity, (role) => role.permissions)
@JoinTable()
roles: Relation<RoleEntity>[];

View File

@ -1,4 +1,5 @@
import { Exclude, Expose, Type } from 'class-transformer';
import type { Relation } from 'typeorm';
import {
BaseEntity,
Column,
@ -7,7 +8,6 @@ import {
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { UserEntity } from '@/modules/user/entities';

View File

@ -243,7 +243,7 @@ export class RbacResolver<P extends AbilityTuple = AbilityTuple, T extends Mongo
const superUsers = await manager
.createQueryBuilder(UserEntity, 'user')
.leftJoinAndSelect('user.roles', 'roles')
.where('roles.id IN (:...ids', { ids: [superRole.id] })
.where('roles.id IN (:...ids)', { ids: [superRole.id] })
.getMany();
if (superUsers.length < 1) {

View File

@ -24,7 +24,7 @@ export const createRbacApi = () => {
app: [{ name: '角色查询', description: '查询角色信息' }],
manager: [
{ name: '角色管理', description: '管理角色信息' },
{ name: '权限信息', description: '查询权限信息' },
{ name: '权限管理', description: '管理权限信息' },
],
};
return { routes, tags };

View File

@ -34,7 +34,7 @@ export class PermissionService extends BaseService<
) {
const qb = await super.buildListQB(queryBuilder, options, callback);
if (!isNil(options.role)) {
qb.andWhere('role.id IN (:...roles', { roles: [options.role] });
qb.andWhere('role.id IN (:...roles)', { roles: [options.role] });
}
return qb;
}

View File

@ -22,10 +22,7 @@ export function createUserApi() {
};
const tags: Record<'app' | 'manager', (string | TagOption)[]> = {
app: [
{ name: '用户管理', description: '对用户进行CRUD操作' },
{ name: '账户操作', description: '注册登录、查看修改账户信息、修改密码等' },
],
app: [{ name: '账户操作', description: '注册登录、查看修改账户信息、修改密码等' }],
manager: [{ name: '用户管理', description: '管理用户信息' }],
};

View File

@ -132,10 +132,10 @@ export class UserService extends BaseService<UserEntity, UserRepository> {
const { orderBy } = options;
const qb = await super.buildListQB(queryBuilder, options, callback);
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)) {
qb.andWhere('permissions.id IN (:...permissions', {
qb.andWhere('permissions.id IN (:...permissions)', {
permissions: [options.permission],
});
}

View File

@ -1,4 +1,4 @@
import { DynamicModule, Module } from '@nestjs/common';
import { DynamicModule, forwardRef, Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
@ -7,6 +7,10 @@ import { Configure } from '@/modules/config/configure';
import { DatabaseModule } from '@/modules/database/database.module';
import { addEntities, addSubscribers } from '@/modules/database/utils';
import { RbacModule } from '@/modules/rbac/rbac.module';
import { RoleRepository } from '@/modules/rbac/repositories';
import * as entities from './entities';
import * as guards from './guards';
import * as interceptors from './interceptors';
@ -22,11 +26,13 @@ export class UserModule {
module: UserModule,
imports: [
PassportModule,
forwardRef(() => RbacModule),
services.TokenService.JwtModuleFactory(configure),
await addEntities(configure, Object.values(entities)),
DatabaseModule.forRepository(Object.values(repositories)),
],
providers: [
RoleRepository,
...Object.values(interceptors),
...Object.values(services),
...Object.values(strategies),

View File

@ -7,10 +7,12 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify
import { existsSync } from 'fs-extra';
import { isNil } from 'lodash';
import { RbacModule } from '@/modules/rbac/rbac.module';
import { UserModule } from '@/modules/user/user.module';
import * as configs from './config';
import { ContentModule } from './modules/content/content.module';
import { GlobalExceptionFilter } from './modules/core/filters/global-exception.filter';
import { CreateOptions } from './modules/core/types';
import * as dbCommands from './modules/database/commands';
import { DatabaseModule } from './modules/database/database.module';
@ -29,8 +31,12 @@ export const createOptions: CreateOptions = {
await RestfulModule.forRoot(configure),
await ContentModule.forRoot(configure),
await UserModule.forRoot(configure),
await RbacModule.forRoot(configure),
],
globals: { guard: RbacGuard },
globals: {
guard: RbacGuard,
filter: GlobalExceptionFilter,
},
builder: async ({ configure, BootModule }) => {
const container = await NestFactory.create<NestFastifyApplication>(
BootModule,