NickBolles / nestjs-accountsjs

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Full user management with Accounts.JS and Nest.js in minutes.

Basic Usage

npm i @nb/nestjs-accountsjs

note: currently I only have this on my personal NPM registry. Make an issue to remind me to make it public if you can't install it with npm i @nb/nestjs-accountsjs --registry npm.nickbolles.com

Basic Config

app.module.ts

import { Module } from "@nestjs/common";
import { Mongo } from  "@accounts/mongo";
import { AccountsPassword } from "@accounts/password";
import { AccountsJsModule } from "@nb/nestjs-accountsjs";

@Module({
  imports: [
    AccountsJsModule.register({
        serverOptions: { // Options passed to the AccountsServer instance
          db: new Mongo(),
          tokenSecret: "secret",
        },
        services: { // Services passed as the second parameter to the AccountsServer Instance
          password: new AccountsPassword(),
        },
        REST: true, // or an Object with any @accounts/rest options
        GraphQL: true // or an Object with any @accounts/graphql-api options
    }),
  ],
})
export class AppModule {}

With Accounts Server Instance

Alternatively you can pass the accountsjs server that you want to use to register:

AccountsJsModule.register({useServer: accountsServerInstance})

With Options Factory Class

app.module.ts

import { Module } from "@nestjs/common";
import { Mongo } from  "@accounts/mongo";
import { AccountsPassword } from "@accounts/password";
import { AccountsJsModule } from "@nb/nestjs-accountsjs";
import { AppAccountsOptionsFactory } from "./AppAccountsOptionsFactory"

@Module({
  imports: [
    AccountsJsModule.registerAsync( { useClass: AppAccountsOptionsFactory })
  ]
})
export class AppModule {}

AppAccountsOptionsFactory.ts

class AppAccountsOptionsFactory implements AccountsOptionsFactory {
  constructor(@Inject(ConfigService) private readonly configService: ConfigService){}

  createAccountsOptions(): NestAccountsOptionsResult {
    return {
        serverOptions: {
          db: new Mongo(),
          tokenSecret: this.configService.get("secret"),
        },
        services: {
          password: new AccountsPassword(),
        },
        REST: true, // or an Object with any @accounts/rest options
        GraphQL: true // or an Object with any @accounts/graphql-api options
    }
  }
}

Register can take any custom provider format. IMHO, the useClass pattern, and breaking the options factory class into it's own file is the most clean format.

Examples

Built in support for REST and GraphQL

REST

Passing REST: true, or a config object will enable and mount the @accounts/rest-express package.

Examples

Config

true for defaults, or an object with the following keys

Key Default Description
path /accounts The path to mount on
relative true Is the path Relative to the nest route, passing an absolute path is the same as making this false
...AccountsExpressOptions any other AccountsExpress options

REST Path

By default it mounts at the MODULE_PATH, which is the same as what nest-router configures. If it's not configured it defaults to /accounts.

Config Nest router module config Path Examples
{REST: true} none /accounts /accounts/user, /accounts/:service/authenticate
{REST: true} /auth /auth /auth/user, /auth/:service/authenticate
{REST: {path: "myPath"}} /auth /auth/myPath /auth/myPath/user, /auth/myPath/:service/authenticate
{REST: {path: "/myPath"}} /auth /myPath /myPath/user, /myPath/:service/authenticate

The path is passed into resolve for example: resolve("/auth", "myPath") -> /auth/myPath, or resolve("/auth","/myPath") -> /myPath

Relative urls

By default the path is relative to the NEST path as in the second to last example above. You can override this by setting the relative option to false. Really this is equal to passing an absolute path as in the last example above

Config Nest router module config Path Examples
{REST: {relative: false}} none /accounts /accounts/user, /accounts/:service/authenticate
{REST: {relative: false}} /auth /accounts /accounts/user, /accounts/:service/authenticate
{REST: {path: "myPath", relative: false}} /auth /myPath /myPath/user, /myPath/:service/authenticate
{REST: {path: "/myPath", relative: false}} /auth /myPath /myPath/user, /myPath/:service/authenticate

GraphQL

The module will configure @accounts/graphql-api and export it as the ACCOUNTS_JS_GRAPHQL provider. This make it easy to use it with @nestjs/graphql

Examples

Config

true to use defaults, or an object of AccountsModuleConfig

Mounting

app.module.ts

import { Module, Inject } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql"
import { AccountsModule } from "@accounts/graphql-api";
import { AccountsJsModule, ACCOUNTS_JS_GRAPHQL, AccountsOptionsFactory, NestAccountsOptionsResult } from "@nb/nestjs-accountsjs";

@Module({
    imports: [
        AccountsJsModule.register({
          accountsOptions: { 
              serverOptions: {
                db: this.userDatabase,
                tokenSecret: "secret",
            },
            services: {
                password: new AccountsPassword(),
            },
            GraphQL: true
           },
        }),
        GraphQLModule.forRootAsync({ 
            inject: [ACCOUNTS_JS_GRAPHQL], // Inject the build GraphQL-Module
            useFactory: (accountsGQLModule: typeof AccountsModule) => {
                return {
                    modules: [accountsGQLModule] // Pass the module to @nestjs/graphql -> Apollo server
                    // or: 
                    // schema: accountsGQLModule.schemaAsync,
                    // context: this.accountsJSGraphQLModule.context
                };
            }
        })
    ]
})
export class AppModule {}

Merging with Code First schemas

