Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ A big thank you to all contributors and supporters of this repository 💚
<a href="https://github.com/mwickett/" target="_blank">
<img src="https://github.com/mwickett.png?size=64" width="64" height="64" alt="petrvlcek">
</a>
<a href="https://github.com/allpwrfulroot/" target="_blank">
<img src="https://github.com/allpwrfulroot.png?size=64" width="64" height="64" alt="allpwrfulroot">
</a>

## Help & Community [![Slack Status](https://slack.graph.cool/badge.svg)](https://slack.graph.cool)

Expand Down
39 changes: 39 additions & 0 deletions phone-user-management/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# phone-user-management

Functions for phone and deviceID login using Schema Extensions and Graphcool Functions ⚡️

> Note: Schema Extensions are currently only available in the Beta Program.

## Getting Started

```sh
npm -g install graphcool
graphcool init --schema phone-user-management.graphql
```

## Usage

1. Your app calls the Graphcool mutation `signupPhoneUser(phoneNumber: String!, deviceId: String!)` to signup Users
2. Your app calls the Graphcool mutation `authenticatePhoneUser(phoneNumber: String!, deviceId: String!)` to authenticate Users
3. Your app calls the Graphcool mutation `updatePhone(phoneNumber: String!, deviceId: String!, newPhoneNumber: String!)` to change a User's phone number (use case: gets new phone number)
4. Your app calls the Graphcool mutation `updateDevice(phoneNumber: String!, deviceId: String!, newDeviceId: String!)` to change a User's deviceId (use case: gets new phone)

## Notes

The function in this example use a Permanent Authentication Token to fetch or create users via the API so the permissions are not needed. Therefore, it's recommended to **remove Create, Update and Read permissions** for the fields `phoneNumber` and `deviceId` on `User`. Those fields are controlled by the custom mutations that are added in this example.

This also allows you to change the phoneNumber or deviceId fields using a PAT manually as an administrator. For manually updating deviceId, note that the `deviceId` field is hashed and salted using `bcrypt` and for authenticating or updating the user information, the current deviceId is compared to that hashed and salted version.

## Test the Code

First, follow the READMEs to setup all the functions in the functions folder. Then go to the Graphcool Playground:

```sh
graphcool playground
```

and follow the instructions in the functions folder to test the code. It makes sense to setup the functions in the above order.

## Contributions

