sequelize-graphql-auth-service

A production-ready GraphQL API authentication/user management sub-graph. Built with Sequelize, Apollo GraphQL, & Express

MIT License

Stars
7

Auth Service

Authentication service/subgraph optimized for Sequelize ORM

Features

  • User Management
  • RBAC
  • Email authentication
  • Social authentication (Google and Facebook)
  • SMS OTP
  • Multi Client
  • i18n
  • Dockerize
  • Analytics
  • CI/CD

Built with

Prerequisite

Run

  • Create a .env file

copy .env.example file as .env

cp .env.example .env

or pull one from dotenv-vault, if your team has one

npx dotenv-vault@latest pull --dotenvMe=YOUR-TEAM-DOTENV_ME
  • Build docker image
yarn docker:build
  • Start container
yarn docker:start -d

Open Shell

yarn docker:cli

Initialize database

  • Create a root user
npx babel-node src/scripts/createRoot
  • Create client

    • You must pass the client_id in their request headers.
npx babel-node src/scripts/createApp
  • List existing clients
npx babel-node src/scripts/listApp
  • Seed database (optional)
yarn seed

Test

  • Create a .env.test file
cp .env.example .env.test
  • Create Test database:
yarn createdb:test
  • Run tests
yarn test

Build

  • Build production image
docker compose -t usmansbk/simple-server:release . -f Dockerfile.production
  • Push to Docker Hub
docker push usmansbk/auth-service:release

Mailer

The server makes use of AWS SES to send emails. Setup your SES account and add the following environment variables. Verify your development email and ensure you have this AWS IAM Policy.

MAIL_FROM=[email protected]

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-east-1

Check the email-templates docs on how to design email templates.

SMS

Follow the official Twilio documentation to setup your Twilio account and add your TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER to your .env file.

Google authentication

  • Create a Firebase project if you don't have one.

  • Go to APIs & Auth > Credentials in the Google Developers Console and copy your OAuth 2.0 Client IDs Web Client GOOGLE_CLIENT_ID and GOOGLE_SECRET_KEY.

  • Generate an OAuth2 API v2 id token from Google 0Auth 2.0 Playground to test.

Facebook authentication

  • Create a new Facebook app
  • Get your FACEBOOK_APP_ACCESS_TOKEN and FACEBOOK_APP_ID env variables
  • Navigate to RolesTest Users to get a test account access tokens

File upload (S3)

We upload files via REST endpoints. Why not File Upload mutation?

To set up your S3 for file storage:

  • Add your AWS_S3_BUCKET to env file
  • Ensure you've set the full s3 permissions

Images CDN

Follow these instructions to get your CLOUDFRONT_API_ENDPOINT. We use Amazon CloudFront to provide a caching layer to reduce the cost of image process and the latency of subsequent image delivery. The CloudFront domain name provides cached access to the image handler API.

Filtering & Pagination

Filtering

For a more complex filtering, we mimic the sequelize filter query. In order to filter by associations, we assume all associations are aliased (using the as option). This alias must have corresponding field in your graphql type. Example:

If you define a User has-many Task relationship like so,

User.hasMany(Task, { as: "tasks" });

you must define a tasks field in your graphql User type schema

type User {
  tasks(filter: TaskFilter): TaskList!
}

Refer to the sequelize docs for more info on Operators

Pagination

Our cursor-based pagination must adhere to a List interface. This is similar to the relay-connection pagination. But unlike relay, we return our items as a flat list.

# Example
type TaskList implements List {
  items: [Task]!
  totalCount: Int!
  pageInfo: PageInfo!
}

N+1 Problem

We eager-load requested fields that have a matching association alias in their corresponding model. Example: If we have a User has-one Picture relationship:

User.hasOne(Picture, { as: "avatar" });
# This will eager-load the `avatar` association. Both user and avatar will be fetched in a single SQL query
query {
  user {
    id
    name
    avatar {
      url
    }
  }
}

Edge-cases:

  • Eager-loading only works with Query. Mutation isn't supported

  • Nested cursor-paginated fields aren't eager-loaded, and hard to maintain in the frontend.

  • Paginated fields should be added to the root Query for the reason above.

Analytics

Segment allows us to collect data with different analytics tools. To setup our analytics, create a Segment account and add your SEGMENT_WRITE_KEY to the .env file.

Error handling

We use "wrapping exceptions" technique to handle client generated errors. This allows us to take full control of the kind of errors we return, and easily translate them before sending to the end-users.

Internal server errors are logged to sentry. Create a Sentry account and add your SENTRY_DSN to the .env file.

Coding standard

We use Eslint AirBnB coding guidelines and import alias. All aliases are prefixed with a ~. To add a new alias, update the jsconfig.json, .eslintrc.js, and babel.config.json files. We also make use of Husky precommit hook to enforce standard.

Model specific logic should be moved to their associated data sources, and resolver errors should be handled using Wrapping Exception technique.

Readings