Usually you're going to have other GraphQL types and resolvers, merging these with the AccountsGQLModule gets a little bit more tricky. Since Accounts uses GraphQLModules we can utilize their utilities to transform the auto generated schema that nestjs creates.

GraphQLModule.forRootAsync({ 
    inject: [ACCOUNTS_JS_GRAPHQL], // Inject the build GraphQL-Module
    useFactory: (accountsGQLModule: typeof AccountsModule) => {
        const { context } = this.accountsJSGraphQLModule;

        return {
            autoSchemaFile: "schema.gql",
            context,
            // ... any other @nestjs/graphql Options
            transformSchema: async autoGenSchema => { // Intersect the schema and add in AccountsJS GQL Types, resolvers and directives
                return new GraphQLModule({
                    extraSchemas: [autoGenSchema],
                    imports: [this.accountsJSGraphQLModule]
                })
            }
        }
    }
})

Merging with schema first

// todo

Providers

Examples

The module will register several providers for accounts js. This enables these core items for dependency injection in Nest, which can be really powerful. For example you can inject the server into your users service and add an event listener for user created to populate the user with default data. See an example in the with-inject-server-and-opts example

Injector Token Value Type
ACCOUNTS_JS_SERVER AccountsServer Instance AccountsServer
ACCOUNTS_JS_OPTIONS Options for AccountsServer, AccountsServer services, REST and GraphQL NestAccountsOptions
ACCOUNTS_JS_GRAPHQL Accounts JS GraphQLModule AccountsModule from @accounts/graphql-api

Decorators

Examples

Param Decorators

Decorators to match several of the request fields that accounts provides. These are compatible with both HTTP Request handlers and Graphql resolvers and helps to make code more concise and self-documenting

Name Usage Shorthand for
User @User() currentUser: User req.user
UserId @UserID() userId: string req.user.id
AuthToken @AuthToken() authToken?: string multiple, req.headers.Authorization
ClientIP @ClientIP() clientIP: string multiple
UserAgent @UserAgent() userAgent: string multiple

Auth Guard

2 more special decorators exist. The first is @UseGuards(AuthGuard). Auth guard, but default will check for the presence of a user on the Execution context. This can be used at either the class or the method handler level

Class level:

class MyController {
    @Get()
    @UseGuards(AuthGuard)
    mySecret() {
        return "I was a jedi"
    }
}

Method level:

@UseGuards(AuthGuard)
class MyController {
    @Get()
    mySecret() {
        return "I was a jedi"
    }
}

With GraphQL it's exactly the same

@Resolver()
class MyResolver {
    @Query()
    @UseGuards(AuthGuard)
    mySecret() {
        return "I was a jedi"
    }
}

AuthValidator

The second is @AuthValidator, which can be used to customize the AuthGuard behavior. Validators are functions that return a boolean, or a promise that resolves to a boolean. If the result is truthy, the validator succeeds and if all validators succeed the method will be executed.

Validators can be added at the class or the method level, and will stack. So in the example below the @UseGuards(AuthGuard) will run the class validator, IsDarthVader, then it will run TalkingToLuke and AsyncValidator. If any of them fail, the method will not be run.

import { 
    AuthGuard, 
    AuthValidator, 
    AccountsSessionRequest, 
    GQLParam } from "@nb/nestjs-accountsjs"
import { User } from "@accounts/types"

const IsDarthVader = (user: User, params: AccountsSessionRequest | GQLParam, context: ExecutionContext) => user.username === "Darth Vader"

const TalkingToLuke = (user: User, context: ExecutionContext, params: AccountsSessionRequest) => params.body.talkingToLuke)

const AsyncValidator = (user: User) => Promise.resolve(true);

@AuthValidator(IsDarthVader)
class DarthVader {
    @Get()
    @UseGuards(AuthGuard)
    @AuthValidator(TalkingToLuke, AsyncValidator)
    superSecret() {
        return "Luke, I am your father"
    }
}

Making Validators Robust

Note: these are likely to change. There is development in nestjs core 6.7 that adds getType to execution context. Once this is added to the GraphQLExecutionContext we'll update this to pass only the context. If you can, avoid using the third parameter

Above, the TalkingToLuke validator is HTTP specific because it uses the body to the request. We can make this a little more robust by using some of the util methods provided, such as isGQLParam getGQLcontext, getFieldFromDecoratorParams and getFieldFormExecContext.

import { 
    AuthGuard, 
    AuthValidator, 
    AccountsSessionRequest, 
    GQLParam,
    isGQLParam,
    getGQLContext
} from "@nb/nestjs-accountsjs"
import { User } from "@accounts/types"

const IsDarthVader = (user: User) => user.username = "Darth Vader"

const TalkingToLuke = (user: User, context: ExecutionContext, params: AccountsSessionRequest | GQLParam) => isGQLParam(params) ? getGQLContext(params).talkingToLuke : params.body.talkingToLuke;

const AsyncValidator = () => Promise.resolve(true);

@Resolver()
@UseGuards(AuthGuard)
@AuthValidator(IsDarthVader)
class DarthVader {
    @Query()
    @AuthValidator(TalkingToLuke, AsyncValidator)
    superSecret() {
        return "I am your father"
    }
}

Other Decorators

@EnableForService - Guard to only enable if a service exists currently not fully implemented

Interceptor

This module will mount The AccountsSessionInterceptor to initialize the session. This is registered as an APP_INTERCEPTOR, so it will be in effect for the entire app. This is also required for any of the decorators to function correctly.

About


Languages

Language:TypeScript 99.5%Language:JavaScript 0.5%