Authentication
Baked-in authentication using Nokkio's data layer
In our previous case study for Nokkio Data, we built a simple Todo application that had Lists and Todos. In practice, this wouldn't be very useful as anyone could edit any List or Todo. We'd be better off providing Lists and Todos to individual user accounts.
Nokkio's built-in authentication system fits right in with your existing schema to provide quick authentication without boilerplate or third-party services.
Creating the model
To get started, the project needs a new model to act as the authentication model. Back to schema.js:
/** @type {import('@nokkio/schema').Config} */
module.exports = function ({ defineModel, types }) {
const User = defineModel('User', {
username: types.string().unique(),
password: types.password(),
});
const List = defineModel('List', {
name: types.string(),
});
const Todo = defineModel('Todo', {
text: types.string(),
isComplete: types.bool(false),
});
User.hasMany(List);
List.hasMany(Todo);
return { User, List, Todo };
}
The authentication model has a few requirements:
- A username field that is of type string and is also marked as unique to ensure no two users share the same username. The name of the field can be changed, more on that in the next section.
- A password field that is of type: password. This type takes care of ensuring the password is properly encrypted.
Notice also that there is now a relationship between users and lists, which will allow you to show users only the lists they have created. Don't forget to return the User model at the end of the function along with the List and Todo models that were set up previously.
Marking the model
Once the model is defined, the last step is to let Nokkio know that this is the model to use for authentication.
/** @type {import('@nokkio/schema').Config} */
module.exports = function ({ defineModel, types }) {
const User = defineModel('User', {
username: types.string().unique(),
password: types.password(),
});
const List = defineModel('List', {
name: types.string(),
});
const Todo = defineModel('Todo', {
text: types.string(),
isComplete: types.bool(false),
});
User.hasMany(List);
List.hasMany(Todo);
User.actAsAuth();
return { User, List, Todo };
}
To use field names other than username and password, use a map to specify how your field names relate to the expected field names:
/** @type {import('@nokkio/schema').Config} */
module.exports = function ({ defineModel, types }) {
const User = defineModel('User', {
email: types.string().unique(),
pwd: types.password(),
});
const List = defineModel('List', {
name: types.string(),
});
const Todo = defineModel('Todo', {
text: types.string(),
isComplete: types.bool(false),
});
User.hasMany(List);
List.hasMany(Todo);
User.actAsAuth({
username: 'email',
password: 'pwd',
});
return { User, List, Todo };
}
The useAuth hook
Once authentication is setup, the useAuth hook can be used anywhere in your application where you need access to the current user or to detect if a user is authenticated in the first place. The following change updates the Todo application's home screen to require authentication and to fetch only the authenticated user's lists.
import { useAuth } from '@nokkio/auth';
import { useUserLists } from '@nokkio/magic';
export default function IndexPage() {
const { user } = useAuth();
const { lists } = useUserLists(user.id, {
withCounts: ['todos'],
});
return (
<ul>
{lists.map((list) => (
<li key={list.id}>
{list.name} ({list.todosCount} todos)
</li>
))}
</ul>
);
}
Three steps to highlight here:
- We've renamed the file from index.js to index.auth.js. This instructs Nokkio's router to only serve this page if the user is logged in. Users not logged in will be redirected to /login.
- We are now importing the useAuth hook from @nokkio/auth and using it to reference the current user. Since this page's filename is suffixed with auth.js, we know that a user will always be present at this point. Note: The name of this variable will depend on your model name. Since we chose User, it is returned as user here. If we named our model Account, useAuth would return account.
- Finally, instead of useLists, we've changed to theuseUserLists hook. This ensures that the lists that we load on the page belong to the logged-in user.
Detecting if the user is logged in
You can also use the useAuth hook in contexts where the logged in state of the user is unknown. For example, a header that shows the username when logged in, or a link to the login page if not:
import { useAuth } from '@nokkio/auth';
import { Link } from '@nokkio/router';
export default function Header() {
const { isAuthenticated, user } = useAuth();
return (
<header>
<h1>My App</h1>
{isAuthenticated ? (
<div>{user.username}</div>
) : (
<Link to="/login">Login</Link>
)}
</header>
);
}
Logging a user in
If someone visits your application and is not logged in, they'll be redirected to /login. The auth package provides an easy to use form to handle the login:
import { useLoginForm } from '@nokkio/auth';
import { Input } from '@nokkio/forms';
export default function LoginPage() {
const { Form, isProcessing } = useLoginForm();
return (
<Form>
<Input type="text" name="username" />
<Input type="password" name="password" />
<button disabled={isProcessing}>Login</button>
</Form>
);
}
The Form component does all the heavy lifting for you–sending the credentials to the backend to see if they are valid, then redirecting to the page the user was originally trying to access.
Registering a new user
Nokkio also provides a hook to simplify the process of registering a user. The following code is all you need to register a user, log them in, and redirect to the main page of your application.
import { useRegisterForm } from '@nokkio/auth';
import { Input } from '@nokkio/forms';
export default function RegisterPage() {
const { Form, isProcessing } = useRegisterForm();
return (
<Form>
<Input type="text" name="username" />
<Input type="password" name="password" />
<Input name="confirm" type="password" />
<button disabled={isProcessing}>Register</button>
</Form>
);
}
Logout
Another common task when using authentication is showing the username and providing a link for the user to log out with. We can again leverage useAuth, for example in a common header:
import { useAuth } from '@nokkio/auth';
export default function Header() {
const { isAuthenticated, user, logout } = useAuth();
return (
<header>
<h1>My App</h1>
{isAuthenticated && (
<div>
{user.username}
<button onClick={logout}>Log out</button>
</div>
)}
</header>
);
}
First, we use isAuthenticated to know if a user is authenticated or not (you can skip this if you know the component will only be used on pages with the .auth.js suffix). Then, we show the username and provide a button whose onClick calls the logout function from useAuth. That's it!
What's next?
Need do to something a custom? Like access an API with secrets you don't want to spill on the client? Nokkio Endpoints are simple, serverless functions to help you do just that. Learn more about Endpoints