7: Adding User Accounts
7.1: Password Authentication
Meteor already comes with a basic authentication and account management system out of the box, so you only need to add the accounts-password
to enable username and password authentication:
meteor add accounts-password
There are many more authentication methods supported. You can read more about the accounts system here.
We also recommend you to install bcrypt
node module, otherwise you are going to see a warning saying that you are using pure-Javascript implementation of it.
meteor npm install --save bcrypt
You should always use
meteor npm
instead of onlynpm
so you always use thenpm
version pinned by Meteor, this helps you to avoid problems due to different versions of npm installing different modules.
7.2: Create User Account
Now you can create a default user for our app, we are going to use meteorite
as username, we just create a new user on server startup if we didn’t find it in the database.
server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/api/TasksCollection';
..
const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';
Meteor.startup(() => {
if (!Accounts.findUserByUsername(SEED_USERNAME)) {
Accounts.createUser({
username: SEED_USERNAME,
password: SEED_PASSWORD,
});
}
..
});
You should not see anything different in your app UI yet.
7.3: Login Form
You need to provide a way for the users to input the credentials and authenticate, for that we need a form.
Create a new file called LoginForm.vue
and add a form to it. You should use Meteor.loginWithPassword(username, password);
to authenticate your user with the provided inputs.
imports/ui/components/LoginForm.vue
<template>
<form class="login-form" @submit.prevent="handleSubmit">
<div>
<label for="username">Username</label>
<input
id="username"
name="username"
type="text"
placeholder="Username"
required
v-model="username"
/>
</div>
<div>
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
placeholder="Password"
required
v-model="password"
/>
</div>
<div>
<button type="submit">Log In</button>
</div>
</form>
</template>
<script>
import { Meteor } from 'meteor/meteor';
export default {
name: "LoginForm",
data() {
return {
username: "",
password: ""
};
},
methods: {
handleSubmit(event) {
Meteor.loginWithPassword(this.username, this.password);
}
},
}
</script>
Ok, now you have a form, let’s use it.
7.4: Require Authentication
Our app should only allow an authenticated user to access its task management features.
We can accomplish that returning the LoginForm
component when we don’t have an authenticated user, otherwise we return the form, filter, and list component.
Modify the data container to get information about the currently logged in user:
imports/ui/App.vue
..
incompleteCount() {
return TasksCollection.find({ checked: { $ne: true } }).count();
},
currentUser() {
return Meteor.user();
}
..
Then, we can wrap our user functionality in a <template>
tag and add in the v-if
directive to conditionally render our user functionality when there is a logged in user:
imports/ui/App.vue
..
<div class="main">
<template v-if="currentUser">
<TaskForm />
<div class="filter">
<button
v-model="hideCompleted"
@click="toggleHideCompleted"
>
<span v-if="hideCompleted">Show All</span>
<span v-else>Hide Completed Tasks</span>
</button>
</div>
<ul class="tasks">
<Task
class="task"
v-for="task in tasks"
v-bind:key="task._id"
v-bind:task="task"
/>
</ul>
</template>
<template v-else>
<LoginForm />
</template>
</div>
..
7.5: Login Form style
Ok, let’s style the login form now:
Wrap your pairs of label and input in div
s so it will easier to control it on CSS.
client/main.css
.login-form {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
}
.login-form > div {
margin: 8px;
}
.login-form > div > label {
font-weight: bold;
}
.login-form > div > input {
flex-grow: 1;
box-sizing: border-box;
padding: 10px 6px;
background: transparent;
border: 1px solid #aaa;
width: 100%;
font-size: 1em;
margin-right: 16px;
margin-top: 4px;
}
.login-form > div > input:focus {
outline: 0;
}
.login-form > div > button {
background-color: #62807e;
}
Now your login form should be centralized and beautiful.
7.6: Server startup
Every task should have an owner from now on. So go to your database, as you learn before, and remove all the tasks from there:
db.tasks.remove({});
Change your server/main.js
to add the seed tasks using your meteoriote
user as owner.
Make sure you restart the server after this change so Meteor.startup
block will run again. This is probably going to happen automatically any way as you are going to make changes in the server side code.
server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/api/TasksCollection';
const insertTask = (taskText, user) =>
TasksCollection.insert({
text: taskText,
userId: user._id,
createdAt: new Date(),
});
const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';
Meteor.startup(() => {
if (!Accounts.findUserByUsername(SEED_USERNAME)) {
Accounts.createUser({
username: SEED_USERNAME,
password: SEED_PASSWORD,
});
}
const user = Accounts.findUserByUsername(SEED_USERNAME);
if (TasksCollection.find().count() === 0) {
[
'First Task',
'Second Task',
'Third Task',
'Fourth Task',
'Fifth Task',
'Sixth Task',
'Seventh Task',
].forEach(taskText => insertTask(taskText, user));
}
});
See that we are using a new field called userId
with our user _id
field, we are also setting createdAt
field.
7.7: Task owner
Now you can filter the tasks in the UI by the authenticated user. Use the user _id
to add the field userId
to your Mongo selector when getting the tasks from Mini Mongo.
imports/ui/App.vue
..
meteor: {
tasks() {
if (!this.currentUser) {
return [];
}
const hideCompletedFilter = { isChecked: { $ne: true } };
const userFilter = this.currentUser ? { userId: this.currentUser._id } : {};
const pendingOnlyFilter = { ...hideCompletedFilter, ...userFilter };
return TasksCollection.find(
this.hideCompleted ? pendingOnlyFilter : userFilter,
{
sort: { createdAt: -1 },
}
).fetch();
},
incompleteCount() {
return TasksCollection.find({ isChecked: { $ne: true }, userId: this.currentUser._id }).count();
},
currentUser() {
return Meteor.user();
}
}
..
Also update the insert
call to include the field userId
in the TaskForm
. You should pass the user from the App
component to the TaskForm
.
imports/ui/components/TaskForm.vue
..
methods: {
handleSubmit(event) {
if (this.newTask.length === 0) return;
const user = Meteor.user()
TasksCollection.insert({
text: this.newTask.trim(),
createdAt: new Date(), // current time
userId: user._id
});
// Clear form
this.newTask = "";
}
},
..
7.8: Log out
We also can better organize our tasks by showing the username of the owner below our app bar. You can include a new div
right after our template
start tag.
On this you can add an onClick
handler to logout the user as well. It is very straightforward, just call Meteor.logout()
on it.
imports/ui/App.vue
..
const logout = () => Meteor.logout();
return (
..
<template v-if="currentUser">
<div class="user" v-on:click="logout">
{{currentUser.username}} 🚪
</div>
<TaskForm />
..
methods: {
toggleHideCompleted() {
this.hideCompleted = !this.hideCompleted;
},
logout() {
Meteor.logout();
}
},
..
Remember to style your username as well.
client/main.css
.user {
display: flex;
cursor: pointer;
align-self: flex-end;
margin: 8px 16px 0;
font-weight: bold;
}
Phew! You have done quite a lot in this step. Authenticated the user, set the user in the tasks and provided a way for the user to log out.
Your app should look like this:
Review: you can check how your code should be in the end of this step here
In the next step we are going to start using Methods to only change the data after checking some conditions.