Building scalable Flutter apps using GraphQL, Hasura and event-driven serverless, Part 2 - Setting up Auth

Part 1 covers how to deploy Hasura and how to model the relational data with permissions. Part 3 covers how to create the Flutter frontend.
Also, we've written a guide to event-driven programming that may be useful to go through.
Firebase
$ npm install -g firebase-tools // Install Firebase CLI
$ firebase login // Login to your Firebase account
$ firebase init // Initialize a Firebase project
$ npm install –save graphql-request
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const functions = require("firebase-functions"); | |
const admin = require("firebase-admin"); | |
const request = require("graphql-request"); | |
const client = new request.GraphQLClient('<your-graphql-endpoint>', { | |
headers: { | |
"content-type": "application/json", | |
"x-hasura-admin-secret": "<your-admin-secret>" | |
} | |
}) | |
admin.initializeApp(functions.config().firebase); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// REGISTER USER WITH REQUIRED CUSTOM CLAIMS | |
exports.registerUser = functions.https.onCall(async (data, context) => { | |
const email = data.email; | |
const password = data.password; | |
const displayName = data.displayName; | |
if (email == null || password == null || displayName == null) { | |
throw new functions.https.HttpsError('signup-failed', 'missing information'); | |
} | |
try { | |
var userRecord = await admin.auth().createUser({ | |
email: email, | |
password: password, | |
displayName: displayName | |
}); | |
const customClaims = { | |
"https://hasura.io/jwt/claims": { | |
"x-hasura-default-role": "user", | |
"x-hasura-allowed-roles": ["user"], | |
"x-hasura-user-id": userRecord.uid | |
} | |
}; | |
await admin.auth().setCustomUserClaims(userRecord.uid, customClaims); | |
return userRecord.toJSON(); | |
} catch (e) { | |
throw new functions.https.HttpsError('signup-failed', JSON.stringify(error, undefined, 2)); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SYNC WITH HASURA ON USER CREATE | |
exports.processSignUp = functions.auth.user().onCreate(async user => { | |
const id = user.uid; | |
const email = user.email; | |
const name = user.displayName || "No Name"; | |
const mutation = `mutation($id: String!, $email: String, $name: String) { | |
insert_users(objects: [{ | |
id: $id, | |
email: $email, | |
name: $name, | |
}]) { | |
affected_rows | |
} | |
}`; | |
try { | |
const data = await client.request(mutation, { | |
id: id, | |
email: email, | |
name: name | |
}) | |
return data; | |
} catch (e) { | |
throw new functions.https.HttpsError('sync-failed'); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SYNC WITH HASURA ON USER DELETE | |
exports.processDelete = functions.auth.user().onDelete(async (user) => { | |
const mutation = `mutation($id: String!) { | |
delete_users(where: {id: {_eq: $id}}) { | |
affected_rows | |
} | |
}`; | |
const id = user.uid; | |
try { | |
const data = await client.request(mutation, { | |
id: id, | |
}) | |
return data; | |
} catch (e) { | |
throw new functions.https.HttpsError('sync-failed'); | |
} | |
}); |
$ firebase deploy
Hasura
HASURA_GRAPHQL_ADMIN_SECRET -> <your-password>
HASURA_GRAPHQL_JWT_SECRET ->
{
"type":"RS256",
"jwk_url":"https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.
com",
"audience": "<your-firebase-app-id>",
"issuer": "https://securetoken.google.com/<your-firebase-project-id>"
}
“Authorization” : Bearer <JWT TOKEN>
Event Trigger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// INCREMENT USER SCORE IF THE ANSWER IS CORRECT | |
exports.checkAnswer = functions.https.onRequest( async (request, response) => { | |
const answerID = request.body.event.data.new.answer_id; | |
const userID = request.body.event.data.new.user_id; | |
const answerQuery = ` | |
queryAnswer($answerID: uuid!) { | |
question_answers(where: {id: {_eq: $answerID}}) { | |
is_correct | |
} | |
}`; | |
const incrementMutation = ` | |
mutationScore($userID: String!) { | |
update_users(where: {id: {_eq: $userID}}, _inc: {score: 10}) { | |
affected_rows | |
} | |
}`; | |
try { | |
const data = await client.request(answerQuery, { | |
answerID: answerID, | |
}) | |
const isCorrect = data["question_answers"][0]["is_correct"]; | |
console.log(isCorrect); | |
if (!isCorrect) { | |
response.send("correct"); | |
return; | |
} else { | |
await client.request(incrementMutation, { userID: userID }) | |
response.send("correct"); | |
} | |
} catch (e) { | |
throw new functions.https.HttpsError(JSON.stringify(e, undefined, 2)); | |
} | |
}); |
$ firebase deploy
Related reading