Final part of a three-part tutorial
In part two, How to Connect Blockstack to your Backend API, I show you how to create a user object in your API after logging in with Blockstack. In this tutorial, we’ll build a form that sends public information to that API and a separate form that sends secret/sensitive information to Gaia Storage.
Blockstack applications use the Gaia storage system to store data on behalf of a user. When the user logs in to an application, the authentication process gives the application the URL of a Gaia hub, which performs writes on behalf of that user. The Gaia hub authenticates writes to a location by requiring a valid authentication token, generated by a private key authorized to write at that location.
By using Gaia Storage, the decentralized way of storing information:
- Your data is more secure than traditional storage systems, which have one or a few central points of vulnerability.
- Millions of encrypted copies of your data are spread across the world, constantly verifying each other for changes made without your authorization.
- A hacker would need to compromise 51% of the blockchain in order to access your data. This would require more computing power than any known entity possesses.
- You own your own data, your information will be safe and no one can access it, not me, not Blockstack, not even the president.
Prerequisites: Knowledge of setting up your own API. We’ll also be using React.js.
Coming from part two of this three-part tutorial series, this is what App.js
looked like:
import React, { Component } from "react"; | |
import { appConfig } from "./utils/constants"; | |
import { UserSession } from "blockstack"; | |
class App extends Component { | |
state = { | |
userSession: new UserSession({ appConfig }), // coming from Blockstack | |
userData: {}, // coming from Blockstack | |
users: [], // coming from your API | |
currentUser: {} // coming from your API | |
}; | |
componentDidMount = async () => { | |
const { userSession } = this.state; | |
if (!userSession.isUserSignedIn() && userSession.isSignInPending()) { | |
const userData = await userSession.handlePendingSignIn(); | |
if (!userData.username) { | |
throw new Error("This app requires a username"); | |
} | |
window.location = "/"; | |
} | |
this.getUsers(); | |
}; | |
handleSignIn = () => { | |
const { userSession } = this.state; | |
userSession.redirectToSignIn(); | |
}; | |
handleSignOut = () => { | |
const { userSession } = this.state; | |
userSession.signUserOut(); | |
window.location = "/"; | |
}; | |
// We're fetching the users array from your API (make sure the path is correct) | |
// In your app's state, we're storing the userData object that comes from Blockstack when a user signs in | |
// We're searching for the username from userData in the users array, | |
// If that username exists in your API, then we store that user object in state | |
// Otherwise, we create a new user object with the username from userData | |
getUsers() { | |
const { userSession } = this.state; | |
fetch("http://localhost:3000/api/v1/users") | |
.then(res => res.json()) | |
.then(users => { | |
if (userSession.isUserSignedIn()) { | |
const userData = userSession.loadUserData(); | |
this.setState({ | |
userData | |
}); | |
let currentUser = users.find( | |
user => user.username === userData.username | |
); | |
if (currentUser) { | |
this.setState({ users, currentUser }); | |
} else { | |
this.createUser(userData.username); | |
} | |
} | |
}); | |
} | |
createUser = username => { | |
fetch("http://localhost:3000/api/v1/users", { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
Accept: "application/json" | |
}, | |
body: JSON.stringify({ | |
username | |
}) | |
}) | |
.then(res => res.json()) | |
.then(user => { | |
let newArr = [...this.state.users, user]; | |
this.setState({ users: newArr, currentUser: user }); | |
}); | |
}; | |
render() { | |
const { userSession, currentUser } = this.state; | |
return ( | |
<div className="App"> | |
{userSession.isUserSignedIn() ? ( | |
<div className="hello"> | |
<h2>Hello {currentUser.username} !</h2> | |
<button className="button" onClick={this.handleSignOut}> | |
<strong>Sign Out</strong> | |
</button> | |
</div> | |
) : ( | |
<button className="button" onClick={this.handleSignIn}> | |
<strong>Sign In</strong> | |
</button> | |
)} | |
</div> | |
); | |
} | |
} | |
export default App; |
We’re going to add a form in JSX and some handler methods to use our API. We’ll, do the same for Gaia Storage but in the Blockstack way. Skip to step 6 if you only want to see the final code.
1) We’ll be adding a few placeholders to your app’s state:
state = { | |
userSession: new UserSession({ appConfig }), // coming from Blockstack | |
userData: {}, // coming from Blockstack | |
users: [], // coming from your API | |
currentUser: {}, // coming from your API | |
superhero: "", // I'M NEW coming from form input | |
gaiaUser: {}, // I'M NEW coming from Gaia Storage | |
crush: "" // I'M NEW coming from form input | |
}; |
2) Within the App class, we’ll add the changeHandler, submitHandler, and submitGaiaHandler functions for the forms (be sure to add the superhero
attribute to your backend for the fetch PATCH request):
// I'M NEW | |
changeHandler = e => { | |
this.setState({ [e.target.name]: e.target.value }); | |
}; | |
// I'M NEW, standard fetch method | |
submitHandler = e => { | |
const { superhero, currentUser } = this.state; | |
e.preventDefault(); | |
// be sure to add the superhero attribute to the backend | |
fetch(`http://localhost:3000/api/v1/users/${currentUser.id}`, { | |
method: "PATCH", | |
headers: { | |
"Content-Type": "application/json", | |
Accept: "application/json" | |
}, | |
body: JSON.stringify({ | |
superhero | |
}) | |
}) | |
.then(res => res.json()) | |
.then(data => { | |
this.setState({ currentUser: data }); | |
console.log("API", data); // see that the data transferred | |
}); | |
}; | |
// I'M NEW, putFile is a method provided by the Blockstack library | |
submitGaiaHandler = e => { | |
const { userSession, crush } = this.state; | |
const user = { crush: crush }; | |
let options = { encrypt: true }; | |
e.preventDefault(); | |
// encrypt and securely send your secret crush to Gaia Storage | |
userSession | |
.putFile("user.json", JSON.stringify(user), options) | |
.then(data => { | |
this.setState({ gaiaUser: user }); | |
console.log("Gaia Storage", data); // see that the data is encrypted | |
}); | |
// note that at this time, Blockstack only allows PUT but not PATCH | |
// you are replacing the entire gaiaUser object | |
}; |
3) Within the App class, we’ll add a function to retrieve data from Gaia Storage:
// I'M NEW, getFile is also a method provided by the Blockstack library | |
getGaiaUser = () => { | |
const { userSession } = this.state; | |
let options = { decrypt: true }; | |
userSession.getFile("user.json", options).then(data => { | |
if (data) { | |
const user = JSON.parse(data); | |
this.setState({ gaiaUser: user }); | |
} else { | |
const user = {}; | |
this.setState({ gaiaUser: user }); | |
} | |
}); | |
}; |
4) Call getGaiaUser()
function in componentDidMount()
:
componentDidMount = async () => { | |
const { userSession } = this.state; | |
if (!userSession.isUserSignedIn() && userSession.isSignInPending()) { | |
const userData = await userSession.handlePendingSignIn(); | |
if (!userData.username) { | |
throw new Error("This app requires a username"); | |
} | |
window.location = "/"; | |
} | |
this.getUsers(); | |
this.getGaiaUser(); // I'M NEW, find me | |
}; |
5) In render()
, we’ll add the JSX for our new forms:
render() { | |
const { userSession, currentUser, superhero, crush, gaiaUser } = this.state; | |
let hero = currentUser.superhero; | |
let gaiaCrush = gaiaUser.crush; | |
return ( | |
<div className="App"> | |
{userSession.isUserSignedIn() ? ( | |
<div className="hello"> | |
<h2>Hello {currentUser.username} !</h2> | |
<button className="button" onClick={this.handleSignOut}> | |
<strong>Sign Out</strong> | |
</button> | |
<div className="forms"> | |
{/* sending this information to public API */} | |
<div className="superhero"> | |
<form onSubmit={this.submitHandler}> | |
<label htmlFor="superhero"> | |
<h3>Who's your favorite superhero?</h3> | |
</label> | |
<input | |
id="superhero" | |
className="form-control" | |
name="superhero" | |
type="text" | |
placeholder="Ironman" | |
value={superhero} | |
onChange={this.changeHandler} | |
/> | |
<button className="button-small"> | |
<strong>Submit to API</strong> | |
</button> | |
</form> | |
{hero && hero.toLowerCase() === "ironman" ? ( | |
<h4>Good choice, {hero} is the best!</h4> | |
) : hero ? ( | |
<p>{hero} is okay, but Ironman is the best!</p> | |
) : null} | |
<p> | |
This information is accessible to the public should you allow | |
other apps to fetch from your API. | |
</p> | |
</div> | |
<div className="crush"> | |
{/* sending this information to Gaia Storage */} | |
<form onSubmit={this.submitGaiaHandler}> | |
<label htmlFor="crush"> | |
<h3>Who's your current or childhood crush?</h3> | |
</label> | |
<input | |
id="crush" | |
className="form-control" | |
name="crush" | |
type="text" | |
placeholder="His/her name" | |
value={crush} | |
onChange={this.changeHandler} | |
/> | |
<button className="button-small"> | |
<strong>Submit to Gaia Storage</strong> | |
</button> | |
</form> | |
{gaiaCrush ? ( | |
<h4>{gaiaCrush} probably likes you too.</h4> | |
) : null} | |
<p> | |
Your secret is safe, only you have access to this information | |
and it is extremely difficult to hack. | |
</p> | |
</div> | |
</div> | |
</div> | |
) : ( | |
<button className="button" onClick={this.handleSignIn}> | |
<strong>Sign In</strong> | |
</button> | |
)} | |
</div> | |
); | |
} |
6) At the end of this process, App.js
should look like this:
import React, { Component } from "react"; | |
import { appConfig } from "./utils/constants"; | |
import { UserSession } from "blockstack"; | |
class App extends Component { | |
state = { | |
userSession: new UserSession({ appConfig }), // coming from Blockstack | |
userData: {}, // coming from Blockstack | |
users: [], // coming from your API | |
currentUser: {}, // coming from your API | |
superhero: "", // I'M NEW coming from form input | |
gaiaUser: {}, // I'M NEW coming from Gaia Storage | |
crush: "" // I'M NEW coming from form input | |
}; | |
componentDidMount = async () => { | |
const { userSession } = this.state; | |
if (!userSession.isUserSignedIn() && userSession.isSignInPending()) { | |
const userData = await userSession.handlePendingSignIn(); | |
if (!userData.username) { | |
throw new Error("This app requires a username"); | |
} | |
window.location = "/"; | |
} | |
this.getUsers(); | |
this.getGaiaUser(); // I'M NEW, find me | |
}; | |
handleSignIn = () => { | |
const { userSession } = this.state; | |
userSession.redirectToSignIn(); | |
}; | |
handleSignOut = () => { | |
const { userSession } = this.state; | |
userSession.signUserOut(); | |
window.location = "/"; | |
}; | |
// We're fetching the users array from your API (make sure the path is correct) | |
// In your app's state, we're storing the userData object that comes from Blockstack when a user signs in | |
// We're searching for the username from userData in the users array, | |
// If that username exists in your API, then we store that user object in state | |
// Otherwise, we create a new user object with the username from userData | |
getUsers() { | |
const { userSession } = this.state; | |
fetch("http://localhost:3000/api/v1/users") | |
.then(res => res.json()) | |
.then(users => { | |
if (userSession.isUserSignedIn()) { | |
const userData = userSession.loadUserData(); | |
this.setState({ | |
userData | |
}); | |
let currentUser = users.find( | |
user => user.username === userData.username | |
); | |
if (currentUser) { | |
this.setState({ users, currentUser }); | |
} else { | |
this.createUser(userData.username); | |
} | |
} | |
}); | |
} | |
createUser = username => { | |
fetch("http://localhost:3000/api/v1/users", { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
Accept: "application/json" | |
}, | |
body: JSON.stringify({ | |
username | |
}) | |
}) | |
.then(res => res.json()) | |
.then(user => { | |
let newArr = [...this.state.users, user]; | |
this.setState({ users: newArr, currentUser: user }); | |
}); | |
}; | |
// I'M NEW | |
changeHandler = e => { | |
this.setState({ [e.target.name]: e.target.value }); | |
}; | |
// I'M NEW, standard fetch method | |
submitHandler = e => { | |
const { superhero, currentUser } = this.state; | |
e.preventDefault(); | |
// be sure to add the superhero attribute to the backend | |
fetch(`http://localhost:3000/api/v1/users/${currentUser.id}`, { | |
method: "PATCH", | |
headers: { | |
"Content-Type": "application/json", | |
Accept: "application/json" | |
}, | |
body: JSON.stringify({ | |
superhero | |
}) | |
}) | |
.then(res => res.json()) | |
.then(data => { | |
this.setState({ currentUser: data }); | |
console.log("API", data); // see that the data transferred | |
}); | |
}; | |
// I'M NEW, putFile is a method provided by the Blockstack library | |
submitGaiaHandler = e => { | |
const { userSession, crush } = this.state; | |
const user = { crush: crush }; | |
let options = { encrypt: true }; | |
e.preventDefault(); | |
// encrypt and securely send your secret crush to Gaia Storage | |
userSession | |
.putFile("user.json", JSON.stringify(user), options) | |
.then(data => { | |
this.setState({ gaiaUser: user }); | |
console.log("Gaia Storage", data); // see that the data is encrypted | |
}); | |
// note that at this time, Blockstack only allows PUT but not PATCH | |
// you are replacing the entire gaiaUser object | |
}; | |
// I'M NEW, getFile is also a method provided by the Blockstack library | |
getGaiaUser = () => { | |
const { userSession } = this.state; | |
let options = { decrypt: true }; | |
userSession.getFile("user.json", options).then(data => { | |
if (data) { | |
const user = JSON.parse(data); | |
this.setState({ gaiaUser: user }); | |
} else { | |
const user = {}; | |
this.setState({ gaiaUser: user }); | |
} | |
}); | |
}; | |
render() { | |
const { userSession, currentUser, superhero, crush, gaiaUser } = this.state; | |
let hero = currentUser.superhero; | |
let gaiaCrush = gaiaUser.crush; | |
return ( | |
<div className="App"> | |
{userSession.isUserSignedIn() ? ( | |
<div className="hello"> | |
<h2>Hello {currentUser.username} !</h2> | |
<button className="button" onClick={this.handleSignOut}> | |
<strong>Sign Out</strong> | |
</button> | |
<div className="forms"> | |
{/* sending this information to public API */} | |
<div className="superhero"> | |
<form onSubmit={this.submitHandler}> | |
<label htmlFor="superhero"> | |
<h3>Who's your favorite superhero?</h3> | |
</label> | |
<input | |
id="superhero" | |
className="form-control" | |
name="superhero" | |
type="text" | |
placeholder="Ironman" | |
value={superhero} | |
onChange={this.changeHandler} | |
/> | |
<button className="button-small"> | |
<strong>Submit to API</strong> | |
</button> | |
</form> | |
{hero && hero.toLowerCase() === "ironman" ? ( | |
<h4>Good choice, {hero} is the best!</h4> | |
) : hero ? ( | |
<p>{hero} is okay, but Ironman is the best!</p> | |
) : null} | |
<p> | |
This information is accessible to the public should you allow | |
other apps to fetch from your API. | |
</p> | |
</div> | |
<div className="crush"> | |
{/* sending this information to Gaia Storage */} | |
<form onSubmit={this.submitGaiaHandler}> | |
<label htmlFor="crush"> | |
<h3>Who's your current or childhood crush?</h3> | |
</label> | |
<input | |
id="crush" | |
className="form-control" | |
name="crush" | |
type="text" | |
placeholder="His/her name" | |
value={crush} | |
onChange={this.changeHandler} | |
/> | |
<button className="button-small"> | |
<strong>Submit to Gaia Storage</strong> | |
</button> | |
</form> | |
{gaiaCrush ? ( | |
<h4>{gaiaCrush} probably likes you too.</h4> | |
) : null} | |
<p> | |
Your secret is safe, only you have access to this information | |
and it is extremely difficult to hack. | |
</p> | |
</div> | |
</div> | |
</div> | |
) : ( | |
<button className="button" onClick={this.handleSignIn}> | |
<strong>Sign In</strong> | |
</button> | |
)} | |
</div> | |
); | |
} | |
} | |
export default App; |
7) Let’s sprinkle some CSS on this in App.css
and make it a little easier on the eyes:
.App { | |
display: flex; | |
text-align: center; | |
height: 100vh; | |
} | |
.hello { | |
margin: auto; | |
} | |
.button { | |
max-width: 100%; | |
max-height: 4em; | |
font-size: 1em; | |
margin: auto; | |
display: inline-block; | |
padding: 1em; | |
color: #000; | |
transition: color 0.3s linear; | |
border-color: #000; | |
border-width: 2px; | |
} | |
.button-small:hover { | |
color: white; | |
border: 2px solid white; | |
background-color: black; | |
} | |
.button-small { | |
max-width: 100%; | |
max-height: 4em; | |
font-size: 0.8em; | |
margin: auto; | |
display: inline-block; | |
padding: 0.5em; | |
color: #000; | |
transition: color 0.3s linear; | |
border-color: #000; | |
border-width: 2px; | |
} | |
.button:hover { | |
color: white; | |
border: 2px solid white; | |
background-color: black; | |
} | |
.forms { | |
display: flex; | |
margin-top: 5em; | |
} | |
.superhero, | |
.crush { | |
flex: 1; | |
padding: 0em 6em 0em 6em; | |
} | |
.form-control { | |
font-size: 1em; | |
padding: 0.35em; | |
border-radius: 0.3em 0em 0.3em 0.3em; | |
} |
8) Test the two different forms, open console to see the data that is returned upon submitting each form.
9) You should see encrypted information if you click the link that Gaia Storage returns on App.js:134:
Congratulations for making it to the end of this tutorial! You have now successfully implemented Blockstack authentication, connected Blockstack to a public API, and securely transferred data to Gaia Storage.
There is still lots to learn, but you now have the fundamentals to start building decentralized and hybrid apps. Remember you can always dive into the Blockstack documentation or reach out to the community on Slack if you get stuck.
Thank you for following along through all of the tutorials, reach out if you have any questions. Wish you the best in your blockchain endeavors!
Bring your friends and come learn JavaScript in a fun never before seen way! waddlegame.com
Top comments (0)