diff --git a/TODOLIST.md b/TODOLIST.md index ca2065e..36ce465 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -1,7 +1,8 @@ # TODO LIST -- [ ] 用户组权限 - - [ ] 默认用户组 +- [x] 用户组 + - [ ] 权限 + - [x] 默认用户组 - [ ] 上传到指定Categroy - [ ] 整顿collectin info的goods 列表 - [ ] Token 使用日志显示 @@ -9,6 +10,6 @@ - [ ] 接入统计 - [ ] 配置文件写入初始化用户账号密码 - [ ] 接入AuthBox -- [ ] 缓存整理 - [ ] Redis 接入 - - [x] 支持Session \ No newline at end of file + - [x] 支持Session + - [ ] 缓存整理 \ No newline at end of file diff --git a/src/models/System.ts b/src/models/System.ts new file mode 100644 index 0000000..375b703 --- /dev/null +++ b/src/models/System.ts @@ -0,0 +1,20 @@ +import { model, SchemaDefinition, Model as M } from "mongoose"; +import { Base, IDoc, IDocRaw } from "./common"; + +const Definition: SchemaDefinition = { + key: { type: String, required: true }, + value: { type: String, required: true } +}; + +export interface ISystem extends IDocRaw { + key: string; + value: string; +} + +const SystemSchema = new Base(Definition).createSchema(); + +export const Flag = "sys"; + +export type SystemDoc = IDoc; + +export const Model: M = model(Flag, SystemSchema); diff --git a/src/models/User-Usergroup.ts b/src/models/User-Usergroup.ts new file mode 100644 index 0000000..ae38d5d --- /dev/null +++ b/src/models/User-Usergroup.ts @@ -0,0 +1,50 @@ +import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; +import { FLAG as UF, IUser } from "@models/User"; +import { FLAG as GF, IUsergroups } from "@models/Usergroup"; +import { ObjectId } from "@models/common"; +import { Base, IDoc, IDocRaw } from "./common"; + +const Definition: SchemaDefinition = { + user: { + type: SchemaTypes.ObjectId, + ref: UF, + index: true, + required: true + }, + usergroup: { + type: SchemaTypes.ObjectId, + ref: GF, + index: true, + required: true + } +}; + +/** + * User-Usergroup Model FLAG + */ +export const FLAG = "user-usergroups"; + +/** + * User-Usergroup Doc Interface + */ +export interface IUserUsergroups extends IDocRaw { + user: ObjectId | IUser; + usergroup: ObjectId | IUsergroups; +} + +/** + * User-Usergroup Raw Doc Interface + */ +export interface IUserUsergroupsRaw extends IUserUsergroups { + user: IUser; + usergroup: IUsergroups; +} + +export type UserUsergroupDoc = IDoc; + +const UserUsergroupsSchema = new Base(Definition).createSchema(); + +/** + * User-Usergroup Model + */ +export const Model: M = model(FLAG, UserUsergroupsSchema); diff --git a/src/models/Usergroup.ts b/src/models/Usergroup.ts new file mode 100644 index 0000000..fe1a7c6 --- /dev/null +++ b/src/models/Usergroup.ts @@ -0,0 +1,36 @@ +import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; +import { FLAG as UF, IUser } from "@models/User"; +import { ObjectId } from "@models/common"; +import { Base, IDoc, IDocRaw } from "./common"; + +const Definition: SchemaDefinition = { + name: { type: String, required: true } +}; + +export const FLAG = "usergroups"; + +export interface IUsergroups extends IDocRaw { + name: string; +} + +export type UsergroupDoc = IDoc; + +const UsergroupsSchema = new Base(Definition).createSchema(); + +UsergroupsSchema.path("name").validate({ + isAsync: true, + validator: async function nameExistValidator(val, respond) { + if (!this.isNew) { + const id = this.getQuery()._id; + const ug = await Model.findById(id).exec(); + if (ug.toObject().user === val) { + return respond(true); + } + } + const ug = await Model.findOne({ name: val }).exec(); + return respond(!ug); + }, + message: "The Name is exist" +}); + +export const Model: M = model(FLAG, UsergroupsSchema); diff --git a/src/modules/common/dtos/ids.dto.ts b/src/modules/common/dtos/ids.dto.ts index ce69680..02ee5ff 100644 --- a/src/modules/common/dtos/ids.dto.ts +++ b/src/modules/common/dtos/ids.dto.ts @@ -79,6 +79,12 @@ export class GidDto implements IGidDto { public readonly gid: ObjectId; } +export class UGidDto implements IGidDto { + @ApiModelProperty({ type: String, description: "Usergroup ID" }) + @IsMongoId() + public readonly gid: ObjectId; +} + export interface IUidDto { /** * User MongoID diff --git a/src/modules/common/services/system.service.ts b/src/modules/common/services/system.service.ts new file mode 100644 index 0000000..c131a45 --- /dev/null +++ b/src/modules/common/services/system.service.ts @@ -0,0 +1,38 @@ +import { Component } from "@nestjs/common"; +import { ObjectId } from "@models/common"; +import { Model as SystemModel } from "@models/System"; +import { Model as UsergroupsModel } from "@models/Usergroup"; + +@Component() +export class SystemService { + + public static DEFAULT_USERGROUP_FLAG = "DEFAULT_USERGROUP"; + + /** + * Get Default Usergroup ID + * @returns Usergroup ID + */ + public async getDefaultUsergroup(): Promise { + let gid: any = await SystemModel.findOne({ + key: SystemService.DEFAULT_USERGROUP_FLAG + }).exec(); + if (!gid) { + gid = (await UsergroupsModel.findOne().exec())._id; + this.setDefaultUsergroup(gid); + } + return gid; + } + + /** + * Set Default Usergroup ID + * @param gid Usergroup ID + */ + public setDefaultUsergroup(gid: ObjectId) { + return SystemModel.findOneAndUpdate( + { key: SystemService.DEFAULT_USERGROUP_FLAG }, { value: gid }, + { upsert: true } + ) + .exec(); + } + +} diff --git a/src/modules/common/services/usergroups.service.ts b/src/modules/common/services/usergroups.service.ts new file mode 100644 index 0000000..98f5cdb --- /dev/null +++ b/src/modules/common/services/usergroups.service.ts @@ -0,0 +1,120 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { Model as UsersModel } from "@models/User"; +import { Model as UsergroupsModel } from "@models/Usergroup"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { ObjectId } from "@models/common"; +import { DEF_PER_COUNT, IPerPage } from "@dtos/page"; + +@Component() +export class UsergroupsService { + + private DEF_PER_OBJ: IPerPage = { + perNum: DEF_PER_COUNT, + page: 1 + }; + + public async add(obj: object) { + try { + return await UsergroupsModel.create(obj); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async edit(id: ObjectId, obj: object) { + try { + return await UsergroupsModel.update( + { _id: id }, obj, { runValidators: true, context: "query" } + ).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public usersCount(gid: ObjectId) { + return UserUsergroupsModel.count({ usergroup: gid }).exec(); + } + + public async usersCountPage(id: ObjectId, perNum = DEF_PER_COUNT) { + const total = await this.usersCount(id); + return Math.ceil(total / perNum); + } + + public getGroup(gid: ObjectId) { + return UsergroupsModel.findById(gid).exec(); + } + + public async getGroupUsers( + gid: ObjectId, pageObj: IPerPage = this.DEF_PER_OBJ + ) { + const perNum = pageObj.perNum; + const page = pageObj.page; + return (await UserUsergroupsModel.find({ usergroup: gid }) + .skip((page - 1) * perNum).limit(perNum) + .populate("user").exec() + ).map((item) => { + return item.toObject().user; + }); + } + + public count() { + return UsergroupsModel.count({ }).exec(); + } + + public async countPage(perNum = DEF_PER_COUNT) { + const total = await this.count(); + return Math.ceil(total / perNum); + } + + public list(pageObj: IPerPage = this.DEF_PER_OBJ) { + const perNum = pageObj.perNum; + const page = pageObj.page; + return UsergroupsModel.find({ }) + .skip((page - 1) * perNum).limit(perNum) + .sort({ createdAt: -1 }) + .exec(); + } + + public async remove(gid: ObjectId) { + if ((await this.count()) === 1) { + throw new BadRequestException("Nnn delete unique group"); + } + try { + return await UsergroupsModel.findByIdAndRemove(gid).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async addUserToGroup(gid: ObjectId, uid: ObjectId) { + if (!(await UsersModel.findById(uid).exec())) { + throw new BadRequestException("The User ID is not exist"); + } + if (!(await UsergroupsModel.findById(gid).exec())) { + throw new BadRequestException("The Usergroup ID is not exist"); + } + if (await UserUsergroupsModel.findOne({ + user: uid, usergroup: gid + }).exec()) { + throw new BadRequestException("User has been in the usergroup"); + } + try { + return await UserUsergroupsModel.create({ + user: uid, usergroup: gid + }); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async removeUserFromGroup(gid: ObjectId, uid: ObjectId) { + try { + await UserUsergroupsModel.findOneAndRemove({ + user: uid, usergroup: gid + }).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + +} diff --git a/src/modules/common/services/users.service.ts b/src/modules/common/services/users.service.ts index d9d1c1d..27bd62a 100644 --- a/src/modules/common/services/users.service.ts +++ b/src/modules/common/services/users.service.ts @@ -1,10 +1,68 @@ import { Component, BadRequestException } from "@nestjs/common"; import { ObjectId } from "@models/common"; import { Model as UsersModel, UserDoc } from "@models/User"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { IUsergroups } from "@models/Usergroup"; +import { SystemService } from "@services/system"; +import { IPerPage, DEF_PER_COUNT } from "@dtos/page"; @Component() export class UsersService { + private DEF_PER_OBJ: IPerPage = { + perNum: DEF_PER_COUNT, + page: 1 + }; + + constructor(private readonly sysSvr: SystemService) { } + + public async addUser(obj, gid?: ObjectId) { + try { + const user = await UsersModel.addUser(obj.username, obj.password); + if (!gid) { + gid = await this.sysSvr.getDefaultUsergroup(); + } + await UserUsergroupsModel.create({ + user: user._id, usergroup: gid + }); + return user; + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async removeUser(uid: ObjectId) { + try { + await UsersModel.removeUser(uid); + await UserUsergroupsModel.remove({ user: uid }).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public countUsergroups(uid: ObjectId) { + return UserUsergroupsModel.count({ user: uid }).exec(); + } + + public async countPageUsergroups(uid: ObjectId, perNum = DEF_PER_COUNT) { + const total = await this.countUsergroups(uid); + return Math.ceil(total / perNum); + } + + public async getUsergroups( + uid: ObjectId, pageObj: IPerPage = this.DEF_PER_OBJ + ) { + const perNum = pageObj.perNum; + const page = pageObj.page; + const groups = await UserUsergroupsModel + .find({ user: uid }).populate("usergroup") + .skip((page - 1) * perNum).limit(perNum) + .exec(); + return groups.map((item) => { + return item.toObject().usergroup as IUsergroups; + }); + } + /** * 修改`User`属性, 除了`username` * @param id User ID diff --git a/src/modules/controllers.module.ts b/src/modules/controllers.module.ts index fd4cf44..7770fe7 100644 --- a/src/modules/controllers.module.ts +++ b/src/modules/controllers.module.ts @@ -14,6 +14,7 @@ import { CollectionsAdminController } from "./collections/collections.admin.controller"; import { TokensAdminController } from "./tokens/tokens.controller"; +import { UsergroupsAdminController } from "./usergroups/usergroups.controller"; // endregion Controllers // region Middlewares @@ -33,19 +34,28 @@ import { import { CollectionsService } from "@services/collections"; import { UsersService } from "@services/users"; import { TokensService } from "@services/tokens"; +import { UsergroupsService } from "@services/usergroups"; +import { SystemService } from "@services/system"; // endregion Services export const controllers = [ FilesController, GoodsController, - UsersAdminController, AuthAdminController, RegexpsAdminController, + UsersAdminController, AuthAdminController, + UsergroupsAdminController, + RegexpsAdminController, CategoriesAdminController, GoodsAdminController, TokensAdminController, CollectionsController, CollectionsAdminController ]; +export const services = [ + CollectionsService, TokensService, UsersService, UsergroupsService, + SystemService +]; + @Module({ controllers, - components: [ CollectionsService, TokensService, UsersService ] + components: [ ...services ] }) export class ControllersModule { private uploadFileMethod = { diff --git a/src/modules/database/database.providers.ts b/src/modules/database/database.providers.ts index d495479..eff2f04 100644 --- a/src/modules/database/database.providers.ts +++ b/src/modules/database/database.providers.ts @@ -2,6 +2,10 @@ import * as mongoose from "mongoose"; import { isArray } from "util"; import { config } from "@utils/config"; import { Model as UsersModel } from "@models/User"; +import { Model as UsergroupsModel } from "@models/Usergroup"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { Model as SystemModel } from "@models/System"; +import { SystemService } from "@services/system"; import { systemLogger } from "../common/helper/log"; const getDatabaseUrl = () => { @@ -21,21 +25,40 @@ export const connectDatabase = () => { return new Promise((resolve, reject) => { const connection = mongoose.connect(getDatabaseUrl(), { useMongoClient: true, - }, (err) => { + }, async (err) => { if (err) { return reject(err); } systemLogger.info("Connected Database."); - UsersModel.count({ }).exec().then((num) => { - if (num === 0) { - return UsersModel.addUser("root", "admin"); - } - }); + await injectData(); return resolve(connection); }); }); }; +export const injectData = async () => { + let num = await UsersModel.count({ }).exec(); + if (num === 0) { + return UsersModel.addUser("root", "admin"); + } + num = await UsergroupsModel.count({ }).exec(); + if (num === 0) { + const group = await UsergroupsModel.create({ name: "admin" }); + const conditions = (await UsersModel.find({ }).exec()) + .map((item) => { + return { + user: item._id, + usergroup: group._id + }; + }); + await SystemModel.findOneAndUpdate( + { key: SystemService.DEFAULT_USERGROUP_FLAG }, + { value: group._id.toString() }, { upsert: true } + ).exec(); + await UserUsergroupsModel.create(conditions); + } +}; + export const databaseProviders = [ { provide: "DbConnectionToken", diff --git a/src/modules/usergroups/usergroups.controller.ts b/src/modules/usergroups/usergroups.controller.ts new file mode 100644 index 0000000..187cfac --- /dev/null +++ b/src/modules/usergroups/usergroups.controller.ts @@ -0,0 +1,156 @@ +import { UseGuards, Controller, Get, Query, HttpStatus, HttpCode, Post, Body, Param, Delete } from "@nestjs/common"; +import { ApiUseTags, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { RolesGuard } from "@guards/roles"; +import { UsergroupsService } from "@services/usergroups"; +import { Roles } from "@decorators/roles"; +import { ParseIntPipe } from "@pipes/parse-int"; +import { PerPageDto, ListResponse, DEF_PER_COUNT } from "@dtos/page"; +import { UGidDto } from "@dtos/ids"; + +import { AddUsergroupDto, EditUsergroupDto, UserUsergroupDto } from "./usergroups.dto"; + +@UseGuards(RolesGuard) +@ApiUseTags("User Groups") +@Controller("api/v1/usergroups") +export class UsergroupsAdminController { + + constructor(private readonly ugSvr: UsergroupsService) { } + + @Roles("admin") + @Get() + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Get Usergroup List" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Usergroup List", + type: ListResponse + }) + // endregion Swagger Docs + public async getList(@Query(new ParseIntPipe()) query: PerPageDto) { + const curPage = query.page || 1; + const perNum = query.perNum || DEF_PER_COUNT; + const resData = new ListResponse(); + resData.current = curPage; + resData.totalPages = await this.ugSvr.countPage(perNum); + resData.total = await this.ugSvr.count(); + if (resData.totalPages >= resData.current) { + resData.data = await this.ugSvr.list({ + page: curPage, perNum + }); + } + return resData; + } + + @Roles("admin") + @Post() + // region Swagger Docs + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ title: "New Usergroup" }) + @ApiResponse({ + status: HttpStatus.CREATED, description: "Success" + }) + // endregion Swagger Docs + public addUsergroup(@Body() body: AddUsergroupDto) { + return this.ugSvr.add(body); + } + + @Roles("admin") + @Get("/:gid/delete") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Delete Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Delete Success" + }) + // endregion Swagger Docs + public async removeUsergroupByGet(@Param() param: UGidDto) { + return this.ugSvr.remove(param.gid); + } + + @Roles("admin") + @Delete("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Delete Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Delete Success" + }) + // endregion Swagger Docs + public async removeUsergroupByDelete(@Param() param: UGidDto) { + return this.ugSvr.remove(param.gid); + } + + @Roles("admin") + @Post("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Modify Usergroup Info" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Modify Success" + }) + // endregion Swagger Docs + public async editUsergroup( + @Param() param: UGidDto, @Body() body: EditUsergroupDto + ) { + return this.ugSvr.edit(param.gid, body); + } + + @Roles("admin") + @Get("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Get Usergroup Info" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Usergroup Info" + }) + // endregion Swagger Docs + public async getUsergroup( + @Param() param: UGidDto, @Query(new ParseIntPipe()) query: PerPageDto + ) { + let group: any = await this.ugSvr.getGroup(param.gid); + if (!group) { + return group; + } + group = group.toObject(); + const users = new ListResponse(); + const curPage = query.page || 1; + const perNum = query.perNum || DEF_PER_COUNT; + users.current = curPage; + users.totalPages = await this.ugSvr.usersCountPage(param.gid, perNum); + users.total = await this.ugSvr.usersCount(param.gid); + if (users.totalPages >= users.current) { + users.data = await this.ugSvr.getGroupUsers(param.gid, { + page: curPage, perNum + }); + } + group.users = users; + return group; + } + + @Roles("admin") + @Get("/:gid/add/:uid") + // region Swagger Docs + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ title: "Add User to Usergroup" }) + @ApiResponse({ + status: HttpStatus.CREATED, description: "Add Success" + }) + // endregion Swagger Docs + public addUser(@Param() param: UserUsergroupDto) { + return this.ugSvr.addUserToGroup(param.gid, param.uid); + } + + @Roles("admin") + @Get("/:gid/remove/:uid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Remove User from Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Remove Success" + }) + // endregion Swagger Docs + public removeUser(@Param() param: UserUsergroupDto) { + return this.ugSvr.removeUserFromGroup(param.gid, param.uid); + } + +} diff --git a/src/modules/usergroups/usergroups.dto.ts b/src/modules/usergroups/usergroups.dto.ts new file mode 100644 index 0000000..fcacae5 --- /dev/null +++ b/src/modules/usergroups/usergroups.dto.ts @@ -0,0 +1,25 @@ +import { ApiModelProperty } from "@nestjs/swagger"; +import { IsString, IsMongoId } from "class-validator"; +import { ObjectId } from "@models/common"; +import { IGidDto, IUidDto } from "@dtos/ids"; + +export class AddUsergroupDto { + @ApiModelProperty({ type: String, description: "Usergroup Name" }) + @IsString() + public readonly name: string; +} + +export class EditUsergroupDto { + @ApiModelProperty({ type: String, description: "Usergroup Name" }) + @IsString() + public readonly name: string; +} + +export class UserUsergroupDto implements IGidDto, IUidDto { + @ApiModelProperty({ type: String, description: "User ID" }) + @IsMongoId() + public readonly uid: ObjectId; + @ApiModelProperty({ type: String, description: "Usergroup ID" }) + @IsMongoId() + public readonly gid: ObjectId; +} diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index ecbd06a..f1eeabb 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -6,6 +6,7 @@ import { ApiBearerAuth, ApiUseTags, ApiResponse, ApiOperation, ApiImplicitParam } from "@nestjs/swagger"; import { Model as UserModel, IUser, UserDoc } from "@models/User"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; import { Model as TokensModel } from "@models/Token"; import { Model as GoodsModels } from "@models/Good"; import { CollectionDoc } from "@models/Collection"; @@ -14,14 +15,16 @@ import { Roles } from "@decorators/roles"; import { RolesGuard } from "@guards/roles"; import { ParseIntPipe } from "@pipes/parse-int"; import { TokensService } from "@services/tokens"; +import { UsergroupsService } from "@services/usergroups"; import { CollectionsService } from "@services/collections"; import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; import { PerPageDto, ListResponse, DEF_PER_COUNT } from "@dtos/page"; import { UidDto } from "@dtos/ids"; import { DefResDto } from "@dtos/res"; import { - CreateUserDto, ModifyPasswordDto, EditUserDto + CreateUserDto, ModifyPasswordDto, EditUserDto, UsergroupBodyDto } from "./users.dto"; @UseGuards(RolesGuard) @@ -35,7 +38,9 @@ export class UsersAdminController { constructor( private readonly tokensSvr: TokensService, private readonly collectionsSvr: CollectionsService, - private readonly usersSvr: UsersService + private readonly usersSvr: UsersService, + private readonly ugSvr: UsergroupsService, + private readonly sysSvr: SystemService ) { } @Roles("admin") @@ -75,14 +80,8 @@ export class UsersAdminController { status: HttpStatus.BAD_REQUEST, description: "Add User Fail" }) // endregion Swagger Docs - public async addUser(@Body() user: CreateUserDto) { - let obj; - try { - obj = await UserModel.addUser(user.username, user.password); - } catch (error) { - throw new BadRequestException(error.toString()); - } - return obj; + public addUser(@Body() user: CreateUserDto) { + return this.usersSvr.addUser(user); } @Roles("admin") @@ -153,12 +152,8 @@ export class UsersAdminController { status: HttpStatus.BAD_REQUEST, description: "Delete User Fail" }) // endregion Swagger Docs - public async delete(@Param() user: UidDto) { - try { - await UserModel.removeUser(user.uid); - } catch (error) { - throw new BadRequestException(error.toString()); - } + public delete(@Param() user: UidDto) { + this.usersSvr.removeUser(user.uid); return new DefResDto(); } @@ -362,6 +357,44 @@ export class UsersAdminController { // endregion Collection Methods //////////////////////////////////////// + //////////////////////////////////////// + // region Usergroup Methods + //////////////////////////////////////// + + @Roles("admin") + @Get("/:uid/usergroups") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Get Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, type: ListResponse + }) + // endregion Swagger Docs + public async getUsergroups( + @Param() param: UidDto, @Query(new ParseIntPipe()) query: PerPageDto + ) { + const curPage = query.page || 1; + const perNum = query.perNum || DEF_PER_COUNT; + const totalPages = + await this.usersSvr.countPageUsergroups(param.uid, query.perNum); + const totalCount = await this.usersSvr.countUsergroups(param.uid); + + const resData = new ListResponse(); + resData.current = curPage; + resData.totalPages = totalPages; + resData.total = totalCount; + if (totalPages >= curPage) { + resData.data = await this.usersSvr.getUsergroups(param.uid, { + page: curPage, perNum + }); + } + return resData; + } + + //////////////////////////////////////// + // endregion Usergroup Methods + //////////////////////////////////////// + @Roles("admin") @Get("/:uid") // region Swagger Docs diff --git a/src/modules/users/users.dto.ts b/src/modules/users/users.dto.ts index df44a52..49b6c2d 100644 --- a/src/modules/users/users.dto.ts +++ b/src/modules/users/users.dto.ts @@ -1,6 +1,7 @@ import { IsString, IsMongoId } from "class-validator"; import { ObjectId } from "@models/common"; import { ApiUseTags, ApiModelProperty } from "@nestjs/swagger"; +import { IUidDto, IGidDto } from "@dtos/ids"; export class CreateUserDto { @ApiModelProperty({ type: String, description: "Username" }) @@ -25,3 +26,12 @@ export class ModifyPasswordDto { @IsString() public readonly newPassword: string; } + +export class UsergroupBodyDto { + @ApiModelProperty({ type: String, description: "Action" }) + @IsString() + public readonly type: "add" | "remove"; + @ApiModelProperty({ type: String, description: "Usergroup ID" }) + @IsMongoId({ each: true }) + public readonly gids: ObjectId; +} diff --git a/test/api/user_usergroup.e2e.ts b/test/api/user_usergroup.e2e.ts new file mode 100644 index 0000000..1d611a2 --- /dev/null +++ b/test/api/user_usergroup.e2e.ts @@ -0,0 +1,82 @@ +import supertest = require("supertest"); +import faker = require("faker"); + +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; +import { UsergroupsService } from "@services/usergroups"; + +import { connect, drop, addCategoryAndRegexp } from "../helpers/database"; +import { init } from "../helpers/server"; +import { newUsergroup, getLinkIdsByUserId, getLinkIdsByUsergroupId } from "../helpers/database/usergroups"; +import { newUser, newUserWithUsergroup } from "../helpers/database/user"; + +describe("User's Usergroup E2E Api", () => { + + let request: supertest.SuperTest; + + before(() => { + return connect(); + }); + + const ids = { + users: [ ], + usergroups: [ ] + }; + + after(() => { + return drop(ids); + }); + + before(async () => { + request = await init(); + }); + + const user = { + username: `${faker.name.firstName()}${Math.random()}`, + password: faker.random.words() + }; + step("Login", async () => { + const userDoc = await newUserWithUsergroup( + user.username, user.password + ); + ids.users.push(userDoc._id); + await request.post("/api/v1/auth/login").send(user).then(); + }); + + step("Get Usergroup", async () => { + const url = `/api/v1/users/${ids.users[0]}/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.should.have.property("total", 1); + }); + + step("User Add 4 Usergroups", async () => { + const uid = ids.users[0]; + for (let i = 0; i < 4; i++) { + const group = await newUsergroup(undefined, uid); + ids.usergroups.push(group._id); + } + }); + + step("Have 5 Usergroups ", async () => { + const url = `/api/v1/users/${ids.users[0]}/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.should.have.property("total", 1 + ids.usergroups.length); + }); + + step("Remove one usergroup", () => { + const uid = ids.users[0]; + const url = `/api/v1/usergroups/${ids.usergroups[0]}/remove/${uid}`; + return request.get(url).then(); + }); + + step("Have 4 Usergroups ", async () => { + const url = `/api/v1/users/${ids.users[0]}/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.should.have.property("total", ids.usergroups.length); + }); + +}); diff --git a/test/api/usergroups.e2e.ts b/test/api/usergroups.e2e.ts new file mode 100644 index 0000000..057b57d --- /dev/null +++ b/test/api/usergroups.e2e.ts @@ -0,0 +1,120 @@ +import supertest = require("supertest"); +import faker = require("faker"); + +import { + connect, drop, addCategoryAndRegexp +} from "../helpers/database"; +import { init } from "../helpers/server"; +import { + newUsergroup, getLinkIdsByUserId +} from "../helpers/database/usergroups"; +import { newUser, newUserWithUsergroup } from "../helpers/database/user"; + +describe("Usergroup E2E Api", () => { + + let request: supertest.SuperTest; + + before(() => { + return connect(); + }); + + const ids = { + users: [ ], + usergroups: [ ], + userusergroups: [ ] + }; + + after(() => { + return drop(ids); + }); + + before(async () => { + request = await init(); + }); + + const user = { + username: `${faker.name.firstName()}${Math.random()}`, + password: faker.random.words() + }; + step("Login", async () => { + const userDoc = + await newUserWithUsergroup(user.username, user.password); + ids.users.push(userDoc._id); + await request.post("/api/v1/auth/login").send(user).then(); + }); + + step("New Usergroup * 2", async () => { + for (let i = 0; i < 2; i++) { + const url = `/api/v1/usergroups`; + const name = `${faker.random.word()}${Math.random()}`; + const { status, body: result } = + await request.post(url).send({ name }).then(); + status.should.be.eql(201); + ids.usergroups.push(result._id); + } + }); + + step("Usergroup List", async () => { + const url = `/api/v1/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.total.should.aboveOrEqual(2); + result.data.should.be.an.Array(); + }); + + step("Get Usergroup Info", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.users.data.should.be.an.Array(); + result.users.total.should.be.eql(0); + }); + + step("Add User into Usergroup", async () => { + const gid = ids.usergroups[ids.usergroups.length - 1]; + const uid = ids.users[0]; + const url = `/api/v1/usergroups/${gid}/add/${uid}`; + const { status } = await request.get(url).then(); + status.should.be.eql(201); + }); + + step("Have 1 User", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.users.total.should.be.eql(1); + }); + + step("Fail to Add User into Same Usergroup", async () => { + const gid = ids.usergroups[ids.usergroups.length - 1]; + const uid = ids.users[0]; + const url = `/api/v1/usergroups/${gid}/add/${uid}`; + const { status } = await request.get(url).then(); + status.should.be.eql(400); + }); + + step("Modify Usergroup's name", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}`; + const name = `${faker.random.word()}${Math.random()}`; + const { status } = await request.post(url).send({ name }).then(); + status.should.be.eql(200); + const { body: result } = await request.get(url).then(); + result.should.have.property("name", name); + }); + + step("Delete By GET", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}/delete`; + const { status } = await request.get(url).then(); + }); + + step("Delete By DELETE", async () => { + const id = ids.usergroups[ids.usergroups.length - 2]; + const url = `/api/v1/usergroups/${id}`; + const { status } = await request.delete(url).then(); + }); + +}); diff --git a/test/helpers/database.ts b/test/helpers/database.ts index 393c1a8..3a1cfdf 100644 --- a/test/helpers/database.ts +++ b/test/helpers/database.ts @@ -1,7 +1,6 @@ import faker = require("faker"); import { config } from "@utils/config"; import { ObjectId } from "@models/common"; -import { connectDatabase } from "../../src/modules/database/database.providers"; import { Model as ValuesModel } from "@models/Value"; import { Model as GoodsModels } from "@models/Good"; @@ -10,6 +9,11 @@ import { Model as UsersModel } from "@models/User"; import { Model as TokensModel } from "@models/Token"; import { Model as CollectionsModel } from "@models/Collection"; import { Model as CategoriesModel, CategoryDoc } from "@models/Categroy"; +import { Model as UsergroupsModel } from "@models/Usergroup"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; + +import { connectDatabase } from "../../src/modules/database/database.providers"; +import { newUser as newUserFn } from "./database/user"; config.db.database = "storebox-test"; @@ -37,26 +41,27 @@ export const drop = async (ids?: IIds) => { await CategoriesModel.remove({ }).exec(); return; } - for (const id of (ids.values || [ ])) { - await ValuesModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.goods || [ ])) { - await GoodsModels.findByIdAndRemove(id).exec(); - } - for (const id of (ids.regexps || [ ])) { - await RegexpsModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.users || [ ])) { - await UsersModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.tokens || [ ])) { - await TokensModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.categories || [ ])) { - await CategoriesModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.collections || [ ])) { - await CollectionsModel.findByIdAndRemove(id).exec(); + const MODEL_IDMETHOD_MAP = { + "values": ValuesModel, + "goods": GoodsModels, + "regexps": RegexpsModel, + "users": UsersModel, + "tokens": TokensModel, + "categories": CategoriesModel, + "collections": CollectionsModel, + "usergroups": UsergroupsModel, + "userusergroups": UserUsergroupsModel + }; + for (const method of Object.keys(MODEL_IDMETHOD_MAP)) { + const model = MODEL_IDMETHOD_MAP[method]; + for (const id of (ids[method] || [ ])) { + await model.findByIdAndRemove(id).exec(); + if (method === "users") { + await UserUsergroupsModel.remove({ user: id }).exec(); + } else if (method === "usergroups") { + await UserUsergroupsModel.remove({ usergroup: id }).exec(); + } + } } }; @@ -68,9 +73,7 @@ export const addCategoryAndRegexp = async (regexp: RegExp) => { return [category, reg]; }; -export const newUser = (username: string, password: string) => { - return UsersModel.addUser(username, password); -}; +export const newUser = newUserFn; export const newRegexp = (name: string, value: RegExp, link?) => { return RegexpsModel.addRegexp(name, value.source, link); diff --git a/test/helpers/database/user.ts b/test/helpers/database/user.ts new file mode 100644 index 0000000..b7fb827 --- /dev/null +++ b/test/helpers/database/user.ts @@ -0,0 +1,22 @@ +import { Model as UsersModel } from "@models/User"; +import { ObjectId } from "@models/common"; +import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; +import faker = require("faker"); + +export const newUser = ( + username = `${faker.name.firstName()}${Math.random()}`, + password = `${faker.random.words()}${Math.random()}` +) => { + return UsersModel.addUser(username, password); +}; + +export const newUserWithUsergroup = ( + username = `${faker.name.firstName()}${Math.random()}`, + password = `${faker.random.words()}${Math.random()}`, + gid?: ObjectId +) => { + return new UsersService(new SystemService()).addUser({ + username, password + }, gid); +}; diff --git a/test/helpers/database/usergroups.ts b/test/helpers/database/usergroups.ts new file mode 100644 index 0000000..2e5a39f --- /dev/null +++ b/test/helpers/database/usergroups.ts @@ -0,0 +1,31 @@ +import faker = require("faker"); +import { ObjectId } from "@models/common"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { UsergroupsService } from "@services/usergroups"; + +/// + +export const getLinkIdsByUserId = async (uid: ObjectId) => { + return (await UserUsergroupsModel.find({ user: uid }).exec()) + .map((item) => { + return item._id as ObjectId; + }); +}; + +export const getLinkIdsByUsergroupId = async (gid: ObjectId) => { + return (await UserUsergroupsModel.find({ usergroup: gid }).exec()) + .map((item) => { + return item._id as ObjectId; + }); +}; + +export const newUsergroup = async ( + name = `${faker.random.word}${Math.random()}`, uid?: ObjectId +) => { + const svr = new UsergroupsService(); + const group = await svr.add({ name }); + if (uid) { + await svr.addUserToGroup(group._id, uid); + } + return group; +}; diff --git a/test/issues/ban_user_n_its_token.e2e.ts b/test/issues/ban_user_n_its_token.e2e.ts index 98ea6ee..5d3e5e9 100644 --- a/test/issues/ban_user_n_its_token.e2e.ts +++ b/test/issues/ban_user_n_its_token.e2e.ts @@ -5,12 +5,13 @@ import { connect, drop, newUser } from "../helpers/database"; import { init } from "../helpers/server"; import { UsersService } from "@services/users"; import { TokensService } from "@services/tokens"; +import { SystemService } from "@services/system"; describe("Fix Issues", () => { let request: supertest.SuperTest; const tokensSvr = new TokensService(); - const usersSvr = new UsersService(); + const usersSvr = new UsersService(new SystemService()); before(() => { return connect(); diff --git a/test/services/users.spec.ts b/test/services/users.spec.ts index bc0fcaa..56df9ab 100644 --- a/test/services/users.spec.ts +++ b/test/services/users.spec.ts @@ -1,4 +1,5 @@ import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; import { Model as UsersModel } from "@models/User"; import db = require("../helpers/database"); import faker = require("faker"); @@ -20,7 +21,7 @@ describe("Users Service Test Unit", () => { }); beforeEach(() => { - usersSvr = new UsersService(); + usersSvr = new UsersService(new SystemService()); }); const user = {