Skip to content

Commit

Permalink
Implemented HttpContext support (#72)
Browse files Browse the repository at this point in the history
* [WIP] HttpContext & AuthProvider support

* enabled strictNullChecks

* Implemented BaseHttpController support

* Implemented AuthProvider support

* Added default principal

* Implemented AuthProvider and HttpContext
  • Loading branch information
remojansen authored Nov 11, 2017
1 parent 60abd8e commit 355fae5
Show file tree
Hide file tree
Showing 15 changed files with 491 additions and 23 deletions.
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Some utilities for the development of express applications with Inversify.

## Installation

You can install `inversify-express-utils` using npm:

```
Expand All @@ -27,6 +28,7 @@ Please refer to the [InversifyJS documentation](https://github.com/inversify/Inv
## The Basics

### Step 1: Decorate your controllers

To use a class as a "controller" for your express app, simply add the `@controller` decorator to the class. Similarly, decorate methods of the class to serve as request handlers.
The following example will declare a controller that responds to `GET /foo'.

Expand Down Expand Up @@ -73,6 +75,7 @@ export class FooController implements interfaces.Controller {
```

### Step 2: Configure container and server

Configure the inversify container in your composition root as usual.

Then, pass the container to the InversifyExpressServer constructor. This will allow it to register all controllers and their dependencies from your container and attach them to the express app.
Expand Down Expand Up @@ -109,9 +112,11 @@ app.listen(3000);
```

## InversifyExpressServer

A wrapper for an express Application.

### `.setConfig(configFn)`

Optional - exposes the express application object for convenient loading of server-level middleware.

```ts
Expand All @@ -126,6 +131,7 @@ server.setConfig((app) => {
```

### `.setErrorConfig(errorConfigFn)`

Optional - like `.setConfig()`, except this function is applied after registering all app middleware and controller routes.

```ts
Expand All @@ -139,6 +145,7 @@ server.setErrorConfig((app) => {
```

### `.build()`

Attaches all registered controllers and middleware to the express application. Returns the application instance.

```ts
Expand All @@ -152,6 +159,7 @@ server
```

## Using a custom Router

It is possible to pass a custom `Router` instance to `InversifyExpressServer`:

```ts
Expand All @@ -177,6 +185,7 @@ let server = new InversifyExpressServer(container, null, { rootPath: "/api/v1" }
```

## Using a custom express application

It is possible to pass a custom `express.Application` instance to `InversifyExpressServer`:

```ts
Expand All @@ -203,30 +212,177 @@ Registers the decorated controller method as a request handler for a particular
Shortcut decorators which are simply wrappers for `@httpMethod`. Right now these include `@httpGet`, `@httpPost`, `@httpPut`, `@httpPatch`, `@httpHead`, `@httpDelete`, and `@All`. For anything more obscure, use `@httpMethod` (Or make a PR :smile:).

### `@request()`

Binds a method parameter to the request object.

### `@response()`

Binds a method parameter to the response object.

### `@requestParam(name?: string)`

Binds a method parameter to request.params object or to a specific parameter if a name is passed.

### `@queryParam(name?: string)`

Binds a method parameter to request.query or to a specific query parameter if a name is passed.

### `@requestBody(name?: string)`

Binds a method parameter to request.body or to a specific body property if a name is passed. If the bodyParser middleware is not used on the express app, this will bind the method parameter to the express request object.

### `@requestHeaders(name?: string)`

Binds a method parameter to the request headers.

### `@cookies()`

Binds a method parameter to the request cookies.

### `@next()`

Binds a method parameter to the next() function.

## HttpContext

The `HttpContext` property allow us to access the current request,
response and user with ease. `HttpContext` is available as a property
in controllers derived from `BaseHttpController`.

```ts
import { injectable, inject } from "inversify";
import {
controller, httpGet, BaseHttpController
} from "inversify-express-utils";

@injectable()
@controller("/")
class UserPreferencesController extends BaseHttpController {

@inject("AuthService") private readonly _authService: AuthService;

@httpGet("/")
public async get() {
const token = this.httpContext.request.headers["x-auth-token"];
return await this._authService.getUserPreferences(token);
}
}
```

If you are creating a custom controller you will need to inject `HttpContext` manually
using the `@httpContext` decorator:

```ts
import { injectable, inject } from "inversify";
import {
controller, httpGet, BaseHttpController, httpContext, interfaces
} from "inversify-express-utils";

const authService = inject("AuthService")

@injectable()
@controller("/")
class UserPreferencesController {

@httpContext private readonly _httpContext: interfaces.HttpContext;
@authService private readonly _authService: AuthService;

@httpGet("/")
public async get() {
const token = this.httpContext.request.headers["x-auth-token"];
return await this._authService.getUserPreferences(token);
}
}
```

## AuthProvider

The `HttpContext` will not have access to the current user if you don't
create a custom `AuthProvider` implementation:

```ts
const server = new InversifyExpressServer(
container, null, null, null, CustomAuthProvider
);
```

We need to implement the `AuthProvider` interface.

The `AuthProvider` allow us to get an user (`Principal`):

```ts
import { injectable, inject } from "inversify";
import {} from "inversify-express-utils";

const authService = inject("AuthService");

@injectable()
class CustomAuthProvider implements interfaces.AuthProvider {

@authService private readonly _authService: AuthService;

public async getUser(
req: express.Request,
res: express.Response,
next: express.NextFunction
): Promise<interfaces.Principal> {
const token = req.headers["x-auth-token"]
const user = await this._authService.getUser(token);
const principal = new Principal(user);
return principal;
}

}
```

We alsoneed to implement the Principal interface.
The `Principal` interface allow us to:

- Access the details of an user
- Check if it has access to certain resource
- Check if it is authenticated
- Check if it is in an user role

```ts
class Principal implements interfaces.Principal {
public details: any;
public constrcutor(details: any) {
this.details = details;
}
public isAuthenticated(): Promise<boolean> {
return Promise.resolve(true);
}
public isResourceOwner(resourceId: any): Promise<boolean> {
return Promise.resolve(resourceId === 1111);
}
public isInRole(role: string): Promise<boolean> {
return Promise.resolve(role === "admin");
}
}
```

We can then access the current user (Principal) via the `HttpContext`:

```ts
@injectable()
@controller("/")
class UserDetailsController extends BaseHttpController {

@inject("AuthService") private readonly _authService: AuthService;

@httpGet("/")
public async getUserDetails() {
if (this.httpContext.user.isAuthenticated()) {
return this.httpContext.user.details;
} else {
throw new Error();
}
}
}
```

## Examples

Some examples can be found at the [inversify-express-example](https://github.com/inversify/inversify-express-example) repository.

## License
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "inversify-express-utils",
"version": "4.1.0",
"version": "4.2.0",
"description": "Some utilities for the development of express applications with Inversify",
"main": "lib/index.js",
"jsnext:main": "es/index.js",
Expand Down
8 changes: 8 additions & 0 deletions src/base_http_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { httpContext } from "../src/decorators";
import { interfaces } from "../src/interfaces";
import { injectable } from "inversify";

@injectable()
export class BaseHttpController {
@httpContext protected httpContext: interfaces.HttpContext;
}
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const TYPE = {
Controller: Symbol("Controller")
AuthProvider: Symbol("AuthProvider"),
Controller: Symbol("Controller"),
HttpContext: Symbol("HttpContext")
};

const METADATA_KEY = {
Expand Down
4 changes: 4 additions & 0 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as express from "express";
import { interfaces } from "./interfaces";
import { METADATA_KEY, PARAMETER_TYPE } from "./constants";
import { inject } from "inversify";
import { TYPE } from "../src/constants";

export const httpContext = inject(TYPE.HttpContext);

export function controller(path: string, ...middleware: interfaces.Middleware[]) {
return function (target: any) {
Expand Down
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { InversifyExpressServer } from "./server";
import { controller, httpMethod, httpGet, httpPut, httpPost, httpPatch,
httpHead, all, httpDelete, request, response, requestParam, queryParam,
requestBody, requestHeaders, cookies, next } from "./decorators";
requestBody, requestHeaders, cookies, next, httpContext } from "./decorators";
import { TYPE } from "./constants";
import { interfaces } from "./interfaces";
import { BaseHttpController } from "./base_http_controller";

export {
interfaces,
Expand All @@ -25,5 +26,7 @@ export {
requestBody,
requestHeaders,
cookies,
next
next,
BaseHttpController,
httpContext
};
23 changes: 23 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ namespace interfaces {
rootPath: string;
}

export interface Principal {
details: any;
isAuthenticated(): Promise<boolean>;
// Allows content-based auth
isResourceOwner(resourceId: any): Promise<boolean>;
// Allows role-based auth
isInRole(role: string): Promise<boolean>;
}

export interface AuthProvider {
getUser(
req: express.Request,
res: express.Response,
next: express.NextFunction
): Promise<Principal>;
}

export interface HttpContext {
request: express.Request;
response: express.Response;
user: Principal;
}

}

export { interfaces };
Loading

0 comments on commit 355fae5

Please sign in to comment.