Skip to content

Commit

Permalink
BaseService with generic auth (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
gastonmorixe authored Apr 4, 2021
1 parent 7b00eed commit 519f97f
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 72 deletions.
10 changes: 5 additions & 5 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 0 additions & 3 deletions src/casl/casl-ability.factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Injectable } from '@nestjs/common'
import {
// ForbiddenError,
InferSubjects,
Expand All @@ -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'
Expand All @@ -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<
Expand Down
20 changes: 1 addition & 19 deletions src/entities/school/school.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
}

Expand Down
72 changes: 29 additions & 43 deletions src/entities/school/school.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,65 +17,56 @@ import {
UpdateSchoolInput,
} from './school.inputs'

const MODEL_CLASS = School
type TModelDocType = SchoolDocument

@Injectable()
export class SchoolService {
constructor(
@InjectModel(School.name)
private model: Model<SchoolDocument>,
@Inject(REQUEST) private request: any,
) {}
export class SchoolService extends BaseService {
modelClass = MODEL_CLASS
dbModel: Model<TModelDocType>
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<TModelDocType>,
@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()
}

getById(_id: Types.ObjectId) {
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<SchoolDocument>

try {
model = await this.model.findByIdAndDelete(_id).exec()
} catch (error) {
console.error(error)
return
}

return model
return this.dbModel.findByIdAndDelete(_id).exec()
}
}
2 changes: 1 addition & 1 deletion src/entities/user/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
3 changes: 2 additions & 1 deletion src/entities/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions src/utils/BaseService.ts
Original file line number Diff line number Diff line change
@@ -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<mongoose.Document>
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<mongoose.Model<mongoose.Document>> {
// 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,
)
}
}
}

0 comments on commit 519f97f

Please sign in to comment.