/LaravelHasuraJWTStack

A working stack for Hasura and Laravel for GraphQL enthusiasts.

Primary LanguagePHP

Laravel + Hasura + MinIO GraphQL JWT Stack with Docker (November 2020) 🎉🎉🎉

This backend-only boilerplate is created for people who are really really curious about GraphQL with Laravel 7.x and want to experience the wonders of Hasura GraphQL Builder. 😎 This is also for the frontend developers (yep, you, guys 😉) who just want to have a ready-to-use backend with all the basic features like JWT authentication 🔐, multi-role system 👔, and file upload feature 💾.

Boilerplate Features

Btw, if you have cool suggestions, you can fork this repo or discuss it with me directly (Facebook). 😁 If you have a job offering, I am up for an interview! (Yes, I need MONEY! 💰💰💰)

P.S. There's a PPT presentation for more info here. Its password is laravelhasura.

Laravel 7.x Features

  • GraphQL-ready ⚡
  • GraphQL JWT Authentication System 🔐 (Login, Register, Social Login, etc.)
  • JWT Claims for Hasura GraphQL Server 💡
  • Multi-Roles for Users with Approval and Access Level Logic 👔
  • Anonymous Role for Newly Registered Users with Role Under Approval 👀
  • MinIO Object Storage Integration 💾
  • RESTful API route for Photo Upload 📷
  • RESTful API route for Multiple Photo Upload 📷
  • RESTful API route for Document Upload 📚
  • RESTful API route for Multiple Document Upload 📚
  • Payment Gateway Integration (Paymongo/Stripe) 💰
  • SMS Gateway Integration (Nexmo) 📮