A straightforward adaptation of [@stevewpatterson](https://github.com/stevewpatterson)'s email auth example by [allpwrfulroot](https://github.com/allpwrfulroot) :tada:
36 changes: 36 additions & 0 deletions phone-user-management/functions/authenticate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# authenticate

Authenticate users in your app using phoneNumber and deviceId and receive a token in return.

## Authentication flow in app

1. Your app calls the Graphcool mutation `authenticatePhoneUser(phoneNumber: String!, deviceId: String!)`
2. If no user exists yet that corresponds to the passed `phoneNumber`, or the `deviceId` does not match, an error will be returned
3. If a user with the passed `phoneNumber` exists and the `deviceId` matches, the mutation returns a valid token for the user
4. Your app stores the token and uses it in its `Authorization` header for all further requests to Graphcool

## Setup the Authentication Function

* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `authenticate.js`.
* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings.

## Test the Code

First, you need to create a new user with the `signup` function. Then, go to the Graphcool Playground:

```sh
graphcool playground
```

and run this mutation to authenticate as that user:

```graphql
mutation {
# replace __PHONE_NUMBER__ and __DEVICE_ID__
authenticatePhoneUser(phoneNumber: "__PHONE_NUMBER__", deviceId: "__DEVICE_ID__") {
token
}
}
```

If the phoneNumber/deviceId combo are valid you should see that it returns a token. The returned token can be used to authenticate requests to your Graphcool API as that user.
53 changes: 53 additions & 0 deletions phone-user-management/functions/authenticate/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const fromEvent = require('graphcool-lib').fromEvent
const bcrypt = require('bcrypt')

module.exports = function(event) {
const phoneNumber = event.data.phoneNumber
const deviceId = event.data.deviceId
const graphcool = fromEvent(event)
const api = graphcool.api('simple/v1')

function getGraphcoolUser(phoneNumber) {
return api.request(`
query {
User(phoneNumber: "${phoneNumber}") {
id
deviceId
}
}`)
.then((userQueryResult) => {
if (userQueryResult.error) {
return Promise.reject(userQueryResult.error)
} else {
return userQueryResult.User
}
})
}

function generateGraphcoolToken(graphcoolUserId) {
return graphcool.generateAuthToken(graphcoolUserId, 'User')
}

return getGraphcoolUser(phoneNumber)
.then((graphcoolUser) => {
if (graphcoolUser === null) {
return Promise.reject("Invalid Credentials") //returning same generic error so user can't find out what phoneNumbers are registered.
} else {
return bcrypt.compare(deviceId, graphcoolUser.deviceId)
.then((res) => {
if (res === true) {
return graphcoolUser.id
} else {
return Promise.reject("Invalid Credentials")
}
})
}
})
.then(generateGraphcoolToken)
.then((token) => {
return { data: { token } }
})
.catch((error) => {
return { error: error.toString() }
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type AuthenticatePhoneUserPayload {
token: String!
}

extend type Mutation {
authenticatePhoneUser(phoneNumber: String!, deviceId: String!): AuthenticatePhoneUserPayload
}
37 changes: 37 additions & 0 deletions phone-user-management/functions/signup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# phone-user-creation

Signup new users in your app using phone number and device ID and receive a token in return.

## Signup flow in app

1. Your app calls the Graphcool mutation `signupPhoneUser(phoneNumber: String!, deviceId: String!)`
2. If no user exists yet that corresponds to the passed `phoneNumber`, a new `User` node will be created with the deviceId (after being hashed and salted)
3. If a user with the passed `phoneNumber` exists, a `User` node is not created and an error is returned
4. If a user is created, then the `signupPhoneUser(phoneNumber: String!, deviceId: String!)` mutation returns the id for the new user

## Setup the Create User Function

* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `signup.js`.
* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings.
* Remove all Create permissions for the `User` type. The function uses a Permanent Access Token to create users via the API so the permissions are not needed.

## Test the Code

Go to the Graphcool Playground:

```sh
graphcool playground
```

Run this mutation to create a user:

```graphql
mutation {
# replace __PHONE_NUMBER__ and __DEVICE_ID__
signupPhoneUser(phoneNumber: "__PHONE_NUMBER__", deviceId: "__DEVICE_ID__") {
id
}
}
```

You should see that a new user has been created. The returned id can be used to query your Graphcool API for that user. Note that running the mutation again with the same phoneNumber will return an error stating that the phoneNumber is already in use.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type SignupPhoneUserPayload {
id: ID!
}

extend type Mutation {
signupPhoneUser(phoneNumber: String!, deviceId: String!): SignupPhoneUserPayload
}
62 changes: 62 additions & 0 deletions phone-user-management/functions/signup/signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fromEvent = require('graphcool-lib').fromEvent
const bcrypt = require('bcrypt')
const validator = require('validator')

module.exports = function(event) {
const phoneNumber = event.data.phoneNumber
const deviceId = event.data.deviceId
const graphcool = fromEvent(event)
const api = graphcool.api('simple/v1')
const SALT_ROUNDS = 10

function getGraphcoolUser(phoneNumber) {
return api.request(`
query {
User(phoneNumber: "${phoneNumber}") {
id
}
}`)
.then((userQueryResult) => {
if (userQueryResult.error) {
return Promise.reject(userQueryResult.error)
} else {
return userQueryResult.User
}
})
}

function createGraphcoolUser(phoneNumber, deviceIdHash) {
return api.request(`
mutation {
createUser(
phoneNumber: "${phoneNumber}",
deviceId: "${deviceIdHash}"
) {
id
}
}`)
.then((userMutationResult) => {
return userMutationResult.createUser.id
})
}

if (validator.isMobilePhone(phoneNumber, 'any')) {
return getGraphcoolUser(phoneNumber)
.then((graphcoolUser) => {
if (graphcoolUser === null) {
return bcrypt.hash(deviceId, SALT_ROUNDS)
.then(hash => createGraphcoolUser(phoneNumber, hash))
} else {
return Promise.reject("phone number already in use")
}
})
.then((id) => {
return { data: { id } }
})
.catch((error) => {
return { error: error.toString() }
})
} else {
return { error: "Not a valid phone number" }
}
}
36 changes: 36 additions & 0 deletions phone-user-management/functions/update-device/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# update-device

Update the deviceId for users in your app who login using phoneNumber and deviceId.

## Update deviceId flow in app

1. Your app calls the Graphcool mutation `updateDevice(phoneNumber: String!, deviceId: String!, newDeviceId: String!)`
3. If a user with the phoneNumber and deviceId combination is not found, an error is returned
4. If a user with the passed `phoneNumber` exists and `deviceId` matches, the user's `deviceId` field is updated to the new deviceId.
5. The mutation returns the id of the updated user

## Setup the deviceId Update Function

* Create a new Schema Extension Function and paste the schema from `schema-extension.graphql` and code from `update-device.js`.
* add a PAT to the project *called the same as your function*. The token can be obtained from the Authentication tab in the project settings.

## Test the Code

First, you need to create a new user with the `signup` function. Then, go to the Graphcool Playground:

```sh
graphcool playground
```

Run this mutation to change a user's deviceId:

```graphql
mutation {
# replace __PHONE_NUMBER__ , __OLD_DEVICE_ID__ , and __NEW_DEVICE_ID__
updateDevice(phoneNumber: "__PHONE_NUMBER__", deviceId: "__OLD_DEVICE_ID__", newDeviceId: "__NEW_DEVICE_ID__") {
id
}
}
```

If the phoneNumber/newDeviceId combo are valid you should see that it updates the user's deviceId field and returns the user's id.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type UpdateDevicePayload {
id: ID!
}

extend type Mutation {
updateDevice(phoneNumber: String!, deviceId: String!, newDeviceId: String!): UpdateDevicePayload
}
66 changes: 66 additions & 0 deletions phone-user-management/functions/update-device/update-device.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const fromEvent = require('graphcool-lib').fromEvent
const bcrypt = require('bcrypt')

module.exports = function(event) {
const phoneNumber = event.data.phoneNumber
const deviceId = event.data.deviceId
const newDeviceId = event.data.newDeviceId
const graphcool = fromEvent(event)
const api = graphcool.api('simple/v1')
const saltRounds = 10

function getGraphcoolUser(phoneNumber) {
return api.request(`
query {
User(phoneNumber: "${phoneNumber}") {
id
deviceId
}
}`)
.then((userQueryResult) => {
if (userQueryResult.error) {
return Promise.reject(userQueryResult.error)
} else {
return userQueryResult.User
}
})
}

function updateGraphcoolUser(id, newDeviceIdHash) {
return api.request(`
mutation {
updateUser(
id:"${id}",
deviceId:"${newDeviceIdHash}"
) {
id
}
}`)
.then((userMutationResult) => {
return userMutationResult.updateUser.id
})
}

return getGraphcoolUser(phoneNumber)
.then((graphcoolUser) => {
if (graphcoolUser === null) {
return Promise.reject("Invalid Credentials")
} else {
return bcrypt.compare(deviceId, graphcoolUser.deviceId)
.then((res) => {
if (res == true) {
return bcrypt.hash(newDeviceId, saltRounds)
.then(hash => updateGraphcoolUser(graphcoolUser.id, hash))
} else {
return Promise.reject("Invalid Credentials")
}
})
}
})
.then((id) => {
return { data: { id } }
})
.catch((error) => {
return { error: error.toString() }
})
}
Loading