Skip to content

Commit

Permalink
Admin user management (#80)
Browse files Browse the repository at this point in the history
* Updated BaseEntity navigation properties to reference User type

* Used auto mapper's array mapper for mapping array to DTO

* Removed vscode local settings from version control

* Updated createdBy field in user entity

* Added user auto mapper profile

* Added user auto mapper profile

* Updated package.json and set tenant and user globally. Closes #16, closes #17

* Implement save or update feature. Closes #11

* Moved hooks to base entity class and implemented soft-delete

* Implement soft delete feature. Closes #56

* Fixed #63, and refactor some part of test files (#14)

* Simplify returned entity in signUp

* Removed global variables and wrote UserController tests

* Correct model class extensions

* Implement user management by admin

* User management

* Updated swagger and packages
  • Loading branch information
ofuochi authored Oct 21, 2019
1 parent a170084 commit 7e89d42
Show file tree
Hide file tree
Showing 18 changed files with 645 additions and 92 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"@types/mailgun-js": "^0.22.3",
"@types/method-override": "0.0.31",
"@types/mocha": "^5.2.7",
"@types/mongoose": "^5.5.21",
"@types/mongoose": "^5.5.22",
"@types/supertest": "^2.0.8",
"@types/swagger-ui-express": "^3.0.1",
"@types/winston": "^2.4.4",
Expand All @@ -110,7 +110,7 @@
"eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.18.2",
"ghooks": "^2.0.4",
"mocha": "^6.2.1",
"mocha": "^6.2.2",
"mongodb-memory-server": "^5.2.8",
"nodemon": "^1.19.4",
"nyc": "^14.1.1",
Expand Down
22 changes: 10 additions & 12 deletions src/domain/model/base.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { instanceMethod, pre, prop, Ref, Typegoose } from "@hasezoey/typegoose";
import { Expose } from "class-transformer";
import { Query } from "mongoose";

import { Writable } from "../utils/writable";
import { User } from "./user";
import { iocContainer } from "../../infrastructure/config/ioc";
import { TYPES } from "../constants/types";
import { DecodedJwt } from "../../ui/services/auth_service";
import { TYPES } from "../constants/types";
import { Writable } from "../utils/writable";
import { User } from "./user";

// eslint-disable-next-line
@pre<BaseEntity>("findOneAndUpdate", function(this: Query<BaseEntity>, next) {
Expand All @@ -27,13 +26,17 @@ import { DecodedJwt } from "../../ui/services/auth_service";
}
})
// eslint-disable-next-line
@pre<BaseEntity>("save", function(next) {
@pre<any>("save", function(next) {
try {
if (iocContainer.isBound(TYPES.DecodedJwt)) {
const currentUser = iocContainer.get<DecodedJwt>(TYPES.DecodedJwt);
this.setCreatedBy(currentUser.userId);
(this as Writable<
BaseEntity
>).createdBy = currentUser.userId as any;
if (this.type === "User" && !this.tenant)
this.setTenant(currentUser.tenantId);
} else if (this.type === "User") {
this.setCreatedBy(this);
(this as Writable<BaseEntity>).createdBy = this;
}
next();
} catch (error) {
Expand Down Expand Up @@ -91,9 +94,4 @@ export abstract class BaseEntity extends Typegoose {
activate(): void {
(this as Writable<BaseEntity>).isActive = true;
}

@instanceMethod
private setCreatedBy(user: any) {
(this as Writable<BaseEntity>).createdBy = user;
}
}
18 changes: 10 additions & 8 deletions src/domain/model/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ export class User extends BaseEntity implements IMustHaveTenant {
tenantId?: string;
}) => {
const id = tenantId || iocContainer.get<any>(TYPES.TenantId);

if (!id) throw new Error("Tenant Id is required");

return new User({
Expand Down Expand Up @@ -162,15 +161,18 @@ export class User extends BaseEntity implements IMustHaveTenant {
setPassword(password: string) {
(this as Writable<User>).password = password;
}

@instanceMethod
setTenant(tenant: any) {
(this as Writable<this>).tenant = tenant;
}
@instanceMethod
update(user: Partial<this>): void {
if (this.firstName) this.setFirstName(user.firstName as string);
if (this.lastName) this.setLastName(user.lastName as string);
if (this.password) this.setPassword(user.password as string);
if (this.username) this.setUsername(user.username as string);
if (this.email) this.setEmail(user.email as string);
if (this.role) this.setRole(user.role as UserRole);
if (user.firstName) this.setFirstName(user.firstName as string);
if (user.lastName) this.setLastName(user.lastName as string);
if (user.password) this.setPassword(user.password as string);
if (user.username) this.setUsername(user.username as string);
if (user.email) this.setEmail(user.email as string);
if (user.role) this.setRole(user.role as UserRole);
}

@instanceMethod
Expand Down
73 changes: 59 additions & 14 deletions src/infrastructure/db/repositories/base_repository.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { plainToClassFromExist } from "class-transformer";
import { unmanaged } from "inversify";
import { Document, Model } from "mongoose";
import { TYPES } from "../../../domain/constants/types";
import {
IBaseRepository,
Query
} from "../../../domain/interfaces/repositories";
import { BaseEntity } from "../../../domain/model/base";
import { provideSingleton } from "../../config/ioc";
import { iocContainer, provideSingleton } from "../../config/ioc";

@provideSingleton(BaseRepository)
export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>
implements IBaseRepository<TEntity> {
protected Model: Model<TModel>;

protected _constructor: () => TEntity;

public constructor(
@unmanaged() model: Model<TModel>,
@unmanaged() constructor: () => TEntity
Expand All @@ -24,7 +26,13 @@ export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>

public async findAll() {
return new Promise<TEntity[]>((resolve, reject) => {
this.Model.find({ isDeleted: { $ne: true } }, (err, res) => {
const query = JSON.parse(
JSON.stringify({
isDeleted: { $ne: true },
tenant: this.getCurrentTenant()
})
);
this.Model.find(query, "-__v", (err, res) => {
if (err) return reject(err);
const results = res.map(r => this.readMapper(r));
return resolve(results);
Expand All @@ -33,13 +41,19 @@ export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>
}

public async findById(id: string) {
const query = JSON.parse(
JSON.stringify({
_id: id,
isDeleted: { $ne: true },
tenant: this.getCurrentTenant()
})
);

return new Promise<TEntity>((resolve, reject) => {
this.Model.findById(id, "-__v", (err, res) => {
this.Model.findOne(query, "-__v", (err, res) => {
if (err) return reject(err);
if (!res) return resolve();

const result = this.readMapper(res);
resolve(result.isDeleted ? undefined : result);
resolve(this.readMapper(res));
});
});
}
Expand All @@ -58,8 +72,15 @@ export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>
public async insertOrUpdate(doc: TEntity): Promise<TEntity> {
return new Promise<TEntity>((resolve, reject) => {
if (doc.id) {
const query = JSON.parse(
JSON.stringify({
_id: doc.id,
isDeleted: { $ne: true },
tenant: this.getCurrentTenant()
})
);
this.Model.findByIdAndUpdate(
{ _id: doc.id },
query,
doc,
{ new: true },
(err, res) => {
Expand All @@ -81,35 +102,52 @@ export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>
}

public findManyById(ids: string[]) {
const query = JSON.parse(
JSON.stringify({
_id: { $in: ids },
isDeleted: { $ne: true },
tenant: this.getCurrentTenant()
})
);
return new Promise<TEntity[]>((resolve, reject) => {
const query = { _id: { $in: ids } };
this.Model.find(query, (err, res) => {
if (err) return reject(err);
let results = res.map(r => this.readMapper(r));
results = results.filter(r => !r.isDeleted);
const results = res.map(r => this.readMapper(r));
resolve(results);
});
});
}

public findManyByQuery(query: Query<TEntity>) {
query = JSON.parse(
JSON.stringify({
...query,
isDeleted: { $ne: true },
tenant: this.getCurrentTenant()
})
);
return new Promise<TEntity[]>((resolve, reject) => {
this.Model.find(query, "-__v", (err, res) => {
if (err) return reject(err);
if (!res) return resolve();
let result = res.map(r => this.readMapper(r));
result = result.filter(r => !r.isDeleted);
const result = res.map(r => this.readMapper(r));
resolve(result);
});
});
}
public async findOneByQuery(query: Query<TEntity>) {
query = JSON.parse(
JSON.stringify({
...query,
isDeleted: { $ne: true },
tenant: this.getCurrentTenant()
})
);
return new Promise<TEntity>((resolve, reject) => {
this.Model.findOne(query, "-__v", (err, res) => {
if (err) return reject(err);
if (!res) return resolve();
const result = this.readMapper(res);
resolve(result.isDeleted ? undefined : result);
resolve(this.readMapper(res));
});
});
}
Expand Down Expand Up @@ -148,6 +186,7 @@ export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>
// throw new Error("Method not implemented.");
// }

// #region Helper methods
/**
* Maps '_id' from mongodb to 'id' of TEntity
*
Expand All @@ -167,4 +206,10 @@ export class BaseRepository<TEntity extends BaseEntity, TModel extends Document>
delete obj._id;
return plainToClassFromExist(entity, obj);
}
private getCurrentTenant() {
return this._constructor().type !== "Tenant"
? iocContainer.get<any>(TYPES.TenantId)
: undefined;
}
// #endregion
}
15 changes: 13 additions & 2 deletions src/ui/api/controllers/base_controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import httpStatus from "http-status-codes";
import { validate, ValidationError } from "class-validator";
import httpStatus from "http-status-codes";
import { Controller } from "tsoa";
import { BaseCreateEntityDto } from "../../models/base_dto";
import { isIdValid } from "../../../infrastructure/utils/server_utils";
import { HttpError } from "../../error";
import { BaseCreateEntityDto } from "../../models/base_dto";

export abstract class BaseController extends Controller {
protected async checkBadRequest(input: BaseCreateEntityDto) {
Expand All @@ -22,4 +23,14 @@ export abstract class BaseController extends Controller {
throw new HttpError(httpStatus.CONFLICT);
}
}
protected async checkUUID(id: any) {
if (!isIdValid(id)) {
this.setStatus(httpStatus.BAD_REQUEST);

throw new HttpError(
httpStatus.BAD_REQUEST,
`ID "${id}" is invalid`
);
}
}
}
7 changes: 4 additions & 3 deletions src/ui/api/controllers/tenant_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { provideSingleton } from "../../../infrastructure/config/ioc";
import { isIdValid } from "../../../infrastructure/utils/server_utils";
import { HttpError } from "../../error";
import { ITenantService } from "../../interfaces/tenant_service";
import { CreateTenantInput } from "../../models/tenant_dto";
import { CreateTenantInput, TenantDto } from "../../models/tenant_dto";
import { TenantService } from "../../services/tenant_service";
import { BaseController } from "./base_controller";

Expand All @@ -18,9 +18,10 @@ export class TenantController extends BaseController {
@inject(TenantService) private readonly _tenantService: ITenantService;

@Get("{tenantName}")
public async get(tenantName: string) {
public async get(tenantName: string): Promise<TenantDto> {
const tenant = await this._tenantService.get(tenantName);
return tenant || this.setStatus(httpStatus.NOT_FOUND);
if (!tenant) throw new HttpError(httpStatus.NOT_FOUND);
return tenant;
}
@Post()
@Security("X-Auth-Token", ["admin"])
Expand Down
46 changes: 43 additions & 3 deletions src/ui/api/controllers/user_controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Body, Post, Route, Security, Tags } from "tsoa";
import { plainToClass } from "class-transformer";
import httpStatus from "http-status-codes";
import { Body, Delete, Get, Post, Put, Route, Security, Tags } from "tsoa";
import { inject, provideSingleton } from "../../../infrastructure/config/ioc";
import { HttpError } from "../../error";
import { IUserService } from "../../interfaces/user_service";
import { UserDto, UserSignUpInput } from "../../models/user_dto";
import {
UserDto,
UserSignUpInput,
UserUpdateInput
} from "../../models/user_dto";
import { UserService } from "../../services/user_service";
import { BaseController } from "./base_controller";

Expand All @@ -13,7 +20,40 @@ export class UserController extends BaseController {

@Post()
@Security("X-Auth-Token", ["admin"])
async create(@Body() input: UserSignUpInput): Promise<UserDto> {
public async create(@Body() input: UserSignUpInput): Promise<UserDto> {
await this.checkBadRequest(plainToClass(UserSignUpInput, input));
return this._userService.create(input);
}
@Put("{id}")
@Security("X-Auth-Token", ["admin"])
public async update(
id: string,
@Body() input: UserUpdateInput
): Promise<void> {
this.checkUUID(id);
if (!input) return this.setStatus(httpStatus.NO_CONTENT);
await this.checkBadRequest(plainToClass(UserUpdateInput, input));
input = JSON.parse(JSON.stringify(input));
await this._userService.update({ ...input, id });
}
@Get("{id}")
@Security("X-Auth-Token", ["admin"])
public async get(id: string): Promise<UserDto> {
this.checkUUID(id);
const userDto = await this._userService.get(id);
if (userDto) return userDto;
throw new HttpError(httpStatus.NOT_FOUND);
}
@Get()
@Security("X-Auth-Token", ["admin"])
public async getAll(): Promise<UserDto[]> {
return this._userService.getAll();
}
@Delete("{id}")
@Security("X-Auth-Token", ["admin"])
public async delete(id: string) {
this.checkUUID(id);
const isDeleted = await this._userService.delete(id);
if (!isDeleted) this.setStatus(httpStatus.NOT_FOUND);
}
}
Loading

0 comments on commit 7e89d42

Please sign in to comment.