Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented HttpContext support #72

Merged
merged 6 commits into from
Nov 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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