From 305000e803671da5a272950a8d095b2128ad8521 Mon Sep 17 00:00:00 2001 From: Gaston Morixe Date: Sun, 4 Apr 2021 16:09:37 -0300 Subject: [PATCH] BaseService with generic auth --- src/app.module.ts | 10 ++-- src/casl/casl-ability.factory.ts | 4 +- src/entities/school/school.resolver.ts | 20 +------ src/entities/school/school.service.ts | 72 +++++++++++--------------- src/entities/user/user.model.ts | 2 +- src/entities/user/user.module.ts | 3 +- src/utils/BaseService.ts | 41 +++++++++++++++ 7 files changed, 80 insertions(+), 72 deletions(-) create mode 100644 src/utils/BaseService.ts diff --git a/src/app.module.ts b/src/app.module.ts index eb494f0..45415bb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,7 +7,7 @@ import gitCommitInfo from 'git-commit-info' import * as Utils from './utils' import { APP_GUARD } from '@nestjs/core' import { GqlAuthGuard } from './auth/gql-auth.guard' -import { PoliciesGuard } from './casl/policy.guard' +// import { PoliciesGuard } from './casl/policy.guard' // import { // ApolloErrorConverter, // required: core export @@ -94,10 +94,10 @@ import { CaslModule } from './casl/casl.module' provide: APP_GUARD, useClass: GqlAuthGuard, }, - { - provide: APP_GUARD, - useClass: PoliciesGuard, - }, + // { + // provide: APP_GUARD, + // useClass: PoliciesGuard, + // }, // ObjectidScalar, // Logger, // JwtStrategy, diff --git a/src/casl/casl-ability.factory.ts b/src/casl/casl-ability.factory.ts index b77d0f0..1794659 100644 --- a/src/casl/casl-ability.factory.ts +++ b/src/casl/casl-ability.factory.ts @@ -1,4 +1,3 @@ -import { Injectable } from '@nestjs/common' import { // ForbiddenError, InferSubjects, @@ -7,7 +6,6 @@ import { AbilityClass, ExtractSubjectType, } from '@casl/ability' -import { plainToClass } from 'class-transformer' import { User } from '../entities/user/user.model' import { School } from '../entities/school/school.model' @@ -32,7 +30,6 @@ export enum Action { export type AppAbility = Ability<[Action, Subjects]> -// @Injectable() export class CaslAbilityFactory { static createForUser(user: User | undefined) { const { can, cannot, build } = new AbilityBuilder< @@ -60,6 +57,7 @@ export class CaslAbilityFactory { // School abilities can(Action.Create, School) can(Action.List, School) + // can(Action.Read, School, { createdBy: user._id }) can(Action.Read, School, { createdBy: user._id }) can(Action.Update, School, { createdBy: user._id }) can(Action.Delete, School, { createdBy: user._id }) diff --git a/src/entities/school/school.resolver.ts b/src/entities/school/school.resolver.ts index b7d3e52..5f43b16 100644 --- a/src/entities/school/school.resolver.ts +++ b/src/entities/school/school.resolver.ts @@ -8,14 +8,6 @@ import { ID, } from '@nestjs/graphql' import { Types } from 'mongoose' -import { plainToClass } from 'class-transformer' - -// Auth -import { ForbiddenError } from '@casl/ability' -import { CheckPolicies } from '../../casl/policy.guard' -import { Action } from '../../casl/casl-ability.factory' -import { CaslAbilityFactory } from '../../casl/casl-ability.factory' -import { CurrentUser } from '../../auth/currentUser' // School import { School, SchoolDocument } from './school.model' @@ -37,39 +29,29 @@ export class SchoolResolver { constructor(private service: SchoolService) {} @Query(() => School) - @CheckPolicies((a) => true) async school(@Args('_id', { type: () => ID }) _id: Types.ObjectId) { return this.service.getById(_id) } @Query(() => [School]) - @CheckPolicies((a) => true) async schools( @Args('filters', { nullable: true }) filters?: ListSchoolInput, ) { - const schoolList = await this.service.list(filters) - for (const school of schoolList) { - await this.service.checkPermissons(Action.Read, school._id) - } - return schoolList + return this.service.list(filters) } @Mutation(() => School) - @CheckPolicies((a) => true) async createSchool(@Args('payload') payload: CreateSchoolInput) { return this.service.create(payload) } @Mutation(() => School) - @CheckPolicies((a) => true) async updateSchool(@Args('payload') payload: UpdateSchoolInput) { return this.service.update(payload) } @Mutation(() => School) - @CheckPolicies((a) => true) async deleteSchool(@Args('_id', { type: () => ID }) _id: Types.ObjectId) { - //await this.service.checkPermissons(Action.Delete, _id) return this.service.delete(_id) } diff --git a/src/entities/school/school.service.ts b/src/entities/school/school.service.ts index b302ef1..d3ece4c 100644 --- a/src/entities/school/school.service.ts +++ b/src/entities/school/school.service.ts @@ -1,19 +1,14 @@ import { Injectable, Inject } from '@nestjs/common' -import { Args, ID } from '@nestjs/graphql' -import { REQUEST } from '@nestjs/core' +import { CONTEXT } from '@nestjs/graphql' import { InjectModel } from '@nestjs/mongoose' import { Model, Types } from 'mongoose' -import { plainToClass } from 'class-transformer' + +// Service +import { BaseService } from '../../utils/BaseService' // Auth -import { ForbiddenError } from '@casl/ability' -import { CaslAbilityFactory } from '../../casl/casl-ability.factory' -import { CheckPolicies } from '../../casl/policy.guard' import { Action } from '../../casl/casl-ability.factory' -// User -import { User } from '../user/user.model' - // School import { School, SchoolDocument } from './school.model' import { @@ -22,36 +17,32 @@ import { UpdateSchoolInput, } from './school.inputs' +const MODEL_CLASS = School +type TModelDocType = SchoolDocument + @Injectable() -export class SchoolService { - constructor( - @InjectModel(School.name) - private model: Model, - @Inject(REQUEST) private request: any, - ) {} +export class SchoolService extends BaseService { + modelClass = MODEL_CLASS + dbModel: Model + context: any - async checkPermissons(action: Action, _id?: Types.ObjectId) { - // Checks permissons for a single record - const currentUser = this.request.req?.user - const ability = CaslAbilityFactory.createForUser(currentUser) - if (_id) { - const doc = await this.model.findById(_id).exec() - const model = plainToClass(School, doc?.toObject()) - ForbiddenError.from(ability).throwUnlessCan(action, model) - return doc - } else { - ForbiddenError.from(ability).throwUnlessCan(action, School) - return true // Necessary? - } + constructor( + @InjectModel(MODEL_CLASS.name) + dbModel: Model, + @Inject(CONTEXT) context: any, + ) { + super() + this.dbModel = dbModel + this.context = context } async create(payload: CreateSchoolInput) { await this.checkPermissons(Action.Create) const updatedPayload = { - createdBy: this.request.req.user, + createdBy: this.currentUser, ...payload, } - const model = new this.model(updatedPayload) + const model = new this.dbModel(updatedPayload) return model.save() } @@ -59,28 +50,23 @@ export class SchoolService { return this.checkPermissons(Action.Read, _id) } - list(filters: ListSchoolInput) { - return this.model.find({ ...filters }).exec() + async list(filters: ListSchoolInput) { + const docs = await this.dbModel.find({ ...filters }).exec() + for (const doc of docs) { + await this.checkPermissons(Action.Read, doc._id) + } + return docs } async update(payload: UpdateSchoolInput) { await this.checkPermissons(Action.Update, payload._id) - return this.model + return this.dbModel .findByIdAndUpdate(payload._id, payload, { new: true }) .exec() } async delete(_id: Types.ObjectId) { await this.checkPermissons(Action.Delete, _id) - let model: any // Model - - try { - model = await this.model.findByIdAndDelete(_id).exec() - } catch (error) { - console.error(error) - return - } - - return model + return this.dbModel.findByIdAndDelete(_id).exec() } } diff --git a/src/entities/user/user.model.ts b/src/entities/user/user.model.ts index 44e82b0..c039f0b 100644 --- a/src/entities/user/user.model.ts +++ b/src/entities/user/user.model.ts @@ -38,7 +38,7 @@ export class User extends Utils.BaseModel { passwordHash: string @GQL.Field(() => String, { nullable: true }) - @DB.Prop({ required: false, min: 3, max: 60, unique: false }) // unique: true, + @DB.Prop({ required: false, min: 3, max: 60 }) // unique: true, mobile: string @GQL.Field(() => [String], { nullable: true }) diff --git a/src/entities/user/user.module.ts b/src/entities/user/user.module.ts index 2143032..b530971 100644 --- a/src/entities/user/user.module.ts +++ b/src/entities/user/user.module.ts @@ -8,8 +8,9 @@ import { User, UserSchema } from './user.model' import { UserResolver } from './user.resolver' // import { AuthModule } from '../auth/auth.module' -import { PoliciesGuard } from '../../casl/policy.guard' import { CaslAbilityFactory } from '../../casl/casl-ability.factory' +// import { PoliciesGuard } from '../../casl/policy.guard' + @Module({ imports: [ // AuthModule, diff --git a/src/utils/BaseService.ts b/src/utils/BaseService.ts new file mode 100644 index 0000000..511f97d --- /dev/null +++ b/src/utils/BaseService.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common' +import { plainToClass } from 'class-transformer' +import mongoose from 'mongoose' + +// Auth +import { ForbiddenError } from '@casl/ability' +import { Action } from '../casl/casl-ability.factory' +import { CaslAbilityFactory } from '../casl/casl-ability.factory' + +// User +import { User } from '../entities/user/user.model' + +@Injectable() +export abstract class BaseService { + abstract dbModel: any // mongoose.Model + abstract modelClass: any //{ new (): any } + abstract context: any //{ new (): any } + + get currentUser(): User | undefined { + return this.context.req?.user + } + + async checkPermissons( + action: Action, + id?: mongoose.Types.ObjectId, + ): Promise> { + // Checks permissons for a single record + const ability = CaslAbilityFactory.createForUser(this.currentUser) + if (id) { + const doc = await this.dbModel.findById(id).exec() + const model = plainToClass(this.modelClass, doc?.toObject()) as any + ForbiddenError.from(ability).throwUnlessCan(action, model) + return doc + } else { + ForbiddenError.from(ability).throwUnlessCan( + action, + this.modelClass as any, + ) + } + } +}