glinda93
glinda93

Reputation: 8459

How to apply a decorator to all class methods using class decorator

I'm using experimental typescript decorators to manage access control in express.

class AccountController extends Controller {
  
  login(req: Request, res: Response) {
    const { email, password } = req.body;
    const token = await this.model.login(email, password);
    return res.json({
      token
    });
  }

  @hasRole('ADMIN')
  list(req: Request, res: Response) {
    res.json({
      data: await this.model.findAll()
    });
  }
}

hasRole method decorator is working fine and I'm happy with it.

The Controller implements REST methods:

class Controller {
  list(req: Request, res: Response) { // impl }
  get(req: Request, res: Response) { // impl } 
  create(req: Request, res: Response) { // impl }
  update(req: Request, res: Response) { // impl }
  delete(req: Request, res: Response) { // impl }
}

The problem is, I have to apply the same decorators to most of other controllers and I find it very repetitive. For example, StockController should allow access to only MERCHANT role, and I have to do something like the following:

class StockController extends Controller {
  @hasRole('MERCHANT')
  list(req: Request, res: Response) { 
    return super.list(req, res);
  }
  @hasRole('MERCHANT')
  get(req: Request, res: Response) { 
    return super.get(req, res);
  } 
  @hasRole('MERCHANT')
  create(req: Request, res: Response) { 
    return super.create(req, res);
  }
  @hasRole('MERCHANT')
  update(req: Request, res: Response) { 
    return super.update(req, res);
  }
  @hasRole('MERCHANT')
  delete(req: Request, res: Response) { 
    return super.delete(req, res);
  }
}

This approach is not only dull and repetitive but also unsafe, because if I add a method to Controller and accidentally forget to add the method to child controllers, they will allow unwanted access.

I'd like to deal this problem with class decorator and use something like the following:

@requireRole('MERCHANT')
class StockController extends Controller {}

However, from what I see in the doc:

The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition.

As far as I understand it, I cannot implement "method hook" in class decorators. Any suggestions?

For your information, hasRole decorator looks like the following:

export function hasRole(role: string) {
  return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(req: Request, res: Response) {
      const user = res.locals.user;
      if (user && user.hasRole(role)) {
        originalMethod.apply(this, [req, res]);
      } else {
        res.status(403).json({});
      }
    }
  }
}

Upvotes: 5

Views: 4175

Answers (1)

noitse
noitse

Reputation: 1045

This is possible by overriding class methods

function AttachToAllClassDecorator<T>(someParam: string) {
    return function(target: new (...params: any[]) => T) {
        for (const key of Object.getOwnPropertyNames(target.prototype)) {
            // maybe blacklist methods here
            let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);
            if (descriptor) {
                descriptor = someDecorator(someParam)(key, descriptor);
                Object.defineProperty(target.prototype, key, descriptor);
            }
        }
    }
}

Basically going through all methods (maybe add some logic around it for whitelisting/blacklisting some methods) and overriding with new method that has method decorator wrapped.

Here is basic example for method decorator.

function someDecorator(someParam: string): (methodName: string, descriptor: PropertyDescriptor) => PropertyDescriptor {
    return (methodName: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
        let method = descriptor.value;

        descriptor.value = function(...args: any[]) {
            console.warn(`Here for descriptor ${methodName} with param ${someParam}`);

            return method.apply(this, args);
        }

        return descriptor;
    }
}

TS Playground

Upvotes: 7

Related Questions