To Be Added in the Future

  • Frontend (Gatsby React with Authentication Scaffolding) (LET'S GO FULLSTACK SOON! 🎉)

Boilerplate Structure

Laravel Hasura Stack

Technologies Used

  • GraphQL
  • Hasura
  • Postgres
  • Laravel
  • JWT
  • MinIO
  • Docker

Directory Structure

  • /laravel - Laravel application
  • /nginx - Configuration files for nginx to serve PHP
  • laravel.dockerfile - Dockerfile to boostrap Laravel app
  • nginx.dockerfile - Dockerfile to serve nginx website
  • hasura.dockerfile - Dockerfile for Hasura GraphQL engine
  • docker-compose.yaml - Composition of nginx + laravel + postgres to work together

How It Started (TL;DR) 🥱😴

I came upon an article about GraphQL while browsing the web for some cool tech stuff. I got excited all of a sudden after reading that it addresses over-fetching (getting more data than you need). For most, it's not big of a deal, but for backend developers, it is sort of a challenge. Yes, I know that we can solve it by adding more logic to our existing RESTful API's, but then again, that means more work for the backend people. 😭 So yeah, I like making my life easier so I chose GraphQL. 🤣 You can read more about other issues addressed by GraphQL here.

RESTful API Call

RESTful API Call

GraphQL API Call

GraphQL API Call

And so I tried researching how to create a GraphQL server with Laravel since I am comfortable with it. There I found the Laravel Lighthouse. Consequently, by pure luck, I found Jose Luis Fonseca's package (joselfonseca/lighthouse-graphql-passport-auth) which gave me instant authentication-related GraphQL mutations such as login, register, and logout! Thanks, Mr. Fonseca! 👏

I then studied how to create my own queries, mutations, and subscriptions but the amount of stuff to learn is A LOT! And so my lazy self looked for an easier route and there I found Hasura GraphQL API Builder, the best friend of all GraphQL enthusiasts! 🤩

With Hasura, all you need to do is create a database table via Hasura Console and all queries, mutations, and subscriptions are automagically created for you!

The Problem with Hasura 😱

Hasura is almost perfect really. But it lacks two very important features that most developers are looking for. And those are authentication 🔐 and non-database related computations 📈📉📊.

Hasura does not have a built-in authentication system. It is by design, according to them. It needs a 3rd party authentication provider like Auth0, AWS Cognito, AuthGuardian, etc. Adding custom logic on top of Hasura such as complicated computations is also not possible with Hasura. It is also by design. Either build a serverless function or create a RESTful API somewhere.

The Solution to Hasura's Predicament 💡

I almost gave up with Hasura but then a question popped out of my head...

Is it possible for Laravel and Hasura to fulfill each other's weaknesses?

I mean just think about it...

  • Hasura needs a 3rd party authentication service. Laravel can provide that easily (via Laravel Passport)!
  • Hasura needs 3rd party serverless functions for custom computations. Laravel can provide that as well (via RESTful APIs)!
  • GraphQL development with Laravel is really slow for beginners. Hasura can make it faster!

So yep, theoretically, they are match made in heaven. 😍 So the goal became how to make them work with each other.

The Final Product

So it took me a while (months really since I have too much on my plate) to finally figure out how to make 'em work with each other.

Before, Laravel can create JWT access tokens but that isn't enough for Hasura since it requires custom claims. So I looked for a way to add some custom claims to Laravel's default JWT, and there I found the final piece of the puzzle, Cor Bosman's package (corbosman/laravel-passport-claims). Thanks, Mr. Bosman! 👏

Below is the custom claims added using the package:

Hasura Custom Claims

Below is the JWT mode of authentication for the whole boilerplate:

Hasura's JWT Mode Diagram



🌄 Local Deployment 🌄

Requires Docker v2.x or higher

To download Docker images and create PostgreSQL, MinIO and Hasura containers on Docker Hub, you have to run this command:

docker-compose up -d

To remove all containers and volumes (persistent data), you have to run this command:

docker-compose down -v

Make sure to be on the root folder of the cloned boilerplate as this command will look for the docker-compose.yaml. If successful, you should see the 3 services.

Boilerplate


Part 1: MinIO Configuration 🚧

To configure MinIO to work with Laravel, follow the GIF instruction below.

MinIO config

Note: You can always choose another name for the bucket but make sure to add Read and Write Policy to that bucket and update the MINIO_BUCKET variable on Laravel's .env file.


Part 2: Laravel Configuration 🚧

Laravel's files are located on the laravel folder. Do not rename the folder since Nginx will look for this specific folder during online deployment.

Step 1: Since Laravel requires .env file, rename the .env.example to .env. Make sure to install all Laravel dependencies by running this command:

composer install

Step 2: Make sure to generate your own app key before anything else. App Key will be used for all sorts of encryption within Laravel.

php artisan key:generate

Generate New App Key

Step 3: Run fresh migrations to create all necessary tables and seed the roles table as well. To add or customize the roles, look for the RolesSeeder.php file.

php artisan migrate:fresh --seed

Run Fresh Migrations

Step 4: You have to generate your own OAuth Keys as well. To remove mine, just use --force. Make sure to paste the new grant client id and secret to Laravel's env file.

php artisan passport:install --force

Generate New OAuth Keys

Step 5: Make sure to refresh config cache to utilize the new keys.

php artisan config:cache

Clear Configs Step 6: Run the app to test our GraphQL API. By default, the GraphQL endpoint will be at http://localhost:8000/graphql. You can download GraphiQL or GraphQL Playground to test this endpoint.

php artisan serve

Serve the application

Step 7: If we will try to register, the role id is required. We can look for the role id's via Laravel Tinker.

Via Laravel Tinker

Roles on Tinker

Step 8: Since we now have the role id for the user, we can proceed to register an account. To register, we have to use the register mutation and supply all information needed. For this demo, we just need the access_token so we only need to specify that as the output.

mutation registerUser {
  register(object:{
    given_name: "James"
    middle_name: "Sebial"
    surname: "Luchavez"
    email: "james@gmail.com"
    password: "12345678"
    password_confirmation: "12345678"
    role_id: "insert_id_here"
  }){
    tokens{
      access_token
      refresh_token
    }
  }
}

Register Mutation

Step 9: Since we already registered the user above, we can now use it to login as well. To login, we have to use the login mutation and specify access_token to be the output.

mutation loginUser {
  login(object:{
    username: "mrstudyfirst@gmail.com"
    password: "12345678"
  }){
    access_token
    refresh_token
  }
}

Login Mutation

Step 10: The JWT access_token can be decoded to get important information like expiration date, user information, and custom claims (required by Hasura). You can try at jwt.io and paste the access_token.

JWT.io

To view all authentication mutations, photos CRUD, and documents CRUD, import my Postman Collection: https://www.getpostman.com/collections/c58cae3c7bc6bf72462d


Part 3: Hasura Configuration 🚧

For Hasura to utilize the tokens generated by Laravel, it needs to have the OAuth public key. Look for HASURA_GRAPHQL_JWT_SECRET on the docker-compose.yaml file and add the single-line public key. Follow the GIF instruction below for the single-line public key.

Add Public Key to docker-compose

Once finished, run the following command again to recreate the Hasura container:

docker-compose up -d

Part 4: Hasura Schema Stitch with Laravel 🚧

For Hasura to get all of Laravel's Schema operations like queries, mutations, and subscriptions, we need to setup Hasura Remote Schema.

Open the Hasura Console, go to Remote Schema tab. Click Add. Give it a name like "Laravel". Since this is local deployment, Hasura requires host.docker.internal instead of localhost. So on GraphQL server URL, since Laravel's GraphQL API endpoint is at http://localhost:8000/graphql, it will now become http://host.docker.internal:8000/graphql. But for deployed versions, it will require the actual GraphQL endpoint.

Hasura Schema Stitching



📷 How to Upload Photo and Document 📚

As mentioned, one vital part of this boilerplate is the storage service (MinIO). Before you can upload anything, you have to have a valid access_token and you can only get one if you have registered already. To demonstrate, Postman is used.

To view all authentication mutations, photos CRUD, and documents CRUD, import my Postman Collection: https://www.getpostman.com/collections/c58cae3c7bc6bf72462d

List of GraphQL Authentication Mutations, Photos CRUD, Documents CRUD

Also make sure to provide values to variables like this:

Postman Collection Variables

Upload Single Photo 📷

  • Endpoint: http://localhost:8000/api/photos
  • Operation: POST
  • Key: photo
  • Validations: images|max:5120 (max of 5MB)

Upload Single Photo

Upload Multiple Photos 📷

  • Endpoint: http://localhost:8000/api/photos
  • Operation: POST
  • Key: photos[]
  • Validations: images|max:5120 (max of 5MB each)

Upload Multiple Photos

Upload Single Document 📚

  • Endpoint: http://localhost:8000/api/documents
  • Operation: POST
  • Key: document
  • Validations: mimes:doc,docx,pdf,txt|max:5120 (max of 5MB)

Upload Single Document

Upload Multiple Documents 📚

  • Endpoint: http://localhost:8000/api/documents
  • Operation: POST
  • Key: documents[]
  • Validations: mimes:doc,docx,pdf,txt|max:5120 (max of 5MB each)

Upload Multiple Documents



🌎 Online Deployment at KintoHub (optional) 🌎

Since KintoHub's databases can only be accessed within their environment (which makes it really secure), I usually just deploy this setup as it is. I then connect to my Postgres database via Kinto CLI, and then redeploy my Laravel application and Hasura server with updated settings.

Note: You can always deploy this setup on other platforms like Heroku, DigitalOcean, etc. I just use KintoHub since this is where I first deployed it and since it uses Docker by default.

Deploying on KintoHub (optional)

If you do not have an account yet, signup first.

Deploy a PostgreSQL Server (optional)

  1. Click Create Service at the top right of your environment
  2. Click From Catalog and then select PostgreSQL
  3. Fill or generate your username, password and root password for your database
  4. Click Deploy At the top right.

Once complete, go to the Access tab and copy the Root User Connection String. This will take several minutes to complete.

Note: You can deploy your PostgreSQL database elsewhere but make sure to install pgcrypto extension.

Deploy Laravel PHP App (optional)

  1. Click Create Service at the top right of your environment
  2. Choose Backend API from the list
  3. Change the Dockerfile Name to laravel.dockerfile
  4. Change the Port to 9000
  5. Open the Environment Variables tab and paste these values:
APP_DEBUG=true
APP_KEY=insert_new_app_key
APP_NAME=Laravel
DB_CONNECTION=pgsql
DB_PORT=5432
DB_DATABASE=insert_db_name
DB_HOST=insert_db_host
DB_USERNAME=insert_db_username
DB_PASSWORD=insert_db_password
LOG_CHANNEL=errorlog
MINIO_BUCKET=demo
MINIO_ENDPOINT=http://storage.jarcalculator.me:9000
MINIO_KEY=minioadmin
MINIO_REGION=us-east-1
MINIO_SECRET=minioadmin
PASSPORT_CLIENT_ID=insert_grant_client_id
PASSPORT_CLIENT_SECRET=insert_grant_client_secret

Deploy Nginx Php Proxy (optional)

We need to deploy web host proxy to serve the php app on KintoHub.

  1. Click Create Service at the top right of your environment
  2. Choose Web App from the list
  3. Change the Dockerfile Name to nginx.dockerfile
  4. Change the Port to 80
  5. Open the Environment Variables tab to copy and paste the following into the key textbox:
  6. Click Deploy at the top right

When complete, open the Access tab and open the external URL to see your Laravel app!

Deploy Hasura GraphQL Engine (optional)

  1. Click Create Service at the top right of your environment
  2. Choose Backend API from the list
  3. Change the Dockerfile Name to hasura.dockerfile
  4. Change the Port to 8080
  5. Open the Environment Variables tab and paste these values:
HASURA_GRAPHQL_ADMIN_SECRET=12345678
HASURA_GRAPHQL_DATABASE_URL=postgresql://username:password@host:5432/database
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,query-log,websocket-log,webhook-log
HASURA_GRAPHQL_ENABLE_CONSOLE=true
HASURA_GRAPHQL_JWT_SECRET={   "type":"RS256",   "key": "" }
HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous

When complete, open the Access tab and open the external URL to see your Hasura app!