Currently, everyone can select and insert todos. That is ok to start with but now it's time to secure our API by adding authentication.
Authentication is built-in to Nhost and easy to get started.
Let's first create our user by going to the project's Auth section.
You will use this user to access your todos later on. But first, we will install nhost-js-sdk to help us with authentication.
nhost-js-sdk
is an open-source JavaScript library for interacting with Nhost's auth and storage.
$ npm install nhost-js-sdk
Now, we need to configure and initialize a connection to Nhost. Create a new folder, src/utils
, and create nhost.js
inside it.
import { createClient } from "nhost-js-sdk";
const config = {
baseURL: "https://backend-REPLACE.nhost.app",
};
const { auth, storage } = createClient(config);
export { auth, storage };
Please don't forget to replace https://backend-REPLACE.nhost.app
with your endpoint.
Now it's time to wrap our App
with another provider. The NhostAuthProvider
. This provider indicates if a user is logged in or not in the AuthGate
component that we'll implement soon.
First let's install the @nhost/react-auth
package:
$ npm install @nhost/react-auth@v1.0.5
We'll also pass our auth
object as a prop to both providers, NhostAuthProvider
and NhostApolloProvider
, so that the whole app can share the same auth state.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { NhostApolloProvider } from "@nhost/react-apollo";
import { NhostAuthProvider } from "@nhost/react-auth";
import { auth } from "utils/nhost";
ReactDOM.render(
<React.StrictMode>
<NhostAuthProvider auth={auth}>
<NhostApolloProvider
auth={auth}
gqlEndpoint="https://hasura-REPLACE.nhost.app/v1/graphql"
>
<App />
</NhostApolloProvider>
</NhostAuthProvider>
</React.StrictMode>,
document.getElementById("root")
);
Now it's a good time to recap what has been done so far in this chapter. We started by creating a new user for our Nhost project, followed by installing and configuring nhost-js-sdk
. Finally we wrapped our app with NhostAuthProvider
, passing auth
as a prop so that the same auth object gets used in both Nhost providers.
Now, let's create the login component.
We need to write a login component that handles the form's submission of a user's data and some scaffolding for the app routes.
Create a directory src/components
and create the Login
component with the following content:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { auth } from "utils/nhost";
export function Login(props) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const history = useHistory();
async function handleSubmit(e) {
e.preventDefault();
// login
try {
await auth.login({email, password});
} catch (error) {
alert("error logging in");
console.error(error);
return;
}
// redirect back to `/`
history.push("/");
}
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button>Login</button>
</form>
</div>
);
}
Now let's create two routes. One for /
where we'll have our todos listed, and one for /login
where users can log in.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Login } from "components/login";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { NhostApolloProvider } from "@nhost/react-apollo";
import { NhostAuthProvider } from "@nhost/react-auth";
import { auth } from "utils/nhost";
ReactDOM.render(
<React.StrictMode>
<NhostAuthProvider auth={auth}>
<NhostApolloProvider
auth={auth}
gqlEndpoint="https://hasura-REPLACE.nhost.app/v1/graphql"
>
<Router>
<Switch>
<Route exact path="/login">
<Login />
</Route>
<Route exact path="/">
<App />
</Route>
</Switch>
</Router>
</NhostApolloProvider>
</NhostAuthProvider>
</React.StrictMode>,
document.getElementById("root")
);
Finally, add two links to App.js
. One to /login
and one that will logout the user.
import React, { useState } from "react";
import { gql, useSubscription, useMutation } from "@apollo/client";
import { Link } from "react-router-dom";
import { auth } from "utils/nhost";
const GET_TODOS = gql`
subscription {
todos {
id
created_at
name
completed
}
}
`;
const INSERT_TODO = gql`
mutation($todo: todos_insert_input!) {
insert_todos(objects: [$todo]) {
affected_rows
}
}
`;
function App() {
const { data, loading } = useSubscription(GET_TODOS);
const [insertTodo] = useMutation(INSERT_TODO);
const [todoName, setTodoName] = useState("");
if (loading) {
return <div>Loading</div>;
}
return (
<div>
<Link to="/login">Login</Link>
<div onClick={() => auth.logout()}>Logout</div>
<div>
<form
onSubmit={async (e) => {
[...]
</form>
</div>
{!data ? (
"no data"
) : (
<ul>
{data.todos.map((todo) => {
return <li key={todo.id}>{todo.name}</li>;
})}
</ul>
)}
</div>
);
}
export default App;
Phew, that's a lot of code. Let's stop here and recap what we have.
Both routes (/
and /login
) are still unprotected. We'll fix that soon. When we visit /
we still see all todos. That's fine for now since we allow the public
role to select them.
On the other hand, when we log in, we see No data. And if we log out again, our todos are back. What's going on?
When logged out, the public
role was used, which still had permissions to select todos. When we log in, the user
role was used instead, which doesn't have permission to select todos. Yet!
Things we need to do to secure our app:
/
route so that it is available to logged-in users only.public
role. Only logged-in users can insert and select todos.user_id
to the todos
table so that every todo belongs to a user.Let's now create the AuthGate
component that ensures only authenticated users can access specific routes.
import { Redirect } from "react-router-dom";
import { useAuth } from "@nhost/react-auth";
export function AuthGate({ children }) {
const { signedIn } = useAuth();
if (signedIn === null) {
return <div>Loading...</div>;
}
if (!signedIn) {
return <Redirect to="/login" />;
}
return children;
}
AuthGate
checks whether a user is logged in and redirects to /login
otherwise.
In src/index.js
we add the AuthGate
component around the routes we want only logged-in users to access.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Login } from "components/login";
import { AuthGate } from "components/auth-gate";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { NhostApolloProvider } from "@nhost/react-apollo";
import { NhostAuthProvider } from "@nhost/react-auth";
import { auth } from "utils/nhost";
ReactDOM.render(
<React.StrictMode>
<NhostAuthProvider auth={auth}>
<NhostApolloProvider
auth={auth}
gqlEndpoint="https://hasura-xxx.nhost.app/v1/graphql"
>
<Router>
<Switch>
<Route exact path="/login">
<Login />
</Route>
<AuthGate>
<Route exact path="/">
<App />
</Route>
</AuthGate>
</Switch>
</Router>
</NhostApolloProvider>
</NhostAuthProvider>
</React.StrictMode>,
document.getElementById("root")
);
Now users can only access /
if they are logged-in.
A todo should belong to a user and only be visible to that user. Before we do that all existing todos must be deleted. To delete all existing todos, click Browse Rows for the todos
table, select all todos and click on the trash can icon.
That deleted all todos. Now let's add the new user_id
column.
Click on Modify and then click the button Add a new column. Name the new column user_id
with the type UUID
. Make sure to uncheck Nullable. Click Save.
user_id
refers to the id of the user the todo belongs to.
For our GraphQL API and database to understand that the user_id
refers to the user's id we need to create a Foreign Key.
A Foreign key is a link between two tables
The foreign key links the users
table to the todos
table.
You create the foreign key in the same Modify tab. Scroll down a bit to Foreign Keys and click Add. See the image below of how to set the Foreign Key and click Save.
For Hasura to use the foreign key relation in the GraphQL API, we need to track the foreign key relationship. Go to the DATA tab and click Track all.
We won't use the public
role anymore, so let's remove all permission for that role. See the image below.
Now we'll add permissions for the user
role.
All logged-in users have the
user
role.
First, we'll set the Insert permission.
A user can only insert name
because all other columns will be set automatically. More specifically, user_id
will be set to the id of the user making the request (x-hasura-user-id
) and is configured in the Column presets
section. See the image below.
For Select permission, set a custom check so users can only select todos where user_id
is the same as their user id. In other words: users are only allowed to select their own todos. See the image below.
You have probably already tested your app during this guide, but you have finished you're app. Congratulations!