diff --git a/package-lock.json b/package-lock.json index 73b0de3..a93cf36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,8 @@ "koa-bodyparser": "^4.4.0", "koa-router": "^12.0.0", "mongoose": "^8.5.2", - "otplib": "^12.0.1" + "otplib": "^12.0.1", + "uuid": "^11.0.5" }, "devDependencies": { "@types/koa": "^2.13.5", @@ -1469,6 +1470,19 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@smithy/middleware-serde": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", @@ -5006,15 +5020,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { diff --git a/package.json b/package.json index 73b6f18..4b03a6a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "koa-bodyparser": "^4.4.0", "koa-router": "^12.0.0", "mongoose": "^8.5.2", - "otplib": "^12.0.1" + "otplib": "^12.0.1", + "uuid": "^11.0.5" } } diff --git a/src/common/ObjectTool.ts b/src/common/ObjectTool.ts index 031e704..27bc37a 100644 --- a/src/common/ObjectTool.ts +++ b/src/common/ObjectTool.ts @@ -1 +1,20 @@ +/** + * 判断一个对象是否为空 + */ export const isEmptyObject = (obj: object) => typeof obj === 'object' && !(Array.isArray(obj)) && Object.keys(obj).length === 0 + +/** + * 删除一个对象中值为 undefined 的元素,返回一个新对象 + * 底层原理是(浅)拷贝所有不为 undefined 的元素到新对象 + * @param obj 需要被清理的存在元素的值为 undefined 的对象 + * @returns 清理了值为 undefined 的元素的对象 + */ +export const clearUndefinedItemInObject = >(obj: T): Partial => { + const newObj: Partial = {}; + (Object.keys(obj) as (keyof T)[]).forEach(key => { + if (obj[key] !== undefined) { + newObj[key] = obj[key]; + } + }); + return newObj; +} diff --git a/src/controller/DanmakuController.ts b/src/controller/DanmakuController.ts index 54890cf..539b6b4 100644 --- a/src/controller/DanmakuController.ts +++ b/src/controller/DanmakuController.ts @@ -1,4 +1,5 @@ import { emitDanmakuService, getDanmakuListByKvidService } from '../service/DanmakuService.js' +import { isPassRbacCheck } from '../service/RbacService.js' import { koaCtx, koaNext } from '../type/koaTypes.js' import { EmitDanmakuRequestDto, GetDanmakuByKvidRequestDto } from './DanmakuControllerDto.js' @@ -12,6 +13,12 @@ export const emitDanmakuController = async (ctx: koaCtx, next: koaNext) => { const data = ctx.request.body as Partial const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const emitDanmakuRequest: EmitDanmakuRequestDto = { /** 非空 - KVID 视频 ID */ videoId: data.videoId, diff --git a/src/controller/RbacController.ts b/src/controller/RbacController.ts new file mode 100644 index 0000000..2644d1a --- /dev/null +++ b/src/controller/RbacController.ts @@ -0,0 +1,250 @@ +import { isPassRbacCheck, createRbacApiPathService, createRbacRoleService, updateApiPathPermissionsForRoleService, getRbacApiPathService, deleteRbacApiPathService, deleteRbacRoleService, getRbacRoleService, adminGetUserRolesByUidService, adminUpdateUserRoleService } from '../service/RbacService.js' +import { koaCtx, koaNext } from '../type/koaTypes.js' +import { AdminGetUserRolesByUidRequestDto, AdminUpdateUserRoleRequestDto, CreateRbacApiPathRequestDto, CreateRbacRoleRequestDto, DeleteRbacApiPathRequestDto, DeleteRbacRoleRequestDto, GetRbacApiPathRequestDto, GetRbacRoleRequestDto, UpdateApiPathPermissionsForRoleRequestDto } from './RbacControllerDto.js' + +/** + * 创建 RBAC API 路径 + * @param ctx context + * @param next context + */ +export const createRbacApiPathController = async (ctx: koaCtx, next: koaNext) => { + const data = ctx.request.body as Partial + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const createRbacApiPathRequest: CreateRbacApiPathRequestDto = { + apiPath: data.apiPath ?? '', + apiPathType: data.apiPathType, + apiPathColor: data.apiPathColor, + apiPathDescription: data.apiPathDescription, + } + const createRbacApiPathResponse = await createRbacApiPathService(createRbacApiPathRequest, uuid, token) + ctx.body = createRbacApiPathResponse + await next() +} + +/** + * 删除 RBAC API 路径 + * @param ctx context + * @param next context + */ +export const deleteRbacApiPathController = async (ctx: koaCtx, next: koaNext) => { + const data = ctx.request.body as Partial + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const deleteRbacApiPathRequest: DeleteRbacApiPathRequestDto = { + apiPath: data.apiPath ?? '', + } + const deleteRbacApiPathResponse = await deleteRbacApiPathService(deleteRbacApiPathRequest, uuid, token) + ctx.body = deleteRbacApiPathResponse + await next() +} + +/** + * 获取 RBAC API 路径 + * @param ctx context + * @param next context + */ +export const getRbacApiPathController = async (ctx: koaCtx, next: koaNext) => { + const apiPath = ctx.query.apiPath as string + const apiPathType = ctx.query.apiPathType as string + const apiPathColor = ctx.query.apiPathColor as string + const apiPathDescription = ctx.query.apiPathDescription as string + const page = parseInt(ctx.query.page as string, 10) + const pageSize = parseInt(ctx.query.pageSize as string, 10) + + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const getRbacApiPathRequest: GetRbacApiPathRequestDto = { + search: { + apiPath, + apiPathType, + apiPathColor, + apiPathDescription, + }, + pagination: { + page, + pageSize, + }, + } + const getRbacApiPathResponse = await getRbacApiPathService(getRbacApiPathRequest, uuid, token) + ctx.body = getRbacApiPathResponse + await next() +} + +/** + * 创建 RBAC 角色 + * @param ctx context + * @param next context + */ +export const createRbacRoleController = async (ctx: koaCtx, next: koaNext) => { + const data = ctx.request.body as Partial + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const createRbacRoleRequest: CreateRbacRoleRequestDto = { + roleName: data.roleName ?? '', + roleType: data.roleType, + roleColor: data.roleColor, + roleDescription: data.roleDescription, + } + const createRbacRoleResponse = await createRbacRoleService(createRbacRoleRequest, uuid, token) + ctx.body = createRbacRoleResponse + await next() +} + +/** + * 删除 RBAC 角色 + * @param ctx context + * @param next context + */ +export const deleteRbacRoleController = async (ctx: koaCtx, next: koaNext) => { + const data = ctx.request.body as Partial + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const deleteRbacRoleRequest: DeleteRbacRoleRequestDto = { + roleName: data.roleName ?? '', + } + const deleteRbacRoleResponse = await deleteRbacRoleService(deleteRbacRoleRequest, uuid, token) + ctx.body = deleteRbacRoleResponse + await next() +} + +/** + * 获取 RBAC 角色 + * @param ctx context + * @param next context + */ +export const getRbacRoleController = async (ctx: koaCtx, next: koaNext) => { + const roleName = ctx.query.roleName as string + const roleType = ctx.query.roleType as string + const roleColor = ctx.query.roleColor as string + const roleDescription = ctx.query.roleDescription as string + const page = parseInt(ctx.query.page as string, 10) + const pageSize = parseInt(ctx.query.pageSize as string, 10) + + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const getRbacRoleRequest: GetRbacRoleRequestDto = { + search: { + roleName, + roleType, + roleColor, + roleDescription, + }, + pagination: { + page, + pageSize, + }, + } + const getRbacRoleResponse = await getRbacRoleService(getRbacRoleRequest, uuid, token) + ctx.body = getRbacRoleResponse + await next() +} + +/** + * 为角色更新 API 路径权限 + * @param ctx context + * @param next context + */ +export const updateApiPathPermissionsForRoleController = async (ctx: koaCtx, next: koaNext) => { + const data = ctx.request.body as Partial + const uuid = ctx.cookies.get('uuid') ?? '' + const token = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid, apiPath: ctx.path }, ctx)) { + return + } + + const updateApiPathPermissionsForRoleRequest: UpdateApiPathPermissionsForRoleRequestDto = { + roleName: data.roleName ?? '', + apiPathPermissions: data.apiPathPermissions ?? [] + } + const updateApiPathPermissionsForRoleResponse = await updateApiPathPermissionsForRoleService(updateApiPathPermissionsForRoleRequest, uuid, token) + ctx.body = updateApiPathPermissionsForRoleResponse + await next() +} + +/** + * 管理员更新用户角色 + * @param ctx context + * @param next context + */ +export const adminUpdateUserRoleController = async (ctx: koaCtx, next: koaNext) => { + const data = ctx.request.body as Partial + + const adminUuid = ctx.cookies.get('uuid') ?? '' + const adminToken = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid: adminUuid, apiPath: ctx.path }, ctx)) { + return + } + + const adminUpdateUserRoleRequest: AdminUpdateUserRoleRequestDto = { + uuid: data.uuid ?? '', + newRoles: data.newRoles ?? [] + } + const adminUpdateUserRoleResponseDto = await adminUpdateUserRoleService(adminUpdateUserRoleRequest, adminUuid, adminToken) + ctx.body = adminUpdateUserRoleResponseDto + await next() +} + +/** + * 通过 uid 获取一个用户的角色 + * @param ctx context + * @param next context + */ +export const adminGetUserRolesByUidController = async (ctx: koaCtx, next: koaNext) => { + const uid = parseInt(ctx.query.uid as string, 10) + + const adminUuid = ctx.cookies.get('uuid') ?? '' + const adminToken = ctx.cookies.get('token') ?? '' + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid: adminUuid, apiPath: ctx.path }, ctx)) { + return + } + + const adminGetUserRolesByUidRequest: AdminGetUserRolesByUidRequestDto = { + uid, + } + const adminGetUserRolesByUidResponse = await adminGetUserRolesByUidService(adminGetUserRolesByUidRequest, adminUuid, adminToken) + ctx.body = adminGetUserRolesByUidResponse + await next() +} diff --git a/src/controller/RbacControllerDto.ts b/src/controller/RbacControllerDto.ts new file mode 100644 index 0000000..ab4060d --- /dev/null +++ b/src/controller/RbacControllerDto.ts @@ -0,0 +1,316 @@ +/** + * 通过 RBAC 检查用户的权限的参数 + */ +export type CheckUserRbacParams = + | { uuid: string; apiPath: string } + | { uid: number; apiPath: string }; + +/** + * 通过 RBAC 检查用户的权限的结果 + */ +export type CheckUserRbacResult = { + status: 200 | 403 | 500; + message: string; +} + +/** + * RBAC API 路径 + */ +type RbacApiPath = { + /** API 路径的 UUID - 非空 - 唯一 */ + apiPathUuid: string; + /** API 路径 - 非空 - 唯一 */ + apiPath: string; + /** API 路径的类型 */ + apiPathType?: string; + /** API 路径的颜色 - 例子:#66CCFFFF */ + apiPathColor?: string; + /** API 路径的描述 */ + apiPathDescription?: string; + /** API 路径创建者 - 非空 */ + creatorUuid: string; + /** API 路径最后更新者 - 非空 */ + lastEditorUuid: string; + /** 系统专用字段-创建时间 - 非空 */ + createDateTime: number; + /** 系统专用字段-最后编辑时间 - 非空 */ + editDateTime: number; +} + +/** + * RBAC API 路径的结果 + */ +type RbacApiPathResult = RbacApiPath & { + /** 该路径是否已经被分配了至少一次 */ + isAssignedOnce: boolean; +} + +/** + * 创建 RBAC API 路径的请求载荷 + */ +export type CreateRbacApiPathRequestDto = { + /** API 路径*/ + apiPath: string; + /** API 路径的类型 */ + apiPathType?: string; + /** API 路径的颜色 - 例子:#66CCFFFF */ + apiPathColor?: string; + /** API 路径的描述 */ + apiPathDescription?: string; +} + +/** + * 创建 RBAC API 路径的请求响应 + */ +export type CreateRbacApiPathResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 如果成功,返回创建的数据 */ + result?: RbacApiPathResult; +} + +/** + * 删除 RBAC API 路径的请求载荷 + */ +export type DeleteRbacApiPathRequestDto = { + /** API 路径*/ + apiPath: string; +} + +/** + * 删除 RBAC API 路径的请求响应 + */ +export type DeleteRbacApiPathResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 该 API 路径是否已经绑定到角色(如果绑定了角色则无法删除) */ + isAssigned: boolean; +} + +/** + * 获取 API 路劲的请求载荷 + */ +export type GetRbacApiPathRequestDto = { + /** 搜索项 */ + search: { + /** API 路径*/ + apiPath?: string; + /** API 路径的类型 */ + apiPathType?: string; + /** API 路径的颜色 - 例子:#66CCFFFF */ + apiPathColor?: string; + /** API 路径的描述 */ + apiPathDescription?: string; + }; + /** 分页查询 */ + pagination: { + /** 当前在第几页 */ + page: number; + /** 一页显示多少条 */ + pageSize: number; + }; +} + +/** + * 获取 API 路劲的请求响应 + */ +export type GetRbacApiPathResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 如果成功,返回数据 */ + result?: RbacApiPathResult[]; + /** 如果成功,返回合计数据 */ + count?: number; +} + +/** + * RBAC 角色 + */ +type RbacRole = { + /** 角色的 UUID */ + roleUuid: string; + /** 角色的名字 */ + roleName: string; + /** 角色的类型 */ + roleType?: string; + /** 角色的颜色 - 例子:#66CCFFFF */ + roleColor?: string; + /** 角色的描述 */ + roleDescription?: string; + /** 这个角色有哪些 API 路径的访问权 */ + apiPathPermissions: string[]; + /** API 路径创建者 - 非空 */ + creatorUuid: string; + /** API 路径最后更新者 - 非空 */ + lastEditorUuid: string; + /** 系统专用字段-创建时间 - 非空 */ + createDateTime: number; + /** 系统专用字段-最后编辑时间 - 非空 */ + editDateTime: number; +} + +/** + * 创建 RBAC 角色的请求载荷 + */ +export type CreateRbacRoleRequestDto = { + /** 角色的名字 */ + roleName: string; + /** 角色的类型 */ + roleType?: string; + /** 角色的颜色 - 例子:#66CCFFFF */ + roleColor?: string; + /** 角色的描述 */ + roleDescription?: string; +} + +/** + * 创建 RBAC 角色的请求响应 + */ +export type CreateRbacRoleResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 如果成功,返回创建的数据 */ + result?: RbacRole; +} + +/** + * 删除 RBAC 角色的请求载荷 + */ +export type DeleteRbacRoleRequestDto = { + /** 角色的名字 */ + roleName: string; +} + +/** + * 删除 RBAC 角色的请求响应 + */ +export type DeleteRbacRoleResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; +} + +/** + * 获取 RBAC 角色的请求载荷 + */ +export type GetRbacRoleRequestDto = { + /** 搜索项 */ + search: { + /** 角色的名字 */ + roleName?: string; + /** 角色的类型 */ + roleType?: string; + /** 角色的颜色 - 例子:#66CCFFFF */ + roleColor?: string; + /** 角色的描述 */ + roleDescription?: string; + }; + /** 分页查询 */ + pagination: { + /** 当前在第几页 */ + page: number; + /** 一页显示多少条 */ + pageSize: number; + }; +} + +/** + * 获取 RBAC 角色的请求响应 + */ +export type GetRbacRoleResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 如果成功,返回数据 */ + result?: ( + & RbacRole + & { apiPathList: RbacApiPathResult[] } + )[]; + /** 如果成功,返回合计数据 */ + count?: number; +} + +/** + * 为角色更新 API 路径权限的请求载荷 + */ +export type UpdateApiPathPermissionsForRoleRequestDto = { + /** 角色的名字 */ + roleName: string; + /** 这个角色有哪些 API 路径的访问权 */ + apiPathPermissions: string[]; +} + +/** + * 为角色更新 API 路径权限的请求响应 + */ +export type UpdateApiPathPermissionsForRoleResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 如果成功,返回数据 */ + result?: RbacRole; +} + +/** + * 通过 UID 获取一个用户的角色的请求载荷 + */ +export type AdminGetUserRolesByUidRequestDto = { + /** 用户的 UID */ + uid: number; +} + +/** + * 通过 UID 获取一个用户的角色的请求响应 + */ +export type AdminGetUserRolesByUidResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; + /** 如果成功,返回数据 */ + result?: { + /** 用户的 UID */ + uid: number; + /** 用户的 UUID */ + uuid: string; + /** 用户名 */ + username: string; + /** 用户昵称 */ + userNickname: string; + /** 用户头像 */ + avatar: string; + /** 用户的角色 */ + roles: RbacRole[]; + }; +} + +/** + * 管理员更新用户角色的请求载荷 + */ +export type AdminUpdateUserRoleRequestDto = { + /** 要被更新角色的用户的 UUID */ + uuid: string; + /** 新的角色 */ + newRoles: string[]; +} + +/** + * 管理员更新用户角色的请求响应 + */ +export type AdminUpdateUserRoleResponseDto = { + /** 是否请求成功 */ + success: boolean; + /** 附加的文本消息 */ + message?: string; +} \ No newline at end of file diff --git a/src/controller/UserController.ts b/src/controller/UserController.ts index d46f146..ff0ff49 100644 --- a/src/controller/UserController.ts +++ b/src/controller/UserController.ts @@ -1,9 +1,10 @@ import { getCorrectCookieDomain } from '../common/UrlTool.js' +import { isPassRbacCheck } from '../service/RbacService.js' import { adminClearUserInfoService, adminGetUserInfoService, approveUserInfoService, - blockUserByUIDService, + // blockUserByUIDService, changePasswordService, checkInvitationCodeService, checkUsernameService, @@ -15,7 +16,7 @@ import { getUserAvatarUploadSignedUrlService, getUserInfoByUidService, getUserSettingsService, - reactivateUserByUIDService, + // reactivateUserByUIDService, requestSendChangeEmailVerificationCodeService, requestSendChangePasswordVerificationCodeService, RequestSendVerificationCodeService, @@ -340,6 +341,14 @@ export const updateUserEmailController = async (ctx: koaCtx, next: koaNext) => { */ export const updateOrCreateUserInfoController = async (ctx: koaCtx, next: koaNext) => { const data = ctx.request.body as Partial + const uid = parseInt(ctx.cookies.get('uid'), 10) + const token = ctx.cookies.get('token') + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const updateOrCreateUserInfoRequest: UpdateOrCreateUserInfoRequestDto = { username: data?.username, userNickname: data?.userNickname, @@ -353,8 +362,6 @@ export const updateOrCreateUserInfoController = async (ctx: koaCtx, next: koaNex userLinkAccounts: data?.userLinkAccounts, userWebsite: data?.userWebsite, } - const uid = parseInt(ctx.cookies.get('uid'), 10) - const token = ctx.cookies.get('token') ctx.body = await updateOrCreateUserInfoService(updateOrCreateUserInfoRequest, uid, token) await next() } @@ -679,43 +686,43 @@ export const checkUsernameController = async (ctx: koaCtx, next: koaNext) => { await next() } -/** - * 根据 UID 封禁一个用户 - * @param ctx context - * @param next context - * @return 封禁用户的请求响应 - */ -export const blockUserByUIDController = async (ctx: koaCtx, next: koaNext) => { - const data = ctx.request.body as Partial - const blockUserByUIDRequest: BlockUserByUIDRequestDto = { - criminalUid: data.criminalUid ?? -1, - } - const uid = parseInt(ctx.cookies.get('uid'), 10) - const token = ctx.cookies.get('token') - - const blockUserByUIDResponse = await blockUserByUIDService(blockUserByUIDRequest, uid, token) - ctx.body = blockUserByUIDResponse - await next() -} - -/** - * 根据 UID 重新激活一个用户 - * @param ctx context - * @param next context - * @return 重新激活用户的请求响应 - */ -export const reactivateUserByUIDController = async (ctx: koaCtx, next: koaNext) => { - const data = ctx.request.body as Partial - const reactivateUserByUIDRequest: ReactivateUserByUIDRequestDto = { - uid: data.uid ?? -1, - } - const uid = parseInt(ctx.cookies.get('uid'), 10) - const token = ctx.cookies.get('token') - - const reactivateUserByUIDResponse = await reactivateUserByUIDService(reactivateUserByUIDRequest, uid, token) - ctx.body = reactivateUserByUIDResponse - await next() -} +// /** +// * 根据 UID 封禁一个用户 +// * @param ctx context +// * @param next context +// * @return 封禁用户的请求响应 +// */ +// export const blockUserByUIDController = async (ctx: koaCtx, next: koaNext) => { +// const data = ctx.request.body as Partial +// const blockUserByUIDRequest: BlockUserByUIDRequestDto = { +// criminalUid: data.criminalUid ?? -1, +// } +// const uid = parseInt(ctx.cookies.get('uid'), 10) +// const token = ctx.cookies.get('token') + +// const blockUserByUIDResponse = await blockUserByUIDService(blockUserByUIDRequest, uid, token) +// ctx.body = blockUserByUIDResponse +// await next() +// } + +// /** +// * 根据 UID 重新激活一个用户 +// * @param ctx context +// * @param next context +// * @return 重新激活用户的请求响应 +// */ +// export const reactivateUserByUIDController = async (ctx: koaCtx, next: koaNext) => { +// const data = ctx.request.body as Partial +// const reactivateUserByUIDRequest: ReactivateUserByUIDRequestDto = { +// uid: data.uid ?? -1, +// } +// const uid = parseInt(ctx.cookies.get('uid'), 10) +// const token = ctx.cookies.get('token') + +// const reactivateUserByUIDResponse = await reactivateUserByUIDService(reactivateUserByUIDRequest, uid, token) +// ctx.body = reactivateUserByUIDResponse +// await next() +// } /** * 获取所有被封禁用户的信息 @@ -727,6 +734,11 @@ export const getBlockedUserController = async (ctx: koaCtx, next: koaNext) => { const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const reactivateUserByUIDResponse = await getBlockedUserService(uid, token) ctx.body = reactivateUserByUIDResponse await next() @@ -742,6 +754,11 @@ export const adminGetUserInfoController = async (ctx: koaCtx, next: koaNext) => const adminUUID = ctx.cookies.get('uuid') const adminToken = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid: adminUUID, apiPath: ctx.path }, ctx)) { + return + } + const isOnlyShowUserInfoUpdatedAfterReviewString = ctx.query.isOnlyShowUserInfoUpdatedAfterReview as string const page = ctx.query.page as string const pageSize = ctx.query.pageSize as string @@ -769,6 +786,11 @@ export const approveUserInfoController = async (ctx: koaCtx, next: koaNext) => { const adminUUID = ctx.cookies.get('uuid') const adminToken = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid: adminUUID, apiPath: ctx.path }, ctx)) { + return + } + const data = ctx.request.body as Partial const approveUserInfoRequest: ApproveUserInfoRequestDto = { @@ -790,6 +812,11 @@ export const adminClearUserInfoController = async (ctx: koaCtx, next: koaNext) = const adminUUID = ctx.cookies.get('uuid') const adminToken = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uuid: adminUUID, apiPath: ctx.path }, ctx)) { + return + } + const data = ctx.request.body as Partial const adminClearUserInfoRequest: AdminClearUserInfoRequestDto = { diff --git a/src/controller/UserControllerDto.ts b/src/controller/UserControllerDto.ts index c7c9751..422871a 100644 --- a/src/controller/UserControllerDto.ts +++ b/src/controller/UserControllerDto.ts @@ -258,7 +258,7 @@ export type GetSelfUserInfoResponseDto = { /** 用户创建时间 */ userCreateDateTime?: number; /** 用户的角色 */ - role?: string; + roles?: string[]; /** 2FA 的类型 */ typeOf2FA?: string; } @@ -306,7 +306,7 @@ export type GetUserInfoByUidResponseDto = { /** 用户创建时间 */ userCreateDateTime?: number; /** 用户的角色 */ - role?: string; + roles?: string[]; }; } diff --git a/src/controller/VideoCommentController.ts b/src/controller/VideoCommentController.ts index bc1a0e9..7d933d8 100644 --- a/src/controller/VideoCommentController.ts +++ b/src/controller/VideoCommentController.ts @@ -1,3 +1,4 @@ +import { isPassRbacCheck } from '../service/RbacService.js' import { adminDeleteVideoCommentService, cancelVideoCommentDownvoteService, cancelVideoCommentUpvoteService, deleteSelfVideoCommentService, emitVideoCommentDownvoteService, emitVideoCommentService, emitVideoCommentUpvoteService, getVideoCommentListByKvidService } from '../service/VideoCommentService.js' import { koaCtx, koaNext } from '../type/koaTypes.js' import { AdminDeleteVideoCommentRequestDto, CancelVideoCommentDownvoteRequestDto, CancelVideoCommentUpvoteRequestDto, DeleteSelfVideoCommentRequestDto, EmitVideoCommentDownvoteRequestDto, EmitVideoCommentRequestDto, EmitVideoCommentUpvoteRequestDto, GetVideoCommentByKvidRequestDto } from './VideoCommentControllerDto.js' @@ -11,6 +12,12 @@ export const emitVideoCommentController = async (ctx: koaCtx, next: koaNext) => const data = ctx.request.body as Partial const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const emitVideoCommentRequest: EmitVideoCommentRequestDto = { /** KVID 视频 ID */ videoId: data.videoId, @@ -154,6 +161,12 @@ export const adminDeleteVideoCommentController = async (ctx: koaCtx, next: koaNe const data = ctx.request.body as Partial const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const adminDeleteVideoCommentRequest: AdminDeleteVideoCommentRequestDto = { /** KVID 视频 ID */ videoId: data.videoId, diff --git a/src/controller/VideoController.ts b/src/controller/VideoController.ts index 0e7a549..22179cf 100644 --- a/src/controller/VideoController.ts +++ b/src/controller/VideoController.ts @@ -1,3 +1,4 @@ +import { isPassRbacCheck } from '../service/RbacService.js' import { approvePendingReviewVideoService, checkVideoExistByKvidService, deleteVideoByKvidService, getPendingReviewVideoService, getThumbVideoService, getVideoByKvidService, getVideoByUidRequestService, getVideoCoverUploadSignedUrlService, getVideoFileTusEndpointService, searchVideoByKeywordService, searchVideoByVideoTagIdService, updateVideoService } from '../service/VideoService.js' import { koaCtx, koaNext } from '../type/koaTypes.js' import { ApprovePendingReviewVideoRequestDto, CheckVideoExistRequestDto, DeleteVideoRequestDto, GetVideoByKvidRequestDto, GetVideoByUidRequestDto, GetVideoFileTusEndpointRequestDto, SearchVideoByKeywordRequestDto, SearchVideoByVideoTagIdRequestDto, UploadVideoRequestDto } from './VideoControllerDto.js' @@ -12,6 +13,11 @@ export const updateVideoController = async (ctx: koaCtx, next: koaNext) => { const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const data = ctx.request.body as Partial const uploadVideoRequest: UploadVideoRequestDto = { title: data.title || '', @@ -124,6 +130,11 @@ export const getVideoFileTusEndpointController = async (ctx: koaCtx, next: koaNe const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const getVideoFileTusEndpointRequest: GetVideoFileTusEndpointRequestDto = { uploadLength: parseInt(ctx.get('Upload-Length'), 10), uploadMetadata: ctx.get('Upload-Metadata') || '', @@ -181,6 +192,11 @@ export const deleteVideoByKvidController = async (ctx: koaCtx, next: koaNext) => const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const data = ctx.request.body as Partial const deleteVideoRequest: DeleteVideoRequestDto = { videoId: data.videoId ?? -1, @@ -201,6 +217,11 @@ export const getPendingReviewVideoController = async (ctx: koaCtx, next: koaNext const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + ctx.body = await getPendingReviewVideoService(uid, token) await next() } @@ -215,6 +236,11 @@ export const approvePendingReviewVideoController = async (ctx: koaCtx, next: koa const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const data = ctx.request.body as Partial const approvePendingReviewVideoRequest: ApprovePendingReviewVideoRequestDto = { videoId: data.videoId ?? -1, diff --git a/src/controller/VideoTagController.ts b/src/controller/VideoTagController.ts index f175206..7dff560 100644 --- a/src/controller/VideoTagController.ts +++ b/src/controller/VideoTagController.ts @@ -1,3 +1,4 @@ +import { isPassRbacCheck } from '../service/RbacService.js' import { createVideoTagService, getVideoTagByTagIdService, searchVideoTagService } from '../service/VideoTagService.js' import { koaCtx, koaNext } from '../type/koaTypes.js' import { CreateVideoTagRequestDto, GetVideoTagByTagIdRequestDto, SearchVideoTagRequestDto } from './VideoTagControllerDto.js' @@ -11,6 +12,12 @@ export const createVideoTagController = async (ctx: koaCtx, next: koaNext) => { const data = ctx.request.body as Partial const uid = parseInt(ctx.cookies.get('uid'), 10) const token = ctx.cookies.get('token') + + // RBAC 权限验证 + if (!await isPassRbacCheck({ uid, apiPath: ctx.path }, ctx)) { + return + } + const createVideoTagRequest: CreateVideoTagRequestDto = { /** 不同语言所对应的 TAG 名 */ tagNameList: data.tagNameList, diff --git a/src/dbPool/schema/RbacSchema.ts b/src/dbPool/schema/RbacSchema.ts new file mode 100644 index 0000000..c9861d6 --- /dev/null +++ b/src/dbPool/schema/RbacSchema.ts @@ -0,0 +1,83 @@ +import { Schema } from 'mongoose' + +/** + * KIRAKIRA RBAC + * + * KIRAKIRA RBAC 原子化权限控制的最小单位是 API 路径。 + * * 一个用户可以拥有多个角色 + * * 一个角色可以对应多位用户 + * * 一个角色可以拥有对多个 API 的访问权限 + * * 一个 API 可以对应多个角色 + */ + +/** + * API 路径的列表 + * KIRAKIRA RBAC 原子化权限控制的最小单位,即精确控制每个 API 接口的访问权限 + */ +class RbacApiSchemaFactory { + /** MongoDB Schema */ + schema = { + /** API 路径的 UUID - 非空 - 唯一 */ + apiPathUuid: { type: String, required: true, unique: true }, + /** API 路径 - 非空 - 唯一 */ + apiPath: { type: String, required: true, unique: true }, + /** API 路径的类型 */ + apiPathType: { type: String }, + /** API 路径的颜色 */ + apiPathColor: { type: String }, + /** API 路径的描述 */ + apiPathDescription: { type: String }, + /** API 路径创建者 - 非空 */ + creatorUuid: { type: String, required: true }, + /** API 路径最后更新者 - 非空 */ + lastEditorUuid: { type: String, required: true }, + /** 系统专用字段-创建时间 - 非空 */ + createDateTime: { type: Number, required: true }, + /** 系统专用字段-最后编辑时间 - 非空 */ + editDateTime: { type: Number, required: true }, + } + /** MongoDB 集合名 */ + collectionName = 'rbac-api-list' + /** Mongoose Schema 实例 */ + schemaInstance = new Schema(this.schema) +} +export const RbacApiSchema = new RbacApiSchemaFactory() + +/** + * RBAC 角色 + * + * 一个用户可以拥有多个角色 + * 一个角色可以对应多位用户 + * 一个角色可以拥有对多个 API 的访问权限 + * 一个 API 可以对应多个角色 + */ +class RbacRoleSchemaFactory { + /** MongoDB Schema */ + schema = { + /** 角色的 UUID */ + roleUuid: { type: String, required: true, unique: true }, + /** 角色的名字 */ + roleName: { type: String, required: true, unique: true }, + /** 角色的类型 */ + roleType: { type: String }, + /** 角色的颜色 */ + roleColor: { type: String }, + /** 角色的描述 */ + roleDescription: { type: String }, + /** 这个角色有哪些 API 路径的访问权 */ + apiPathPermissions: { type: [String], required: true }, + /** API 路径创建者 - 非空 */ + creatorUuid: { type: String, required: true }, + /** API 路径最后更新者 - 非空 */ + lastEditorUuid: { type: String, required: true }, + /** 系统专用字段-创建时间 - 非空 */ + createDateTime: { type: Number, required: true }, + /** 系统专用字段-最后编辑时间 - 非空 */ + editDateTime: { type: Number, required: true }, + } + /** MongoDB 集合名 */ + collectionName = 'rbac-role' + /** Mongoose Schema 实例 */ + schemaInstance = new Schema(this.schema) +} +export const RbacRoleSchema = new RbacRoleSchemaFactory() diff --git a/src/dbPool/schema/UserSchema.ts b/src/dbPool/schema/UserSchema.ts index 454cf52..2558ae8 100644 --- a/src/dbPool/schema/UserSchema.ts +++ b/src/dbPool/schema/UserSchema.ts @@ -20,8 +20,10 @@ class UserAuthSchemaFactory { token: { type: String, required: true }, /** 密码提示 */ passwordHint: String, // TODO: 如何确保密码提示的安全性? + // /** 用户的角色 */ + // role: { type: String, required: true }, /** 用户的角色 */ - role: { type: String, required: true }, + roles: { type: [String], required: true }, /** 用户开启的 2FA 类型 - 非空 */ /* 可以为 email, totp 或 none(表示未开启) */ authenticatorType: { type: String, required: true }, /** 系统专用字段-创建时间 - 非空 */ @@ -50,7 +52,7 @@ const UserLabelSchema = { * 用户的关联账户 */ const UserLinkAccountsSchema = { - /** 关联账户类型 - 非空 - 例:"X" */ + /** 关联账户类型 - 非空 */ accountType: { type: String, required: true }, /** 关联账户唯一标识 - 非空 */ accountUniqueId: { type: String, required: true }, diff --git a/src/route/router.ts b/src/route/router.ts index ad312d5..32831e8 100644 --- a/src/route/router.ts +++ b/src/route/router.ts @@ -7,7 +7,7 @@ import { adminClearUserInfoController, adminGetUserInfoController, approveUserInfoController, - blockUserByUIDController, + // blockUserByUIDController, checkInvitationCodeController, checkUsernameController, checkUserTokenController, @@ -18,7 +18,7 @@ import { getUserAvatarUploadSignedUrlController, getUserInfoByUidController, getUserSettingsController, - reactivateUserByUIDController, + // reactivateUserByUIDController, requestSendChangeEmailVerificationCodeController, requestSendChangePasswordVerificationCodeController, requestSendVerificationCodeController, @@ -45,6 +45,7 @@ import { import { adminDeleteVideoCommentController, cancelVideoCommentDownvoteController, cancelVideoCommentUpvoteController, deleteSelfVideoCommentController, emitVideoCommentController, emitVideoCommentDownvoteController, emitVideoCommentUpvoteController, getVideoCommentListByKvidController } from '../controller/VideoCommentController.js' import { approvePendingReviewVideoController, checkVideoExistController, deleteVideoByKvidController, getPendingReviewVideoController, getThumbVideoController, getVideoByKvidController, getVideoByUidController, getVideoCoverUploadSignedUrlController, getVideoFileTusEndpointController, searchVideoByKeywordController, searchVideoByVideoTagIdController, updateVideoController } from '../controller/VideoController.js' import { createVideoTagController, getVideoTagByTagIdController, searchVideoTagController } from '../controller/VideoTagController.js' +import { adminGetUserRolesByUidController, adminUpdateUserRoleController, createRbacApiPathController, createRbacRoleController, deleteRbacApiPathController, deleteRbacRoleController, getRbacApiPathController, getRbacRoleController, updateApiPathPermissionsForRoleController } from '../controller/RbacController.js' const router = new Router() @@ -270,19 +271,19 @@ router.post('/user/update/password', updateUserPasswordController) // 更新用 router.get('/user/checkUsername', checkUsernameController) // 检查用户名是否可用 // https://localhost:9999/user/checkUsername?username=xxxxxxxx -router.post('/user/blockUser', blockUserByUIDController) // 根据 UID 封禁一个用户 // WARN: 仅限管理员 -// https://localhost:9999/user/blockUser -// cookie: uid, token -// { -// "criminalUid": XXXX -// } +// router.post('/user/blockUser', blockUserByUIDController) // 根据 UID 封禁一个用户 // WARN: 仅限管理员 +// // https://localhost:9999/user/blockUser +// // cookie: uid, token +// // { +// // "criminalUid": XXXX +// // } -router.post('/user/reactivateUser', reactivateUserByUIDController) // 根据 UID 重新激活一个用户 // WARN: 仅限管理员 -// https://localhost:9999/user/reactivateUser -// cookie: uid, token -// { -// "uid": XXXX -// } +// router.post('/user/reactivateUser', reactivateUserByUIDController) // 根据 UID 重新激活一个用户 // WARN: 仅限管理员 +// // https://localhost:9999/user/reactivateUser +// // cookie: uid, token +// // { +// // "uid": XXXX +// // } router.get('/user/blocked/info', getBlockedUserController) // 获取所有被封禁用户的信息 // WARN: 仅限管理员 // https://localhost:9999/user/blocked/info @@ -545,6 +546,104 @@ router.get('/favorites', getFavoritesController) // 获取当前登录用户的 + + + + +router.post('/rbac/createRbacApiPath', createRbacApiPathController) // 创建 RBAC API 路径 +// https://localhost:9999/rbac/createRbacApiPath +// cookie: uuid, token +// { +// "apiPath": "/luo/tian/yi", +// "apiPathType": "tian-yi", +// "apiPathColor": "#66CCFFFF", +// "apiPathDescription": "这里是简介" +// } + +router.delete('/rbac/deleteRbacApiPath', deleteRbacApiPathController) // 删除 RBAC API 路径 +// https://localhost:9999/rbac/deleteRbacApiPath +// cookie: uuid, token +// { +// "apiPath": "/luo/tian/yi" +// } + +router.get('/rbac/getRbacApiPath', getRbacApiPathController) // 获取 RBAC API 路径 +// https://localhost:9999/rbac/getRbacApiPath +// cookie: uuid, token +// +// Query: +// apiPath +// apiPathType +// apiPathColor +// apiPathDescription +// page +// pageSize + +router.post('/rbac/createRbacRole', createRbacRoleController) // 创建 RBAC 角色 +// https://localhost:9999/rbac/createRbacRole +// cookie: uuid, token +// { +// "roleName": "administrator", +// "apiPathType": "administrator", +// "apiPathColor": "#66CCFFFF", +// "apiPathDescription": "这是一个管理员角色,拥有绝大部分内容的管理权限,除了分配角色和其他 ROOT 角色专属的权限。" +// } + +router.delete('/rbac/deleteRbacRole', deleteRbacRoleController) // 删除 RBAC 角色 +// https://localhost:9999/rbac/deleteRbacRole +// cookie: uuid, token +// { +// "roleName": "administrator" +// } + +router.get('/rbac/getRbacRole', getRbacRoleController) // 获取 RBAC 角色 +// https://localhost:9999/rbac/getRbacRole +// cookie: uuid, token +// +// Query: +// roleName +// roleType +// roleColor +// roleDescription +// page +// pageSize + +router.post('/rbac/updateApiPathPermissionsForRole', updateApiPathPermissionsForRoleController) // 为角色更新 API 路径权限 +// https://localhost:9999/rbac/updateApiPathPermissionsForRole +// cookie: uuid, token +// { +// "roleName": "administrator", +// "apiPathPermissions": [ +// "/luo/tian/yi" +// ] +// } + +router.post('/rbac/adminUpdateUserRole', adminUpdateUserRoleController) // 管理员更新用户角色 +// https://localhost:9999/rbac/adminUpdateUserRole +// cookie: uuid, token +// { +// "uuid": "xxxxxxxxxxxxxxxxxxxxxxxxxx", +// "newRoles": [ +// "administrator", +// "user" +// ] +// } + +router.get('/rbac/adminGetUserRolesByUid', adminGetUserRolesByUidController) // 通过 UUID 获取一个用户的角色 +// https://localhost:9999/rbac/adminGetUserRolesByUid +// cookie: uuid, token +// +// Query: +// uid + + + + + + + + + // router.post('/02/koa/user/settings/userSettings/save', saveUserSettingsByUUID) // // http://localhost:9999/02/koa/user/settings/userSettings/save // // diff --git a/src/service/DanmakuService.ts b/src/service/DanmakuService.ts index 153e3b8..2f21e22 100644 --- a/src/service/DanmakuService.ts +++ b/src/service/DanmakuService.ts @@ -3,7 +3,7 @@ import { EmitDanmakuRequestDto, EmitDanmakuResponseDto, GetDanmakuByKvidDto, Get import { insertData2MongoDB, selectDataFromMongoDB } from '../dbPool/DbClusterPool.js' import { QueryType, SelectType } from '../dbPool/DbClusterPoolTypes.js' import { DanmakuSchema } from '../dbPool/schema/DanmakuSchema.js' -import { checkUserRoleService, checkUserTokenService, getUserUuid } from './UserService.js' +import { checkUserTokenService, getUserUuid } from './UserService.js' /** * 用户发送弹幕 @@ -22,11 +22,6 @@ export const emitDanmakuService = async (emitDanmakuRequest: EmitDanmakuRequestD return { success: false, message: '发送弹幕失败,UUID 不存在' } } - if (await checkUserRoleService(uid, 'blocked')) { - console.error('ERROR', '弹幕发送失败,用户已封禁') - return { success: false, message: '弹幕发送失败,用户已封禁' } - } - const { collectionName, schemaInstance } = DanmakuSchema type Danmaku = InferSchemaType const nowDate = new Date().getTime() diff --git a/src/service/RbacService.ts b/src/service/RbacService.ts new file mode 100644 index 0000000..a07f60e --- /dev/null +++ b/src/service/RbacService.ts @@ -0,0 +1,873 @@ +import { InferSchemaType, PipelineStage, Query } from "mongoose"; +import { AdminGetUserRolesByUidRequestDto, AdminGetUserRolesByUidResponseDto, AdminUpdateUserRoleRequestDto, AdminUpdateUserRoleResponseDto, CheckUserRbacParams, CheckUserRbacResult, CreateRbacApiPathRequestDto, CreateRbacApiPathResponseDto, CreateRbacRoleRequestDto, CreateRbacRoleResponseDto, DeleteRbacApiPathRequestDto, DeleteRbacApiPathResponseDto, DeleteRbacRoleRequestDto, DeleteRbacRoleResponseDto, GetRbacApiPathRequestDto, GetRbacApiPathResponseDto, GetRbacRoleRequestDto, GetRbacRoleResponseDto, UpdateApiPathPermissionsForRoleRequestDto, UpdateApiPathPermissionsForRoleResponseDto } from "../controller/RbacControllerDto.js"; +import { checkUserTokenByUuidService, getUserUuid } from "./UserService.js"; +import { deleteDataFromMongoDB, findOneAndUpdateData4MongoDB, insertData2MongoDB, selectDataByAggregateFromMongoDB, selectDataFromMongoDB } from "../dbPool/DbClusterPool.js"; +import { UserAuthSchema, UserInfoSchema } from "../dbPool/schema/UserSchema.js"; +import { RbacApiSchema, RbacRoleSchema } from "../dbPool/schema/RbacSchema.js"; +import { v4 as uuidV4 } from 'uuid' +import { QueryType, SelectType, UpdateType } from "../dbPool/DbClusterPoolTypes.js"; +import { abortAndEndSession, commitSession, createAndStartSession } from "../common/MongoDBSessionTool.js"; +import { koaCtx } from "../type/koaTypes.js"; +import { clearUndefinedItemInObject, isEmptyObject } from "../common/ObjectTool.js"; + +/** + * 通过 RBAC 检查用户的权限 + * @param params 通过 RBAC 检查用户的权限的参数 + * @returns 通过 RBAC 检查用户的权限的结果 + */ +export const checkUserByRbac = async (params: CheckUserRbacParams): Promise => { + try { + const apiPath = params.apiPath + let uuid: string | undefined = undefined + let uid: number | undefined = undefined + if ('uuid' in params) uuid = params.uuid + if ('uid' in params) uid = params.uid + + if (!uuid && uid === undefined) { + console.error('ERROR', '用户执行 RBAC 鉴权时失败,未提供 UUID 或 UID') + return { status: 500, message: `用户执行 RBAC 鉴权时失败,未提供 UUID 或 UID` } + } + + const match = { UUID: uuid, uid } + const clearedMatch = clearUndefinedItemInObject(match) + + const checkUserRbacPipeline: PipelineStage[] = [ + // 匹配用户 + { + $match: clearedMatch, + }, + // 关联 roles 集合 + { + $lookup: { + from: "rbac-roles", + localField: "roles", + foreignField: "roleName", + as: "rolesData" + } + }, + // 展开 rolesData 数组(多个角色) + { $unwind: "$rolesData" }, + // 展开 apiPathNamePermissions 数组(多个权限) + { $unwind: "$rolesData.apiPathPermissions" }, + // 过滤出匹配的 API 路径 + { + $match: { + "rolesData.apiPathPermissions": apiPath + } + }, + // 只返回有权限的数据 + { $project: { UUID: 1 } } + ] + + const { collectionName: userAuthCollectionName, schemaInstance: userAuthSchemaInstance } = UserAuthSchema + type UserAuth = InferSchemaType + const checkUserRbacResult = await selectDataByAggregateFromMongoDB(userAuthSchemaInstance, userAuthCollectionName, checkUserRbacPipeline) + + if (checkUserRbacResult && checkUserRbacResult.success && checkUserRbacResult.result && Array.isArray(checkUserRbacResult.result) && checkUserRbacResult.result.length > 0) { + return { status: 200, message: `用户 ${uuid ? `UUID: ${uuid}` : `UID: ${uid}`} 有权限访问 ${apiPath}` } + } else { + return { status: 403, message: `用户 ${uuid ? `UUID: ${uuid}` : `UID: ${uid}`} 在访问 ${apiPath} 的权限不足,或者用户不存在` } + } + } catch (error) { + console.error('ERROR', '用户执行 RBAC 鉴权时出现错误,未知错误:', error) + return { status: 500, message: '用户执行 RBAC 鉴权时出现错误,未知错误' } + } +} + +/** + * 在 Controller 层通过 RBAC 检查用户的权限 + * 该函数是 checkUserByRbac 的二次封装,包含校验失败时 ctx 中状态码和错误信息的补全功能,并返回简单的 boolean 类型结果,该结果用于在 Controller 中判断后续代码是否需要继续执行 + * @param params 通过 RBAC 检查用户的权限的参数 + * @param ctx koa context + * @returns boolean 类型的权限检查结果,通过返回 true,不通过返回 false + */ +export const isPassRbacCheck = async (params: CheckUserRbacParams, ctx: koaCtx): Promise => { + try { + const rbacCheckResult = await checkUserByRbac(params) + const { status: rbacStatus, message: rbacMessage } = rbacCheckResult + if (rbacStatus !== 200) { + ctx.status = rbacStatus + ctx.body = rbacMessage + console.warn('WARN', 'WARNING', 'RBAC', `${rbacStatus} - ${rbacMessage}`) + return false + } + + return true + } catch (error) { + console.error('ERROR', '在 Controller 层执行 RBAC 鉴权时出现错误,未知错误:', error) + ctx.status = 500 + ctx.body = '在 Controller 层执行 RBAC 鉴权时出现错误,未知错误' + return false + } +} + +/** + * 创建 RBAC API 路径 + * @param createRbacApiPathRequest 创建 RBAC API 路径的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 创建 RBAC API 路径的请求响应 + */ +export const createRbacApiPathService = async (createRbacApiPathRequest: CreateRbacApiPathRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkCreateRbacApiPathRequest(createRbacApiPathRequest)) { + console.error('ERROR', '创建 RBAC API 路径失败,参数不合法') + return { success: false, message: '创建 RBAC API 路径失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '创建 RBAC API 路径失败,用户 Token 校验未通过') + return { success: false, message: '创建 RBAC API 路径失败,用户 Token 校验未通过' } + } + + const { apiPath, apiPathType, apiPathColor, apiPathDescription } = createRbacApiPathRequest + const apiPathUuid = uuidV4() + const now = new Date().getTime() + + const { collectionName: rbacApiCollectionName, schemaInstance: rbacApiSchemaInstance } = RbacApiSchema + type RbacApi = InferSchemaType + + const rbacApiData: RbacApi = { + apiPathUuid, + apiPath, + apiPathType, + apiPathColor, + apiPathDescription, + creatorUuid: uuid, + lastEditorUuid: uuid, + createDateTime: now, + editDateTime: now + } + + const insertResult = await insertData2MongoDB(rbacApiData, rbacApiSchemaInstance, rbacApiCollectionName) + const insertResultData = insertResult?.result?.[0] + + if (!insertResult.success || !insertResultData) { + console.error('ERROR', '创建 RBAC API 路径失败,数据插入失败') + return { success: false, message: '创建 RBAC API 路径失败,数据插入失败' } + } + + return { + success: true, + message: '创建 RBAC API 路径成功', + result: { + apiPathUuid: insertResultData.apiPathUuid, + apiPath: insertResultData.apiPath, + apiPathType: insertResultData.apiPathType, + apiPathColor: insertResultData.apiPathColor, + apiPathDescription: insertResultData.apiPathDescription, + creatorUuid: insertResultData.creatorUuid, + lastEditorUuid: insertResultData.lastEditorUuid, + createDateTime: insertResultData.createDateTime, + editDateTime: insertResultData.editDateTime, + isAssignedOnce: false + } + } + } catch (error) { + console.error('ERROR', '创建 RBAC API 路径时出错,未知错误:', error) + return { success: false, message: '创建 RBAC API 路径时出错,未知错误' } + } +} + +/** + * 删除 RBAC API 路径 + * @param deleteRbacApiPathRequest 删除 RBAC API 路径的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 删除 RBAC API 路径的请求响应 + */ +export const deleteRbacApiPathService = async (deleteRbacApiPathRequest: DeleteRbacApiPathRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkDeleteRbacApiPathRequest(deleteRbacApiPathRequest)) { + console.error('ERROR', '删除 RBAC API 路径失败,参数不合法') + return { success: false, isAssigned: false, message: '删除 RBAC API 路径失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '删除 RBAC API 路径失败,用户 Token 校验未通过') + return { success: false, isAssigned: false, message: '删除 RBAC API 路径失败,用户 Token 校验未通过' } + } + + const { apiPath } = deleteRbacApiPathRequest + + const { collectionName: rbacRoleCollectionName, schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + const chackApiPathUnassignedWhere: QueryType = { + apiPathPermissions: { $in: [apiPath] } + } + const chackApiPathUnassignedSelect: SelectType = { + roleName: 1, + } + + const session = await createAndStartSession() + + const chackApiPathUnassignedResult = await selectDataFromMongoDB(chackApiPathUnassignedWhere, chackApiPathUnassignedSelect, rbacRoleSchemaInstance, rbacRoleCollectionName, { session }) + + if (chackApiPathUnassignedResult.result?.length > 0) { + await abortAndEndSession(session) + console.error('ERROR', '删除 RBAC API 路径失败,该 API 路径已经被绑定到一个角色,请先将其从角色中移出才能删除。') + return { success: false, isAssigned: true, message: '删除 RBAC API 路径失败,该 API 路径已经被绑定到一个角色,请先将其从角色中移出才能删除。' } + } + + const { collectionName: rbacApiCollectionName, schemaInstance: rbacApiSchemaInstance } = RbacApiSchema + type RbacApi = InferSchemaType + + const deleteRbacApiWhere: QueryType = { + apiPath, + } + + const deleteRbacApiResult = await deleteDataFromMongoDB(deleteRbacApiWhere, rbacApiSchemaInstance, rbacApiCollectionName, { session }) + + if (!deleteRbacApiResult.success) { + await abortAndEndSession(session) + console.error('ERROR', '删除 RBAC API 路径失败,数据删除失败') + return { success: false, isAssigned: false, message: '删除 RBAC API 路径失败,数据删除失败' } + } + + await commitSession(session) + return { success: true, isAssigned: false, message: '删除 RBAC API 路径成功' } + } catch (error) { + console.error('ERROR', '创建 RBAC API 路径时出错,未知错误:', error) + return { success: false, isAssigned: false, message: '创建 RBAC API 路径时出错,未知错误' } + } +} + +/** + * 获取 RBAC API 路径 + * @param getRbacApiPathRequest 获取 RBAC API 路径的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 获取 RBAC API 路径的请求响应 + */ +export const getRbacApiPathService = async (getRbacApiPathRequest: GetRbacApiPathRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkGetRbacApiPathRequest(getRbacApiPathRequest)) { + console.error('ERROR', '获取 RBAC API 路径失败,参数不合法') + return { success: false, message: '获取 RBAC API 路径失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '获取 RBAC API 路径失败,用户 Token 校验未通过') + return { success: false, message: '获取 RBAC API 路径失败,用户 Token 校验未通过' } + } + + const { search, pagination } = getRbacApiPathRequest + const clearedSearch = clearUndefinedItemInObject(search) + + let skip = 0 + let pageSize = undefined + if (pagination && pagination.page > 0 && pagination.pageSize > 0) { + skip = (pagination.page - 1) * pagination.pageSize + pageSize = pagination.pageSize + } + + const countRbacApiPathPipeline: PipelineStage[] = [ + ...(!isEmptyObject(clearedSearch) ? [{ + $match: { + $and: Object.entries(clearedSearch).map(([key, value]) => ({ + [key]: { $regex: value, $options: "i" } // 生成模糊查询 + })) + }, + }] : []), + { + $count: 'totalCount', // 统计总文档数 + }, + ] + + const getRbacApiPathPipeline: PipelineStage[] = [ + ...(!isEmptyObject(clearedSearch) ? [{ + $match: { + $and: Object.entries(clearedSearch).map(([key, value]) => ({ + [key]: { $regex: value, $options: "i" } // 生成模糊查询 + })) + }, + }] : []), + { + $lookup: { + from: "rbac-roles", + localField: "apiPath", + foreignField: "apiPathPermissions", + as: "matchedDocs" + } + }, + { + $addFields: { + isAssignedOnce: { $gt: [{ $size: "$matchedDocs" }, 0] } // 如果 matchedDocs 有数据,则为 true + } + }, + { + $project: { + matchedDocs: 0 // 删除 matchedDocs 字段,保持 A 集合的原始结构 + } + }, + { $skip: skip }, // 跳过指定数量的文档 + { $limit: pageSize }, // 限制返回的文档数量 + ] + + const { collectionName: rbacApiCollectionName, schemaInstance: rbacApiSchemaInstance } = RbacApiSchema + type RbacApi = InferSchemaType + + const rbacApiPathCountPromise = selectDataByAggregateFromMongoDB(rbacApiSchemaInstance, rbacApiCollectionName, countRbacApiPathPipeline) + const rbacApiPathDataPromise = selectDataByAggregateFromMongoDB(rbacApiSchemaInstance, rbacApiCollectionName, getRbacApiPathPipeline) + + const [ rbacApiPathCountResult, rbacApiPathDataResult ] = await Promise.all([rbacApiPathCountPromise, rbacApiPathDataPromise]) + const count = rbacApiPathCountResult.result?.[0]?.totalCount + const result = rbacApiPathDataResult.result + + if (!rbacApiPathCountResult.success || !rbacApiPathDataResult.success + || typeof count !== 'number' || count < 0 + || ( Array.isArray(result) && !result ) + ) { + console.error('ERROR', '获取 RBAC API 路径失败,获取数据失败') + return { success: false, message: '获取 RBAC API 路径失败,获取数据失败' } + } + + if (count === 0) { + return { success: true, message: '未查询到 RBAC API 路径', count: 0, result: [] } + } else { + return { success: true, message: '查询 RBAC API 路径成功', count, result } + } + } catch (error) { + console.error('ERROR', '获取 RBAC API 路径时出错,未知错误', error) + return { success: false, message: '获取 RBAC API 路径时出错,未知错误' } + } +} + +/** + * 创建 RBAC 角色 + * @param createRbacRoleRequest 创建 RBAC 角色的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 创建 RBAC 角色的请求响应 + */ +export const createRbacRoleService = async (createRbacRoleRequest: CreateRbacRoleRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkCreateRbacRoleRequest(createRbacRoleRequest)) { + console.error('ERROR', '创建 RBAC 角色失败,参数不合法') + return { success: false, message: '创建 RBAC 角色失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '创建 RBAC 角色失败,用户 Token 校验未通过') + return { success: false, message: '创建 RBAC 角色失败,用户 Token 校验未通过' } + } + + const { roleName, roleType, roleColor, roleDescription } = createRbacRoleRequest + const roleUuid = uuidV4() + const now = new Date().getTime() + + const { collectionName: rbacRoleCollectionName, schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + const rbacRoleData: RbacRole = { + roleUuid, + roleName, + apiPathPermissions: [], + roleType, + roleColor, + roleDescription, + creatorUuid: uuid, + lastEditorUuid: uuid, + createDateTime: now, + editDateTime: now + } + + const insertResult = await insertData2MongoDB(rbacRoleData, rbacRoleSchemaInstance, rbacRoleCollectionName) + const insertResultData = insertResult?.result?.[0] + + if (!insertResult.success || !insertResultData) { + console.error('ERROR', '创建 RBAC 角色失败,数据插入失败') + return { success: false, message: '创建 RBAC 角色失败,数据插入失败' } + } + + return { success: true, message: '创建 RBAC 角色成功', result: insertResultData } + } catch (error) { + console.error('ERROR', '创建 RBAC 角色时出错,未知错误:', error) + return { success: false, message: '创建 RBAC 角色时出错,未知错误' } + } +} + +/** + * 删除 RBAC 角色 + * @param deleteRbacRoleRequest 删除 RBAC 角色的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 删除 RBAC 角色的请求响应 + */ +export const deleteRbacRoleService = async (deleteRbacRoleRequest: DeleteRbacRoleRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkDeleteRbacRoleRequest(deleteRbacRoleRequest)) { + console.error('ERROR', '删除 RBAC 角色失败,参数不合法') + return { success: false, message: '删除 RBAC 角色失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '删除 RBAC 角色失败,用户 Token 校验未通过') + return { success: false, message: '删除 RBAC 角色失败,用户 Token 校验未通过' } + } + + const { roleName } = deleteRbacRoleRequest + + const { collectionName: rbacRoleCollectionName, schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + const deleteRbacRoleWhere: QueryType = { + roleName, + } + + const deleteResult = await deleteDataFromMongoDB(deleteRbacRoleWhere, rbacRoleSchemaInstance, rbacRoleCollectionName) + + if (!deleteResult.success) { + console.error('ERROR', '删除 RBAC 角色失败,数据插入失败') + return { success: false, message: '删除 RBAC 角色失败,数据插入失败' } + } + + return { success: true, message: '删除 RBAC 角色成功' } + } catch (error) { + console.error('ERROR', '删除 RBAC 角色时出错,未知错误:', error) + return { success: false, message: '删除 RBAC 角色时出错,未知错误' } + } +} + +/** + * 获取 RBAC 角色 + * @param getRbacRoleRequest 获取 RBAC 角色的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 获取 RBAC 角色的请求响应 + */ +export const getRbacRoleService = async (getRbacRoleRequest: GetRbacRoleRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkGetRbacRoleRequest(getRbacRoleRequest)) { + console.error('ERROR', '获取 RBAC 角色失败,参数不合法') + return { success: false, message: '获取 RBAC 角色失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '获取 RBAC 角色失败,用户 Token 校验未通过') + return { success: false, message: '获取 RBAC 角色失败,用户 Token 校验未通过' } + } + + const { search, pagination } = getRbacRoleRequest + const clearedSearch = clearUndefinedItemInObject(search) + + let skip = 0 + let pageSize = undefined + if (pagination && pagination.page > 0 && pagination.pageSize > 0) { + skip = (pagination.page - 1) * pagination.pageSize + pageSize = pagination.pageSize + } + + const countRbacRolePipeline: PipelineStage[] = [ + ...(!isEmptyObject(clearedSearch) ? [{ + $match: { + $and: Object.entries(clearedSearch).map(([key, value]) => ({ + [key]: { $regex: value, $options: "i" } // 生成模糊查询 + })) + }, + }] : []), + { + $count: 'totalCount', // 统计总文档数 + }, + ] + + const getRbacRolePipeline: PipelineStage[] = [ + ...(!isEmptyObject(clearedSearch) ? [{ + $match: { + $and: Object.entries(clearedSearch).map(([key, value]) => ({ + [key]: { $regex: value, $options: "i" } // 生成模糊查询 + })) + }, + }] : []), + { + $lookup: { + from: "rbac-api-lists", + localField: "apiPathPermissions", + foreignField: "apiPath", + as: "apiPathList" + } + }, + { + $addFields: { + apiPathList: "$apiPathList" + } + }, + { $skip: skip }, // 跳过指定数量的文档 + { $limit: pageSize }, // 限制返回的文档数量 + ] + + const { collectionName: rbacRoleCollectionName, schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + const rbacRoleCountPromise = selectDataByAggregateFromMongoDB(rbacRoleSchemaInstance, rbacRoleCollectionName, countRbacRolePipeline) + const rbacRoleDataPromise = selectDataByAggregateFromMongoDB(rbacRoleSchemaInstance, rbacRoleCollectionName, getRbacRolePipeline) + + const [ rbacRoleCountResult, rbacRoleDataResult ] = await Promise.all([rbacRoleCountPromise, rbacRoleDataPromise]) + const count = rbacRoleCountResult.result?.[0]?.totalCount + const result = rbacRoleDataResult.result + + if (!rbacRoleCountResult.success || !rbacRoleDataResult.success + || typeof count !== 'number' || count < 0 + || ( Array.isArray(result) && !result ) + ) { + console.error('ERROR', '获取 RBAC 角色失败,获取数据失败') + return { success: false, message: '获取 RBAC 角色失败,获取数据失败' } + } + + if (count === 0) { + return { success: true, message: '未查询到 RBAC 角色', count: 0, result: [] } + } else { + return { success: true, message: '查询 RBAC API 路径成功', count, result } + } + + } catch (error) { + console.error('ERROR', '获取 RBAC 角色时出错,未知错误', error) + return { success: false, message: '获取 RBAC 角色时出错,未知错误' } + } +} + +/** + * 为角色更新 API 路径权限 + * @param updateApiPathPermissionsForRoleRequest 为角色更新 API 路径权限的请求载荷 + * @param uuid 用户 UUID + * @param token 用户 Token + * @returns 为角色更新 API 路径权限的请求响应 + */ +export const updateApiPathPermissionsForRoleService = async (updateApiPathPermissionsForRoleRequest: UpdateApiPathPermissionsForRoleRequestDto, uuid: string, token: string): Promise => { + try { + if (!checkUpdateApiPathPermissionsForRoleRequest(updateApiPathPermissionsForRoleRequest)) { + console.error('ERROR', '为角色更新 API 路径权限失败,参数不合法') + return { success: false, message: '为角色更新 API 路径权限失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(uuid, token)).success) { + console.error('ERROR', '为角色更新 API 路径权限失败,用户 Token 校验未通过') + return { success: false, message: '为角色更新 API 路径权限失败,用户 Token 校验未通过' } + } + + const { roleName, apiPathPermissions } = updateApiPathPermissionsForRoleRequest + const uniqueApiPathPermissions = [...new Set(apiPathPermissions)] + + const { collectionName: rbacApiCollectionName, schemaInstance: rbacApiSchemaInstance } = RbacApiSchema + type RbacApiList = InferSchemaType + + const checkApiPathPermissionsCountWhere: QueryType = { + apiPath: { $in: uniqueApiPathPermissions }, + } + + const checkApiPathPermissionsCountSelect: SelectType = { + apiPath: 1, + } + + const session = await createAndStartSession() + + const checkApiPathPermissionsCountResult = await selectDataFromMongoDB(checkApiPathPermissionsCountWhere, checkApiPathPermissionsCountSelect, rbacApiSchemaInstance, rbacApiCollectionName, { session }) + + if (!checkApiPathPermissionsCountResult.success) { + await abortAndEndSession(session) + console.error('ERROR', '为角色更新 API 路径权限失败,检查 API 路径失败') + return { success: false, message: '为角色更新 API 路径权限失败,检查 API 路径失败' } + } + + if (checkApiPathPermissionsCountResult.result.length !== uniqueApiPathPermissions.length) { + await abortAndEndSession(session) + console.error('ERROR', '为角色更新 API 路径权限失败,检查 API 路径未通过,可能是因为将一个不存在的路径添加到角色中') + return { success: false, message: '为角色更新 API 路径权限失败,检查 API 路径未通过,可能是因为将一个不存在的路径添加到角色中' } + } + + const { collectionName: rbacRoleCollectionName, schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + const updateApiPathPermissions4RoleWhere: QueryType = { + roleName, + } + + const now = new Date().getTime() + const updateApiPathPermissions4RoleData: UpdateType = { + lastEditorUuid: uuid, + apiPathPermissions: uniqueApiPathPermissions as RbacRole['apiPathPermissions'], // TODO: Mongoose issue: #12420 + editDateTime: now, + } + + const updateApiPathPermissions4Role = await findOneAndUpdateData4MongoDB(updateApiPathPermissions4RoleWhere, updateApiPathPermissions4RoleData, rbacRoleSchemaInstance, rbacRoleCollectionName) + + if (!updateApiPathPermissions4Role.success) { + await abortAndEndSession(session) + console.error('ERROR', '为角色更新 API 路径权限失败,更新失败') + return { success: false, message: '为角色更新 API 路径权限失败,更新失败' } + } + + return { success: true, message: '为角色更新 API 路径权限成功', result: updateApiPathPermissions4Role.result } + } catch (error) { + console.error('ERROR', '为角色更新 API 路径权限时出错,未知错误:', error) + return { success: false, message: '为角色更新 API 路径权限时出错,未知错误' } + } +} + +/** + * 管理员更新用户角色 + * @param adminUpdateUserRoleRequest 管理员更新用户角色的请求载荷 + * @param adminUuid 管理员 UUID + * @param adminToken 管理员 Token + * @returns 管理员更新用户角色的请求响应 + */ +export const adminUpdateUserRoleService = async (adminUpdateUserRoleRequest: AdminUpdateUserRoleRequestDto, adminUuid: string, adminToken: string): Promise => { + try { + if (!checkAdminUpdateUserRoleRequest(adminUpdateUserRoleRequest)) { + console.error('ERROR', '管理员更新用户角色失败,参数不合法') + return { success: false, message: '管理员更新用户角色失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(adminUuid, adminToken)).success) { + console.error('ERROR', '管理员更新用户角色失败,用户 Token 校验未通过') + return { success: false, message: '管理员更新用户角色失败,用户 Token 校验未通过' } + } + + const { uuid, newRoles } = adminUpdateUserRoleRequest + const uniqueNewRoels = [...new Set(newRoles)] + + const { collectionName: rbacRoleCollectionName, schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + const checkNewRoelsCountWhere: QueryType = { + roleName: { $in: uniqueNewRoels }, + } + + const checkNewRoelsCountSelect: SelectType = { + roleName: 1, + } + + const session = await createAndStartSession() + + const checkNewRoelsCountResult = await selectDataFromMongoDB(checkNewRoelsCountWhere, checkNewRoelsCountSelect, rbacRoleSchemaInstance, rbacRoleCollectionName, { session }) + + if (!checkNewRoelsCountResult.success) { + await abortAndEndSession(session) + console.error('ERROR', '管理员更新用户角色失败,检查 API 路径失败') + return { success: false, message: '管理员更新用户角色失败,检查 API 路径失败' } + } + + if (checkNewRoelsCountResult.result.length !== uniqueNewRoels.length) { + await abortAndEndSession(session) + console.error('ERROR', '管理员更新用户角色失败,检查角色未通过,可能是因为将一个不存在的角色绑定给用户') + return { success: false, message: '管理员更新用户角色失败,检查角色未通过,可能是因为将一个不存在的角色绑定给用户' } + } + + const { collectionName: userAuthCollectionName, schemaInstance: userAuthSchemaInstance } = UserAuthSchema + type UserAuth = InferSchemaType + + const updateApiPathPermissions4RoleWhere: QueryType = { + UUID: uuid, + } + + const now = new Date().getTime() + const updateApiPathPermissions4RoleData: UpdateType = { + roles: uniqueNewRoels as UserAuth['roles'], // TODO: Mongoose issue: #12420 + editDateTime: now, + } + + const updateRoles4UserResult = await findOneAndUpdateData4MongoDB(updateApiPathPermissions4RoleWhere, updateApiPathPermissions4RoleData, userAuthSchemaInstance, userAuthCollectionName) + + if (!updateRoles4UserResult.success) { + await abortAndEndSession(session) + console.error('ERROR', '管理员更新用户角色失败,更新失败') + return { success: false, message: '管理员更新用户角色失败,更新失败' } + } + + return { success: true, message: '管理员更新用户角色成功' } + } catch (error) { + console.error('ERROR', '管理员更新用户角色时出错,未知错误:', error) + return { success: false, message: '管理员更新用户角色时出错,未知错误' } + } +} + + +/** + * 通过 UID 获取一个用户的角色 + * @param adminGetUserRolesByUidRequest 通过 UID 获取一个用户的角色的请求载荷 + * @param adminUuid 管理员 UUID + * @param adminToken 管理员 Token + * @returns 通过 UID 获取一个用户的角色的请求响应 + */ +export const adminGetUserRolesByUidService = async (adminGetUserRolesByUidRequest: AdminGetUserRolesByUidRequestDto, adminUuid: string, adminToken: string): Promise => { + try { + if (!checkAdminGetUserRolesByUidRequest(adminGetUserRolesByUidRequest)) { + console.error('ERROR', '通过 UID 获取一个用户的角色失败,参数不合法') + return { success: false, message: '通过 UID 获取一个用户的角色失败,参数不合法' } + } + + if (!(await checkUserTokenByUuidService(adminUuid, adminToken)).success) { + console.error('ERROR', '通过 UID 获取一个用户的角色失败,用户 Token 校验未通过') + return { success: false, message: '通过 UID 获取一个用户的角色失败,用户 Token 校验未通过' } + } + + const { uid } = adminGetUserRolesByUidRequest + + const adminGetUserRolesPipeline: PipelineStage[] = [ + { + $match: { + uid, + } + }, + { + $lookup: { + from: "rbac-roles", + localField: "roles", + foreignField: "roleName", + as: "userRole" + } + }, + { + $lookup: { + from: "user-infos", + localField: "UUID", + foreignField: "UUID", + as: "userInfo" + } + }, + { + $unwind: '$userInfo', + }, + { + $project: { + uid: 1, + uuid: '$UUID', + username: '$userInfo.username', + userNickname: '$userInfo.userNickname', + avatar: '$userInfo.avatar', + roles: '$userRole', + } + }, + ] + + const { collectionName: userAuthCollectionName, schemaInstance: userAuthSchemaInstance } = UserAuthSchema + type UserAuth = InferSchemaType + + const { schemaInstance: userInfoSchemaInstance } = UserInfoSchema + type UserInfo = InferSchemaType + + const { schemaInstance: rbacRoleSchemaInstance } = RbacRoleSchema + type RbacRole = InferSchemaType + + + const adminGerUserRolesResult = await selectDataByAggregateFromMongoDB<{ + uid: UserAuth['uid']; + uuid: UserAuth['UUID']; + username: UserInfo['username']; + userNickname: UserInfo['userNickname']; + avatar: UserInfo['avatar']; + roles: RbacRole[]; + }>(userAuthSchemaInstance, userAuthCollectionName, adminGetUserRolesPipeline) + const adminGerUserRolesData = adminGerUserRolesResult.result?.[0] + + console.log('uuuuuu', uid) + console.log('rrrrrrrr', adminGerUserRolesResult.result) + + if (!adminGerUserRolesResult.success || !adminGerUserRolesData) { + console.error('ERROR', '通过 UID 获取一个用户的角色失败,查询数据失败') + return { success: false, message: '通过 UID 获取一个用户的角色失败,查询数据失败' } + } + + return { success: true, message: '通过 UID 获取一个用户的角色成功', result: adminGerUserRolesData } + } catch (error) { + console.error('ERROR', '通过 UID 获取一个用户的角色时出错,未知错误:', error) + return { success: false, message: '通过 UID 获取一个用户的角色时出错,未知错误' } + } +} + +/** + * 校验创建 RBAC API 路径的请求载荷 + * @param createRbacApiPathRequest 创建 RBAC API 路径的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkCreateRbacApiPathRequest = (createRbacApiPathRequest: CreateRbacApiPathRequestDto): boolean => { + return ( + !!createRbacApiPathRequest.apiPath + && createRbacApiPathRequest.apiPathColor ? /^#([0-9A-Fa-f]{8})$/.test(createRbacApiPathRequest.apiPathColor) : true // 如果 apiPathColor 不为空,则测试是否符合八位 HAX 颜色代码格式(例如:#66CCFFFF),如果 apiPathColor 为空,则直接为 true + ) +} + +/** + * 校验删除 RBAC API 路径的请求载荷 + * @param deleteRbacApiPathRequest 删除 RBAC API 路径的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkDeleteRbacApiPathRequest = (deleteRbacApiPathRequest: DeleteRbacApiPathRequestDto): boolean => { + return ( !!deleteRbacApiPathRequest.apiPath ) +} + + +/** + * 校验获取 RBAC API 路径的请求载荷 + * @param getRbacApiPathRequest 获取 RBAC API 路径的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkGetRbacApiPathRequest = (getRbacApiPathRequest: GetRbacApiPathRequestDto): boolean => { + return true // 没有什么好校验的 +} + +/** + * 校验创建 RBAC 角色的请求载荷 + * @param createRbacApiPathRequest 创建 RBAC 角色的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkCreateRbacRoleRequest = (createRbacRoleRequest: CreateRbacRoleRequestDto): boolean => { + return ( + !!createRbacRoleRequest.roleName + && createRbacRoleRequest.roleColor ? /^#([0-9A-Fa-f]{8})$/.test(createRbacRoleRequest.roleColor) : true // 如果 roleColor 不为空,则测试是否符合八位 HAX 颜色代码格式(例如:#66CCFFFF),如果 roleColor 为空,则直接为 true + ) +} + +/** + * 校验删除 RBAC 角色的请求载荷 + * @param createRbacApiPathRequest 删除 RBAC 角色的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkDeleteRbacRoleRequest = (deleteRbacRoleRequest: DeleteRbacRoleRequestDto): boolean => { + return ( !!deleteRbacRoleRequest.roleName ) +} + +/** + * 检查获取 RBAC 角色的请求载荷 + * @param getRbacRoleRequest 获取 RBAC 角色的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkGetRbacRoleRequest = (getRbacRoleRequest: GetRbacRoleRequestDto): boolean => { + return true // 没什么好检查的 +} + +/** + * 校验为角色更新 API 路径权限的请求载荷 + * @param updateApiPathPermissionsForRoleRequest 为角色更新 API 路径权限的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkUpdateApiPathPermissionsForRoleRequest = (updateApiPathPermissionsForRoleRequest: UpdateApiPathPermissionsForRoleRequestDto): boolean => { + return ( + !!updateApiPathPermissionsForRoleRequest.roleName + && !!updateApiPathPermissionsForRoleRequest.apiPathPermissions && Array.isArray(updateApiPathPermissionsForRoleRequest.apiPathPermissions) + && updateApiPathPermissionsForRoleRequest.apiPathPermissions.every(apiPath => !!apiPath) + ) +} + +/** + * 校验管理员更新用户角色的请求载荷 + * @param adminUpdateUserRoleRequest 管理员更新用户角色的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkAdminUpdateUserRoleRequest = (adminUpdateUserRoleRequest: AdminUpdateUserRoleRequestDto): boolean => { + return ( + !!adminUpdateUserRoleRequest.uuid + && !!adminUpdateUserRoleRequest.newRoles && Array.isArray(adminUpdateUserRoleRequest.newRoles) + && adminUpdateUserRoleRequest.newRoles.every(role => !!role) + ) +} + +/** + * 通过 UID 获取一个用户的角色 + * @param adminGetUserRolesByUidRequest 通过 UID 获取一个用户的角色的请求载荷 + * @returns 合法返回 true, 不合法返回 false + */ +const checkAdminGetUserRolesByUidRequest = (adminGetUserRolesByUidRequest: AdminGetUserRolesByUidRequestDto): boolean => { + return ( adminGetUserRolesByUidRequest.uid !== undefined && adminGetUserRolesByUidRequest.uid !== null ) +} diff --git a/src/service/UserService.ts b/src/service/UserService.ts index baa7537..b0dbbb7 100644 --- a/src/service/UserService.ts +++ b/src/service/UserService.ts @@ -175,7 +175,7 @@ export const userRegistrationService = async (userRegistrationRequest: UserRegis passwordHashHash, token, passwordHint, - role: 'user', // newbie will always be user role. + roles: ['user'], // newbie will always has a user role. authenticatorType: 'none', // 刚注册的用户默认没有开启 2FA userCreateDateTime: now, editDateTime: now, @@ -695,11 +695,6 @@ export const updateUserEmailService = async (updateUserEmailRequest: UpdateUserE */ export const updateOrCreateUserInfoService = async (updateOrCreateUserInfoRequest: UpdateOrCreateUserInfoRequestDto, uid: number, token: string): Promise => { try { - if (await checkUserRoleService(uid, 'blocked')) { - console.error('ERROR', '更新或创建用户信息失败,用户已封禁') - return { success: false, message: '更新或创建用户信息失败,用户已封禁' } - } - if (await checkUserToken(uid, token)) { if (checkUpdateOrCreateUserInfoRequest(updateOrCreateUserInfoRequest)) { const { collectionName, schemaInstance } = UserInfoSchema @@ -816,7 +811,7 @@ export const getSelfUserInfoService = async (getSelfUserInfoRequest: GetSelfUser const userAuthSelect: SelectType = { email: 1, // 用户邮箱 userCreateDateTime: 1, // 用户创建日期 - role: 1, // 用户的角色 + roles: 1, // 用户的角色 uid: 1, // 用户 UID UUID: 1, // UUID authenticatorType: 1, // 2FA 的类型 @@ -845,9 +840,9 @@ export const getSelfUserInfoService = async (getSelfUserInfoRequest: GetSelfUser const userAuth = userAuthResult?.result const userInfo = userInfoResult?.result if (userAuth?.length === 0 || userInfo?.length === 0) { - return { success: true, message: '用户未填写用户信息', result: { uid, email: userAuth?.[0]?.email, userCreateDateTime: userAuth[0].userCreateDateTime, role: userAuth[0].role, typeOf2FA: userAuth[0].authenticatorType } } + return { success: true, message: '用户未填写用户信息', result: { uid, email: userAuth?.[0]?.email, userCreateDateTime: userAuth[0].userCreateDateTime, roles: userAuth[0].roles, typeOf2FA: userAuth[0].authenticatorType } } } else if (userAuth?.length === 1 && userAuth?.[0] && userInfo?.length === 1 && userInfo?.[0]) { - return { success: true, message: '获取用户信息成功', result: { ...userInfo[0], email: userAuth[0].email, userCreateDateTime: userAuth[0].userCreateDateTime, role: userAuth[0].role, typeOf2FA: userAuth[0].authenticatorType } } + return { success: true, message: '获取用户信息成功', result: { ...userInfo[0], email: userAuth[0].email, userCreateDateTime: userAuth[0].userCreateDateTime, roles: userAuth[0].roles, typeOf2FA: userAuth[0].authenticatorType } } } else { console.error('ERROR', '获取用户信息时失败,获取到的结果长度不为 1') return { success: false, message: '获取用户信息时失败,结果异常' } @@ -903,7 +898,7 @@ export const getSelfUserInfoByUuidService = async (getSelfUserInfoByUuidRequest: const userAuthSelect: SelectType = { email: 1, // 用户邮箱 userCreateDateTime: 1, // 用户创建日期 - role: 1, // 用户的角色 + roles: 1, // 用户的角色 uid: 1, // 用户 UID UUID: 1, // UUID } @@ -927,9 +922,9 @@ export const getSelfUserInfoByUuidService = async (getSelfUserInfoByUuidRequest: const userAuth = userAuthResult?.result const userInfo = userInfoResult?.result if (userAuth?.length === 0 || userInfo?.length === 0) { - return { success: true, message: '用户未填写用户信息', result: { uuid, email: userAuth?.[0]?.email, userCreateDateTime: userAuth[0].userCreateDateTime, role: userAuth[0].role } } + return { success: true, message: '用户未填写用户信息', result: { uuid, email: userAuth?.[0]?.email, userCreateDateTime: userAuth[0].userCreateDateTime, roles: userAuth[0].roles } } } else if (userAuth?.length === 1 && userAuth?.[0] && userInfo?.length === 1 && userInfo?.[0]) { - return { success: true, message: '获取用户信息成功', result: { ...userInfo[0], email: userAuth[0].email, userCreateDateTime: userAuth[0].userCreateDateTime, role: userAuth[0].role } } + return { success: true, message: '获取用户信息成功', result: { ...userInfo[0], email: userAuth[0].email, userCreateDateTime: userAuth[0].userCreateDateTime, roles: userAuth[0].roles } } } else { console.error('ERROR', '通过 UUID 获取用户信息时失败,获取到的结果长度不为 1') return { success: false, message: '通过 UUID 获取用户信息时失败,结果异常' } @@ -963,7 +958,7 @@ export const getUserInfoByUidService = async (getUserInfoByUidRequest: GetUserIn const userAuthWhere: QueryType = { uid } const userAuthSelect: SelectType = { userCreateDateTime: 1, // 用户创建日期 - role: 1, // 用户的角色 + roles: 1, // 用户的角色 } const { collectionName: userInfoCollectionName, schemaInstance: userInfoSchemaInstance } = UserInfoSchema @@ -987,7 +982,7 @@ export const getUserInfoByUidService = async (getUserInfoByUidRequest: GetUserIn const userAuth = userAuthResult?.result const userInfo = userInfoResult?.result if (userInfo?.length === 1 && userInfo?.[0]) { - return { success: true, message: '获取用户信息成功', result: { ...userInfo[0], userCreateDateTime: userAuth[0].userCreateDateTime, role: userAuth[0].role } } + return { success: true, message: '获取用户信息成功', result: { ...userInfo[0], userCreateDateTime: userAuth[0].userCreateDateTime, roles: userAuth[0].roles } } } else { console.error('ERROR', '获取用户信息时失败,获取到的结果长度不为 1') return { success: false, message: '获取用户信息时失败,结果异常' } @@ -1378,26 +1373,24 @@ export const createInvitationCodeService = async (uid: number, token: string): P try { const userInvitationCodeSelectResult = await selectDataFromMongoDB(userInvitationCodeWhere, userInvitationCodeSelect, schemaInstance, collectionName) - const isAdmin = await checkUserRoleService(uid, 'admin') - /** 如果不是管理员,而且用户创建时间不在七天前 */ - if (!isAdmin) { - try { - const getSelfUserInfoRequest: GetSelfUserInfoRequestDto = { - uid, - token, - } - const selfUserInfo = await getSelfUserInfoService(getSelfUserInfoRequest) - if (!selfUserInfo.success || selfUserInfo.result.userCreateDateTime > nowTime - sevenDaysInMillis) { - console.warn('WARN', 'WARNING', '生成邀请码失败,未超出邀请码生成期限,正在冷却中(第一次)', { uid }) - return { success: true, isCoolingDown: true, message: '生成邀请码失败,未超出邀请码生成期限,正在冷却中(第一次)' } - } - } catch (error) { - console.warn('WARN', 'WARNING', '生成邀请码时出错,查询用户信息出错', { error, uid }) - return { success: false, isCoolingDown: false, message: '生成邀请码时出错,查询用户信息出错' } + // 检查用户上一次创建时间是否在七天内 + try { + const getSelfUserInfoRequest: GetSelfUserInfoRequestDto = { + uid, + token, } + const selfUserInfo = await getSelfUserInfoService(getSelfUserInfoRequest) + if (!selfUserInfo.success || selfUserInfo.result.userCreateDateTime > nowTime - sevenDaysInMillis) { + console.warn('WARN', 'WARNING', '生成邀请码失败,未超出邀请码生成期限,正在冷却中(第一次)', { uid }) + return { success: true, isCoolingDown: true, message: '生成邀请码失败,未超出邀请码生成期限,正在冷却中(第一次)' } + } + } catch (error) { + console.warn('WARN', 'WARNING', '生成邀请码时出错,查询用户信息出错', { error, uid }) + return { success: false, isCoolingDown: false, message: '生成邀请码时出错,查询用户信息出错' } } - if (isAdmin || (userInvitationCodeSelectResult.success && userInvitationCodeSelectResult.result?.length === 0)) { // 是管理员或者没有找到一天内的邀请码,则可以生成邀请码。 + + if (userInvitationCodeSelectResult.success && userInvitationCodeSelectResult.result?.length === 0) { // 没有找到一天内的邀请码,则可以生成邀请码。 try { const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' let finalInvitationCode = '' @@ -2140,63 +2133,64 @@ export const changePasswordService = async (updateUserPasswordRequest: UpdateUse } } -/** - * // TODO: 计划中删除 - * // DELETE ME 这是一个临时的解决方案,以后 Cookie 中直接存储 UUID - * 根据 UID 验证某个用户是否是某个角色 - * @param uid 用户 ID, 为空时会导致校验失败 - * @param role 用户的角色 - * @returns 校验结果,如果用户是这个角色返回 true,否则返回 false - */ -export const checkUserRoleService = async (uid: number, role: string | string[]): Promise => { - try { - if (uid !== undefined && uid !== null && role) { - const { collectionName, schemaInstance } = UserAuthSchema - type UserAuth = InferSchemaType - let userTokenWhere: QueryType = { - uid: -1, - } - if (typeof role === 'string') { - userTokenWhere = { - uid, - role, - } - } else { - userTokenWhere = { - uid, - role: { $in: role }, - } - } - const userTokenSelect: SelectType = { - uid: 1, - } - - try { - const checkUserRoleResult = await selectDataFromMongoDB(userTokenWhere, userTokenSelect, schemaInstance, collectionName) - if (checkUserRoleResult && checkUserRoleResult.success) { - if (checkUserRoleResult.result?.length === 1) { - return true - } else { - console.error('ERROR', `验证用户角色时,用户信息长度不为 1,用户uid:【${uid}】`) - return false - } - } else { - console.error('ERROR', `验证用户角色时未查询到用户信息,用户uid:【${uid}】`) - return false - } - } catch (error) { - console.error('ERROR', `验证用户角色时出错,用户uid:【${uid}】,错误信息:`, error) - return false - } - } else { - console.error('ERROR', `验证用户角色失败!用户 uid 或 role 不存在,用户 UID:${uid}`) - return false - } - } catch (error) { - console.error('ERROR', `验证用户角色失败!用户 UID:${uid}`, error) - return false - } -} +// /** +// * // TODO: 计划中删除 +// * // DELETE ME 这是一个临时的解决方案,以后 Cookie 中直接存储 UUID +// * 根据 UID 验证某个用户是否是某个角色 +// * @param uid 用户 ID, 为空时会导致校验失败 +// * @param role 用户的角色 +// * @returns 校验结果,如果用户是这个角色返回 true,否则返回 false +// */ +// export const checkUserRoleService = async (uid: number, role: string | string[]): Promise => { +// // try { +// // if (uid !== undefined && uid !== null && role) { +// // const { collectionName, schemaInstance } = UserAuthSchema +// // type UserAuth = InferSchemaType +// // let userTokenWhere: QueryType = { +// // uid: -1, +// // } +// // if (typeof role === 'string') { +// // userTokenWhere = { +// // uid, +// // role, +// // } +// // } else { +// // userTokenWhere = { +// // uid, +// // role: { $in: role }, +// // } +// // } +// // const userTokenSelect: SelectType = { +// // uid: 1, +// // } + +// // try { +// // const checkUserRoleResult = await selectDataFromMongoDB(userTokenWhere, userTokenSelect, schemaInstance, collectionName) +// // if (checkUserRoleResult && checkUserRoleResult.success) { +// // if (checkUserRoleResult.result?.length === 1) { +// // return true +// // } else { +// // console.error('ERROR', `验证用户角色时,用户信息长度不为 1,用户uid:【${uid}】`) +// // return false +// // } +// // } else { +// // console.error('ERROR', `验证用户角色时未查询到用户信息,用户uid:【${uid}】`) +// // return false +// // } +// // } catch (error) { +// // console.error('ERROR', `验证用户角色时出错,用户uid:【${uid}】,错误信息:`, error) +// // return false +// // } +// // } else { +// // console.error('ERROR', `验证用户角色失败!用户 uid 或 role 不存在,用户 UID:${uid}`) +// // return false +// // } +// // } catch (error) { +// // console.error('ERROR', `验证用户角色失败!用户 UID:${uid}`, error) +// // return false +// // } +// return role !== 'admin' && role !== 'blocked' +// } /** * 验证某个用户是否是某个角色 @@ -2204,55 +2198,56 @@ export const checkUserRoleService = async (uid: number, role: string | string[]) * @param role 用户的角色 * @returns 校验结果,如果用户是这个角色返回 true,否则返回 false */ -export const checkUserRoleByUUIDService = async (UUID: string, role: string | string[]): Promise => { - try { - if (UUID !== undefined && UUID !== null && role) { - const { collectionName, schemaInstance } = UserAuthSchema - type UserAuth = InferSchemaType - let userTokenWhere: QueryType = { - uid: -1, - } - if (typeof role === 'string') { - userTokenWhere = { - UUID, - role, - } - } else { - userTokenWhere = { - UUID, - role: { $in: role }, - } - } - const userTokenSelect: SelectType = { - uid: 1, - } - - try { - const checkUserRoleResult = await selectDataFromMongoDB(userTokenWhere, userTokenSelect, schemaInstance, collectionName) - if (checkUserRoleResult && checkUserRoleResult.success) { - if (checkUserRoleResult.result?.length === 1) { - return true - } else { - console.error('ERROR', `验证用户角色时,用户信息长度不为 1,用户 UUID: ${UUID}`) - return false - } - } else { - console.error('ERROR', `验证用户角色时未查询到用户信息,用户 UUID:${UUID}`) - return false - } - } catch (error) { - console.error('ERROR', `验证用户角色时出错,用户 UUID:${UUID},错误信息:`, error) - return false - } - } else { - console.error('ERROR', `验证用户角色失败!用户 UUID 或 role 不存在,用户 UUID: ${UUID}`) - return false - } - } catch (error) { - console.error('ERROR', `验证用户角色失败!用户 UUID: ${UUID}`, error) - return false - } -} +// export const checkUserRoleByUUIDService = async (UUID: string, role: string | string[]): Promise => { +// // try { +// // if (UUID !== undefined && UUID !== null && role) { +// // const { collectionName, schemaInstance } = UserAuthSchema +// // type UserAuth = InferSchemaType +// // let userTokenWhere: QueryType = { +// // uid: -1, +// // } +// // if (typeof role === 'string') { +// // userTokenWhere = { +// // UUID, +// // role, +// // } +// // } else { +// // userTokenWhere = { +// // UUID, +// // role: { $in: role }, +// // } +// // } +// // const userTokenSelect: SelectType = { +// // uid: 1, +// // } + +// // try { +// // const checkUserRoleResult = await selectDataFromMongoDB(userTokenWhere, userTokenSelect, schemaInstance, collectionName) +// // if (checkUserRoleResult && checkUserRoleResult.success) { +// // if (checkUserRoleResult.result?.length === 1) { +// // return true +// // } else { +// // console.error('ERROR', `验证用户角色时,用户信息长度不为 1,用户 UUID: ${UUID}`) +// // return false +// // } +// // } else { +// // console.error('ERROR', `验证用户角色时未查询到用户信息,用户 UUID:${UUID}`) +// // return false +// // } +// // } catch (error) { +// // console.error('ERROR', `验证用户角色时出错,用户 UUID:${UUID},错误信息:`, error) +// // return false +// // } +// // } else { +// // console.error('ERROR', `验证用户角色失败!用户 UUID 或 role 不存在,用户 UUID: ${UUID}`) +// // return false +// // } +// // } catch (error) { +// // console.error('ERROR', `验证用户角色失败!用户 UUID: ${UUID}`, error) +// // return false +// // } +// return role !== 'admin' && role !== 'blocked' +// } /** * 检查用户名是否可用 @@ -2297,115 +2292,115 @@ export const checkUsernameService = async (checkUsernameRequest: CheckUsernameRe } } -/** - * 根据 UID 封禁一个用户 - * @param blockUserByUIDRequest 封禁用户的请求载荷 - * @param adminUid 管理员的 UID - * @param adminToken 管理员的 Token - * @returns 封禁用户的请求响应 - */ -export const blockUserByUIDService = async (blockUserByUIDRequest: BlockUserByUIDRequestDto, adminUid: number, adminToken: string): Promise => { - try { - if (checkBlockUserByUIDRequest(blockUserByUIDRequest)) { - if (await checkUserToken(adminUid, adminToken)) { - const isAdmin = await checkUserRoleService(adminUid, 'admin') - if (isAdmin) { - const { criminalUid } = blockUserByUIDRequest - const { collectionName, schemaInstance } = UserAuthSchema - type UserAuth = InferSchemaType - - const blockUserByUIDWhere: QueryType = { - uid: criminalUid, - role: 'user', - } - - const blockUserByUIDUpdate: UpdateType = { - role: 'blocked', - } - try { - const updateResult = await findOneAndUpdateData4MongoDB(blockUserByUIDWhere, blockUserByUIDUpdate, schemaInstance, collectionName, undefined, false) - if (updateResult.success && updateResult.result) { - return { success: true, message: '封禁用户成功' } - } else { - console.error('ERROR', '封禁用户失败,返回结果失败或结果为空') - return { success: false, message: '封禁用户失败,返回结果失败或结果为空' } - } - } catch (error) { - console.error('ERROR', '封禁用户时出错,更新数据时出错', error) - return { success: false, message: '封禁用户时出错,更新数据出错' } - } - } else { - console.error('ERROR', '封禁用户失败,用户权限不足') - return { success: false, message: '封禁用户失败,用户权限不足' } - } - } else { - console.error('ERROR', '封禁用户失败,用户校验未通过') - return { success: false, message: '封禁用户失败,用户校验未通过' } - } - } else { - console.error('ERROR', '封禁用户失败,参数不合法') - return { success: false, message: '封禁用户失败,参数不合法' } - } - } catch (error) { - console.error('ERROR', '封禁用户时出错,未知错误', error) - return { success: false, message: '封禁用户时出错,未知错误' } - } -} - -/** - * 根据 UID 重新激活一个用户 - * @param reactivateUserByUIDRequest 重新激活用户的请求载荷 - * @param adminUid 管理员的 UID - * @param adminToken 管理员的 Token - * @returns 重新激活用户的请求响应 - */ -export const reactivateUserByUIDService = async (reactivateUserByUIDRequest: ReactivateUserByUIDRequestDto, adminUid: number, adminToken: string): Promise => { - try { - if (checkReactivateUserByUIDRequest(reactivateUserByUIDRequest)) { - if (await checkUserToken(adminUid, adminToken)) { - const isAdmin = await checkUserRoleService(adminUid, 'admin') - if (isAdmin) { - const { uid } = reactivateUserByUIDRequest - const { collectionName, schemaInstance } = UserAuthSchema - type UserAuth = InferSchemaType - - const reactivateUserByUIDWhere: QueryType = { - uid, - role: 'blocked', - } - - const reactivateUserByUIDUpdate: UpdateType = { - role: 'user', - } - try { - const updateResult = await findOneAndUpdateData4MongoDB(reactivateUserByUIDWhere, reactivateUserByUIDUpdate, schemaInstance, collectionName, undefined, false) - if (updateResult.success && updateResult.result) { - return { success: true, message: '重新激活用户成功' } - } else { - console.error('ERROR', '重新激活用户失败,返回结果失败或结果为空') - return { success: false, message: '重新激活用户失败,返回结果失败或结果为空' } - } - } catch (error) { - console.error('ERROR', '重新激活用户时出错,更新数据时出错', error) - return { success: false, message: '重新激活用户时出错,更新数据出错' } - } - } else { - console.error('ERROR', '重新激活用户失败,用户权限不足') - return { success: false, message: '重新激活用户失败,用户权限不足' } - } - } else { - console.error('ERROR', '重新激活用户失败,用户校验未通过') - return { success: false, message: '重新激活用户失败,用户校验未通过' } - } - } else { - console.error('ERROR', '重新激活用户失败,参数不合法') - return { success: false, message: '重新激活用户失败,参数不合法' } - } - } catch (error) { - console.error('ERROR', '重新激活用户时出错,未知错误', error) - return { success: false, message: '重新激活用户时出错,未知错误' } - } -} +// /** +// * 根据 UID 封禁一个用户 +// * @param blockUserByUIDRequest 封禁用户的请求载荷 +// * @param adminUid 管理员的 UID +// * @param adminToken 管理员的 Token +// * @returns 封禁用户的请求响应 +// */ +// export const blockUserByUIDService = async (blockUserByUIDRequest: BlockUserByUIDRequestDto, adminUid: number, adminToken: string): Promise => { +// try { +// if (checkBlockUserByUIDRequest(blockUserByUIDRequest)) { +// if (await checkUserToken(adminUid, adminToken)) { +// const isAdmin = await checkUserRoleService(adminUid, 'admin') +// if (isAdmin) { +// const { criminalUid } = blockUserByUIDRequest +// const { collectionName, schemaInstance } = UserAuthSchema +// type UserAuth = InferSchemaType + +// const blockUserByUIDWhere: QueryType = { +// uid: criminalUid, +// role: { $in: ["user"] }, +// } + +// const blockUserByUIDUpdate: UpdateType = { +// role: ['blocked-user'], +// } +// try { +// const updateResult = await findOneAndUpdateData4MongoDB(blockUserByUIDWhere, blockUserByUIDUpdate, schemaInstance, collectionName, undefined, false) +// if (updateResult.success && updateResult.result) { +// return { success: true, message: '封禁用户成功' } +// } else { +// console.error('ERROR', '封禁用户失败,返回结果失败或结果为空') +// return { success: false, message: '封禁用户失败,返回结果失败或结果为空' } +// } +// } catch (error) { +// console.error('ERROR', '封禁用户时出错,更新数据时出错', error) +// return { success: false, message: '封禁用户时出错,更新数据出错' } +// } +// } else { +// console.error('ERROR', '封禁用户失败,用户权限不足') +// return { success: false, message: '封禁用户失败,用户权限不足' } +// } +// } else { +// console.error('ERROR', '封禁用户失败,用户校验未通过') +// return { success: false, message: '封禁用户失败,用户校验未通过' } +// } +// } else { +// console.error('ERROR', '封禁用户失败,参数不合法') +// return { success: false, message: '封禁用户失败,参数不合法' } +// } +// } catch (error) { +// console.error('ERROR', '封禁用户时出错,未知错误', error) +// return { success: false, message: '封禁用户时出错,未知错误' } +// } +// } + +// /** +// * 根据 UID 重新激活一个用户 +// * @param reactivateUserByUIDRequest 重新激活用户的请求载荷 +// * @param adminUid 管理员的 UID +// * @param adminToken 管理员的 Token +// * @returns 重新激活用户的请求响应 +// */ +// export const reactivateUserByUIDService = async (reactivateUserByUIDRequest: ReactivateUserByUIDRequestDto, adminUid: number, adminToken: string): Promise => { +// try { +// if (checkReactivateUserByUIDRequest(reactivateUserByUIDRequest)) { +// if (await checkUserToken(adminUid, adminToken)) { +// const isAdmin = await checkUserRoleService(adminUid, 'admin') +// if (isAdmin) { +// const { uid } = reactivateUserByUIDRequest +// const { collectionName, schemaInstance } = UserAuthSchema +// type UserAuth = InferSchemaType + +// const reactivateUserByUIDWhere: QueryType = { +// uid, +// role: { $in: ['blocked-user'] }, +// } + +// const reactivateUserByUIDUpdate: UpdateType = { +// role: ['user'], +// } +// try { +// const updateResult = await findOneAndUpdateData4MongoDB(reactivateUserByUIDWhere, reactivateUserByUIDUpdate, schemaInstance, collectionName, undefined, false) +// if (updateResult.success && updateResult.result) { +// return { success: true, message: '重新激活用户成功' } +// } else { +// console.error('ERROR', '重新激活用户失败,返回结果失败或结果为空') +// return { success: false, message: '重新激活用户失败,返回结果失败或结果为空' } +// } +// } catch (error) { +// console.error('ERROR', '重新激活用户时出错,更新数据时出错', error) +// return { success: false, message: '重新激活用户时出错,更新数据出错' } +// } +// } else { +// console.error('ERROR', '重新激活用户失败,用户权限不足') +// return { success: false, message: '重新激活用户失败,用户权限不足' } +// } +// } else { +// console.error('ERROR', '重新激活用户失败,用户校验未通过') +// return { success: false, message: '重新激活用户失败,用户校验未通过' } +// } +// } else { +// console.error('ERROR', '重新激活用户失败,参数不合法') +// return { success: false, message: '重新激活用户失败,参数不合法' } +// } +// } catch (error) { +// console.error('ERROR', '重新激活用户时出错,未知错误', error) +// return { success: false, message: '重新激活用户时出错,未知错误' } +// } +// } /** @@ -2417,69 +2412,63 @@ export const reactivateUserByUIDService = async (reactivateUserByUIDRequest: Rea export const getBlockedUserService = async (adminUid: number, adminToken: string): Promise => { try { if (await checkUserToken(adminUid, adminToken)) { - const isAdmin = await checkUserRoleService(adminUid, 'admin') - if (isAdmin) { - const { collectionName: userAuthCollectionName, schemaInstance: userAuthSchemaInstance } = UserAuthSchema + const { collectionName: userAuthCollectionName, schemaInstance: userAuthSchemaInstance } = UserAuthSchema - // TODO: 下方这个 Aggregate 只适用于被封禁用户的搜索 - const blockedUserAggregateProps: PipelineStage[] = [ - { - $match: { - role: 'blocked', - }, + // TODO: 下方这个 Aggregate 只适用于被封禁用户的搜索 + const blockedUserAggregateProps: PipelineStage[] = [ + { + $match: { + role: 'blocked', }, - { - $lookup: { - from: 'user-infos', // WARN: 别忘了加复数 - localField: 'uid', - foreignField: 'uid', - as: 'user_info_data', - }, + }, + { + $lookup: { + from: 'user-infos', // WARN: 别忘了加复数 + localField: 'uid', + foreignField: 'uid', + as: 'user_info_data', }, - { - $unwind: { - path: '$user_info_data', - preserveNullAndEmptyArrays: true, // 保留空数组和null值 - }, + }, + { + $unwind: { + path: '$user_info_data', + preserveNullAndEmptyArrays: true, // 保留空数组和null值 }, - { - $project: { - uid: 1, - UUID: 1, - userCreateDateTime: 1, // 用户创建日期 - role: 1, // 用户的角色 - username: '$user_info_data.username', // 用户名 - userNickname: '$user_info_data.userNickname', // 用户昵称 - avatar: '$user_info_data.avatar', // 用户头像 - userBannerImage: '$user_info_data.userBannerImage', // 用户的背景图 - signature: '$user_info_data.signature', // 用户的个性签名 - gender: '$user_info_data.gender', // 用户的性别 - }, + }, + { + $project: { + uid: 1, + UUID: 1, + userCreateDateTime: 1, // 用户创建日期 + role: 1, // 用户的角色 + username: '$user_info_data.username', // 用户名 + userNickname: '$user_info_data.userNickname', // 用户昵称 + avatar: '$user_info_data.avatar', // 用户头像 + userBannerImage: '$user_info_data.userBannerImage', // 用户的背景图 + signature: '$user_info_data.signature', // 用户的个性签名 + gender: '$user_info_data.gender', // 用户的性别 }, - ] + }, + ] - try { - const userResult = await selectDataByAggregateFromMongoDB(userAuthSchemaInstance, userAuthCollectionName, blockedUserAggregateProps) - if (userResult && userResult.success) { - const userInfo = userResult?.result - if (userInfo?.length > 0) { - return { success: true, message: '获取封禁用户信息成功', - result: userInfo, - } - } else { - return { success: true, message: '没有被封禁用户', result: [] } + try { + const userResult = await selectDataByAggregateFromMongoDB(userAuthSchemaInstance, userAuthCollectionName, blockedUserAggregateProps) + if (userResult && userResult.success) { + const userInfo = userResult?.result + if (userInfo?.length > 0) { + return { success: true, message: '获取封禁用户信息成功', + result: userInfo, } } else { - console.error('ERROR', '获取所有被封禁用户的信息失败,获取到的结果为空') - return { success: false, message: '获取所有被封禁用户的信息失败,结果为空' } + return { success: true, message: '没有被封禁用户', result: [] } } - } catch (error) { - console.error('ERROR', '获取所有被封禁用户的信息失败,查询数据时出错:0', error) - return { success: false, message: '获取所有被封禁用户的信息失败,查询数据时出错' } + } else { + console.error('ERROR', '获取所有被封禁用户的信息失败,获取到的结果为空') + return { success: false, message: '获取所有被封禁用户的信息失败,结果为空' } } - } else { - console.error('ERROR', '获取所有被封禁用户的信息失败,用户权限不足') - return { success: false, message: '获取所有被封禁用户的信息失败,用户权限不足' } + } catch (error) { + console.error('ERROR', '获取所有被封禁用户的信息失败,查询数据时出错:0', error) + return { success: false, message: '获取所有被封禁用户的信息失败,查询数据时出错' } } } else { console.error('ERROR', '获取所有被封禁用户的信息失败,用户校验失败') @@ -2510,11 +2499,6 @@ export const adminGetUserInfoService = async (adminGetUserInfoRequest: AdminGetU return { success: false, message: '管理员获取用户信息失败,用户校验未通过', totalCount: 0 } } - if (!await checkUserRoleByUUIDService(adminUUID, 'admin')) { - console.error('ERROR', '管理员获取用户信息失败,用户权限不足') - return { success: false, message: '管理员获取用户信息失败,用户权限不足', totalCount: 0 } - } - let pageSize = undefined let skip = 0 if (adminGetUserInfoRequest.pagination && adminGetUserInfoRequest.pagination.page > 0 && adminGetUserInfoRequest.pagination.pageSize > 0) { @@ -2630,11 +2614,6 @@ export const approveUserInfoService = async (approveUserInfoRequest: ApproveUser return { success: false, message: '管理员通过用户信息审核失败,用户校验未通过' } } - if (!await checkUserRoleByUUIDService(adminUUID, 'admin')) { - console.error('ERROR', '管理员通过用户信息审核失败,用户权限不足') - return { success: false, message: '管理员通过用户信息审核失败,用户权限不足' } - } - const UUID = approveUserInfoRequest.UUID const { collectionName, schemaInstance } = UserInfoSchema type UserInfo = InferSchemaType @@ -2683,11 +2662,6 @@ export const adminClearUserInfoService = async (adminClearUserInfoRequest: Admin return { success: false, message: '管理员清空某个用户的信息失败,用户校验未通过' } } - if (!await checkUserRoleByUUIDService(adminUUID, 'admin')) { - console.error('ERROR', '管理员清空某个用户的信息失败,用户权限不足') - return { success: false, message: '管理员清空某个用户的信息失败,用户权限不足' } - } - const uid = adminClearUserInfoRequest.uid const UUID = await getUserUuid(uid) if (!UUID) { diff --git a/src/service/VideoCommentService.ts b/src/service/VideoCommentService.ts index b4e7039..e7d1d58 100644 --- a/src/service/VideoCommentService.ts +++ b/src/service/VideoCommentService.ts @@ -5,7 +5,7 @@ import { findOneAndPlusByMongodbId, insertData2MongoDB, selectDataFromMongoDB, u import { QueryType, SelectType } from '../dbPool/DbClusterPoolTypes.js' import { RemovedVideoCommentSchema, VideoCommentDownvoteSchema, VideoCommentSchema, VideoCommentUpvoteSchema } from '../dbPool/schema/VideoCommentSchema.js' import { getNextSequenceValueService } from './SequenceValueService.js' -import { checkUserRoleService, checkUserTokenByUuidService, checkUserTokenService, getUserInfoByUidService, getUserUuid } from './UserService.js' +import { checkUserTokenByUuidService, checkUserTokenService, getUserInfoByUidService, getUserUuid } from './UserService.js' /** * 用户发送视频评论 @@ -18,11 +18,6 @@ export const emitVideoCommentService = async (emitVideoCommentRequest: EmitVideo try { if (checkEmitVideoCommentRequest(emitVideoCommentRequest)) { if ((await checkUserTokenService(uid, token)).success) { - if (await checkUserRoleService(uid, 'blocked')) { - console.error('ERROR', '评论发送失败,用户已封禁') - return { success: false, message: '评论发送失败,用户已封禁' } - } - const UUID = await getUserUuid(uid) // DELETE ME 这是一个临时解决方法,Cookie 中应当存储 UUID if (!UUID) { console.error('ERROR', '评论发送失败,UUID 不存在', { uid }) @@ -959,11 +954,6 @@ export const adminDeleteVideoCommentService = async (adminDeleteVideoCommentRequ return { success: false, message: '管理员删除视频评论失败,用户校验未通过' } } - if (!(await checkUserRoleService(adminUid, 'admin'))) { - console.error('管理员删除视频评论失败,用户权限不足') - return { success: false, message: '管理员删除视频评论失败,用户权限不足' } - } - const adminUUID = await getUserUuid(adminUid) // DELETE ME 这是一个临时解决方法,Cookie 中应当存储 UUID if (!adminUUID) { console.error('ERROR', '管理员删除一条视频评论失败,adminUUID 不存在', { adminUid }) diff --git a/src/service/VideoService.ts b/src/service/VideoService.ts index f4100fc..616792e 100644 --- a/src/service/VideoService.ts +++ b/src/service/VideoService.ts @@ -15,7 +15,7 @@ import { EsSchema2TsType } from '../elasticsearchPool/ElasticsearchClusterPoolTy import { VideoDocument } from '../elasticsearchPool/template/VideoDocument.js' import { createOrUpdateBrowsingHistoryService } from './BrowsingHistoryService.js' import { getNextSequenceValueEjectService } from './SequenceValueService.js' -import { checkUserRoleService, checkUserTokenService, getUserUuid } from './UserService.js' +import { checkUserTokenService, getUserUuid } from './UserService.js' /** * 上传视频 @@ -36,18 +36,6 @@ export const updateVideoService = async (uploadVideoRequest: UploadVideoRequestD return { success: false, message: '上传视频失败, 账户未对齐' } } - if (await checkUserRoleService(uid, 'blocked')) { - console.error('ERROR', '上传视频失败,用户已封禁') - return { success: false, message: '上传视频失败,用户已封禁' } - } - - // DELETE ME: 该验证应当被移除 - if (!await checkUserRoleService(uid, 'admin')) { - console.error('ERROR', '上传视频失败,仅限管理员上传') - return { success: false, message: '上传视频失败,仅限管理员上传' } - } - - const UUID = await getUserUuid(uid) // DELETE ME 这是一个临时解决方法,Cookie 中应当存储 UUID if (!UUID) { console.error('ERROR', '上传视频失败,UUID 不存在', { uid }) @@ -520,17 +508,6 @@ export const searchVideoByKeywordService = async (searchVideoByKeywordRequest: S export const getVideoFileTusEndpointService = async (uid: number, token: string, getVideoFileTusEndpointRequest: GetVideoFileTusEndpointRequestDto): Promise => { try { if ((await checkUserTokenService(uid, token)).success) { - if (await checkUserRoleService(uid, 'blocked')) { - console.error('ERROR', '无法创建 Cloudflare Stream TUS Endpoint, 用户已封禁') - return undefined - } - - // DELETE ME: 该验证应当被移除 - if (!await checkUserRoleService(uid, 'admin')) { - console.error('ERROR', '无法创建 Cloudflare Stream TUS Endpoint, 仅限管理员上传') - return undefined - } - const streamTusEndpointUrl = process.env.CF_STREAM_TUS_ENDPOINT_URL const streamToken = process.env.CF_STREAM_TOKEN @@ -714,86 +691,81 @@ export const deleteVideoByKvidService = async (deleteVideoRequest: DeleteVideoRe return { success: false, message: '删除一个视频失败,adminUUID 不存在' } } - if (await checkUserRoleService(adminUid, 'admin')) { // must have admin role - const videoId = deleteVideoRequest.videoId - const nowDate = new Date().getTime() + const videoId = deleteVideoRequest.videoId + const nowDate = new Date().getTime() - const { collectionName: videoCollectionName, schemaInstance: videoSchemaInstance } = VideoSchema - type Video = InferSchemaType - const deleteWhere: QueryType