Nestjs + CASL + Prisma integration issue

119 views Asked by At

This seems to be working nice if i switch to TypeORM schemas, but all my schemas were modelled using Prisma so i cant switch, and all the docs on the net apperantly tells only how to implement CASL + TypeORM or other ORM tools, but im on Prisma...All i want to do is implementing update functionality in order for user to update only his own data using Prisma + CASL on my Nestjs app, tried to assign string types on the corresponding parts below but no luck. This should normally work since user.sub and the 3rd parameter({id: userPayload.sub}) in can method of ability factory, i really dont get whats wrong, maybe im just missing something, i would be really appreciated if you experienced guys can point out where im mistaken, thanks in advance

defined in user.controller.ts

const isAllowed = ability.can(Actions.Update, userSubject, {id: user.sub });

defined in casl-ability.factory.ts

can([Actions.Read, Actions.Update, Actions.Delete], 'User', {id: userPayload.sub});

error message: Argument of type '{ id: string; }' is not assignable to parameter of type 'string'.

user.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  Req,
  UseGuards,
  ForbiddenException,
} from '@nestjs/common';
import { UserService } from './user.service';
import { Prisma } from '@prisma/client';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { CaslAbilityFactory } from 'src/casl/casl-ability.factory/casl-ability.factory';
import { Actions } from 'src/casl/casl-ability.factory/casl-ability.factory';
import type { AppSubjects } from 'src/casl/casl-ability.factory/casl-ability.factory';
import { User } from '@prisma/client';
import { ForbiddenError } from '@casl/ability';
import { UserPayload } from 'src/casl/casl-ability.factory/casl-ability.factory';

@Controller('user')
export class UserController {
  constructor(
    private readonly userService: UserService,
    private readonly caslAbilityFactory: CaslAbilityFactory,
  ) {}

  @UseGuards(JwtAuthGuard)
  @Patch('/update/:id')
  async updateUser(
    @Body() updateUserDto: Prisma.UserUpdateInput,
    @Req() req: any,
    @Param('id') id: string,
  ) {
    const userSubject: AppSubjects = 'User';
    const user: UserPayload = req.user;
    console.log('user', JSON.stringify(user));

    const ability = this.caslAbilityFactory.createForUser(user);
    const isAllowed = ability.can(Actions.Update, userSubject, {id:  user.sub });
    if (!isAllowed) {
      throw new ForbiddenException(
        'You are not allowed to update a user, please contact the admin for more information.',
      );
    }
    
    return await this.userService.update(id, updateUserDto);
  }
}

casl-ability.factory.ts

import { Injectable } from '@nestjs/common';
import { createPrismaAbility, PrismaQuery, Subjects } from '@casl/prisma';

import {
  PureAbility,
  AbilityBuilder,
  subject,
  ExtractSubjectType,
} from '@casl/ability';
import { User, Prisma, Product, Category, Role } from '@prisma/client';
import { InferSubjects } from '@casl/ability';

export enum Actions {
  Manage = 'manage',
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

export type AppSubjects =
  | 'all'
  | Subjects<{
      User: User;
      Product: Product;
      Category: Category;
    }>;

export type UserPayload = {
  email: string;
  sub: string;
  iat: number;
  exp: number;
  role: Role;
};

type AppAbility = PureAbility<[string, AppSubjects], PrismaQuery>;

@Injectable()
export class CaslAbilityFactory {
  createForUser(userPayload: UserPayload) {
    const { can, cannot, build } = new AbilityBuilder<AppAbility>(
      createPrismaAbility,
    );

    if (userPayload.role === Role.ADMIN) {
      can(Actions.Manage, 'all');
    } else {
      can([Actions.Read, Actions.Update, Actions.Delete], 'User', {
        id: userPayload.sub,
      }); 
      can(Actions.Read, 'Category');
      can(Actions.Read, 'Product');
    }

    return build({
      detectSubjectType: (item) =>
        item.constructor as ExtractSubjectType<InferSubjects<AppAbility>>,
    });
  }
}

ensured both sides are of string type and match, user should be able to update his own personal data when the id in user.req, which is extracted from decoded token, and the id of post he intended to update match

0

There are 0 answers