Thank you for giving me lots of claps! As of this writing, I got almost 300 claps in total.

The code for this tutorial is available here. Or if you want to follow along, you can clone part2 branch here.

Signup

Open up server/src/resolver.js and add the following:

const { User, Team } = require('./models') const JWT_SECRET = process.env.JWT_SECRET function randomChoice(arr) {

return arr[Math.floor(arr.length * Math.random())]

} const avatarColors = [

"D81B60","F06292","F48FB1","FFB74D","FF9800","F57C00","00897B","4DB6AC","80CBC4",

"80DEEA","4DD0E1","00ACC1","9FA8DA","7986CB","3949AB","8E24AA","BA68C8","CE93D8"

] const resolvers = {

...

Mutation: {

...

async signup (_, {id, firstname, lastname, password}) {

const user = await User.findById(id)

const common = {

firstname,

lastname,

name: `${firstname} ${lastname}`,

avatarColor: randomChoice(avatarColors),

password: await bcrypt.hash(password, 10),

status: 'Active'

}

if (user.role === 'Owner') {

const team = await Team.create({

name: `${common.name}'s Team`

})

user.set({

...common,

team: team.id,

jobTitle: 'CEO/Owner/Founder'

})

} else {

user.set(common)

}

await user.save()

const token = jwt.sign({id: user.id, email: user.email}, JWT_SECRET)

return {token, user}

},

},

}

It finds the user data that was created in captureEmail and sets some fields.

As you can see, we randomly pick an avatar color from 10 colors. This is a lazy approach because it’s possible to pick the same color even if you only have a few members. I encourage you to write a better algorithm and share it in a comment. I would be happy to incorporate it in my code.

signup function is used for both Owners and other users. Later we are going to build accounts page where you can invite other users.

If the user is an owner, it also creates a team. Finally it signs the user in and returns the token along with the user object.

Add JWT_SECRET environmental variable to .env and we are ready to test it! Make sure you restart the server.

MONGODB_URI=mongodb://localhost:27017/enamel_tutorial

JWT_SECRET=thisissecret

Open up the playground and signup the user you created last time. You need to go to the database to copy-paste the user id.

If you go check your database, you should see that your team has been created. Notice it’s under folders . That’s because Team is a special type of Folder.

Now the API is working, let’s build a signup form.

client/src/router.js now looks like:

import Vue from 'vue'

import Router from 'vue-router'

import Home from './views/Home.vue'

import Signup from './views/Signup.vue' Vue.use(Router) const router = new Router({

mode: 'history',

routes: [

{

path: '/',

name: 'home',

component: Home,

meta: { title: 'enamel' }

},

{

path: '/signup/:id',

name: 'signup',

component: Signup,

meta: { title: 'Signup - enamel' }

},

]

}) router.afterEach((to, from) => {

document.title = to.meta.title

}) export default router

I added title meta fields so that each route shows a different document title.

Add the signup query to client/src/constants/query.gql :

mutation Signup($id: String!, $firstname: String!, $lastname: String!, $password: String!) {

signup(id: $id, firstname: $firstname, lastname: $lastname, password: $password) {

token

user {

id

email

}

}

}

Here is the signup Vue component. Nothing too crazy here. After signing up the user, it saves the token and user id to localstorage. Then it should take you to workplace, but since we haven’t built yet, it prints the message to the console for now.

// client/src/views/Signup.vue

<template>

<el-container>

<el-header >

</el-header> <el-main>

<div class="container-center">

<div>Welcome to enamel! Finish setting up your account</div> <div v-if="error" class="error">

{{ error }}

</div>

<el-form-item>

<label>First name</label>

<el-input v-model="form.firstname" placeholder="Your first name"></el-input>

<label>Last name</label>

<el-input v-model="form.lastname" placeholder="Your last name"></el-input>

<label>Password</label>

<el-input v-model="form.password" type="password" placeholder="Password"></el-input>

</el-form-item>

<el-form-item>

<el-button type="primary"

</el-form-item>

</el-form> First name Last name Password @click ="signup">Complete </div> </el-main>

</el-container> </template> <script>

import { Signup } from '../constants/query.gql' export default {

data() {

return {

error: false,

form: {

firstname: '',

lastname: '',

password: '',

}

}

},

methods: {

async signup() {

const { firstname, lastname, password } = this.form

if (!(firstname && lastname && password)) {

this.error = 'Please complete the form'

return

}

this.$apollo.mutate({

mutation: Signup,

variables: {

id: this.$route.params.id,

firstname,

lastname,

password

}

}).then(({data: {signup}}) => {

const id = signup.user.id

const token = signup.token

this.saveUserData(id, token) // this.$router.push({name: 'workspace'})

console.log('success!') // For now just print

}).catch((error) => {

this.error = 'Something went wrong'

console.log(error)

})

},

saveUserData (id, token) {

localStorage.setItem('user-id', id)

localStorage.setItem('user-token', token)

this.$root.$data.userId = localStorage.getItem('user-id')

},

}

}

</script> <style scoped> .el-button {

width: 100%;

} .error {

padding-top: 10px;

}

</style>

So how do we get to the signup page? Recall from the last post that after submitting the email form, you should see a message that says “Please check your email”.

In production, I send an email with a signup link. However, you might not want to bother with setting up your email. So I’m going to make the email part of this app a bonus material.

For now, copy-paste your id to the address bar.

If you submit the form, it overrides the existing data and creates another team object. This is obviously not what you want, but it’s suffice for now. (It actually remains this way in production, but hey, you gotta move fast!)

Login

Le’t repeat the same process and create a login functionality. Add this to server/src/resolvers.js :

async login (_, {email, password}) {

const user = await User.findOne({email})

if (!user) {

throw new Error('No user with that email')

}

const valid = await bcrypt.compare(password, user.password)

if (!valid) {

throw new Error('Incorrect password')

}

const token = jwt.sign({id: user.id, email}, JWT_SECRET)

return {token, user}

},

I almost always test a GraphQL API using playground first and then create a frontend. Since this is similar to signup, we are going to skip that and move onto frontend, but I encourage you to try it on playground.

Router:

...

import Login from './views/Login.vue' Vue.use(Router) const router = new Router({

mode: 'history',

routes: [

...

{

path: '/login',

name: 'login',

component: Login,

meta: { title: 'Login - enamel' }

}

]

})

query.gql:

mutation Login($email: String!, $password: String!) {

login(email: $email, password: $password) {

token

user {

id

email

}

}

}

Login.vue:

<template>

<el-container>

<el-header>

</el-header> <el-main>

<div class="container-center">

<h2>Log in</h2>



<div v-if="error" class="error">

{{ error }}

</div>

<el-form-item>

<label>Email</label>

<el-input v-model="form.email" placeholder="Email"></el-input>

<label>Password</label>

<el-input v-model="form.password" type="password" placeholder="Password"></el-input>

</el-form-item>

<el-form-item>

<el-button type="primary"

</el-form-item>

</el-form> Email Password @click .once="login">Log in <div>

<span>Don't have an account?</span>

<router-link :to="{name: 'home'}" class="link">Create an account</router-link>

</div>

</div> </el-main>

</el-container> </template> <script>

import { Login } from '../constants/query.gql' export default {

data() {

return {

error: false,

form: {

email: '',

password: '',

}

}

},

methods: {

async login() {

const { email, password } = this.form

if (email && password) {

this.$apollo.mutate({

mutation: Login,

variables: { email, password }

}).then(async (data) => {

const login = data.data.login

const id = login.user.id

const token = login.token

this.saveUserData(id, token)

// this.$router.push({name: 'workspace'})

console.log('success')

}).catch((error) => {

this.error = 'Invalid email or password'

console.log(error)

})

}

},

saveUserData (id, token) {

localStorage.setItem('user-id', id)

localStorage.setItem('user-token', token)

this.$root.$data.userId = localStorage.getItem('user-id')

},

}

}

</script> <style scoped>

.el-button {

width: 100%;

}

</style>

It should work as you expect:

Bonus Material: Sending Email

For those of you who actually want to setup your email, I will walk through the code.

Initially I used SendGrid. SendGrid has a free plan and an easy API. It was good… until they blocked my account. I contacted the support but got no response so far. The only reason I can think of is that I set the sender as noreply@enamel.tech . I own the domain, but I don’t have the business email account. I thought as long as I own the domain, I can use whatever email address I want.

Anyway, I’m too cheap to buy a business email in this early stage(remember, I have only 1 paying customer), so I settled with nodemailer using my personal email address.

We have already installed nodemailer, so we just need to import it. Here is how you setup nodemailer for gmail:

const nodeMailer = require('nodemailer')

const { welcomeEmail } = require('./emails') const transporter = nodeMailer.createTransport({

host: 'smtp.gmail.com',

port: 465,

secure: true,

auth: {

user: process.env.FROM_EMAIL,

pass: process.env.GMAIL_PASSWORD

}

})

Create a new file server/src/emails.js and add this code:

const url = process.env.CLIENT_URL

const fromEmail = process.env.FROM_EMAIL module.exports.welcomeEmail = function(email, user) {

const text = `

Hi,

Thank you for choosing enamel!

You are just one click away from completing your account registration. Confirm your email:



${url}/signup/${user.id}

` return {

to: `${email}`,

from: {

address: fromEmail,

name: 'enamel'

},

subject: 'Please complete your registration',

text

}

}

We need an environment variable for client url as well as email and password. Make sure you restart the server after change.

Finally, add one line to captureEmail :

async captureEmail (_, {email}) {

const isEmailTaken = await User.findOne({email})

if (isEmailTaken) {

throw new Error('This email is already taken')

}

const user = await User.create({

email,

role: 'Owner',

status: 'Pending'

})

// New code

transporter.sendMail(welcomeEmail(email, user)) return user

},

If you enter your email in the email form(make sure there is no user with the same email), you should get the email from yourself.

Coming Up: Creating Workplace

That’s it for part 3! Check out the github for reference if you need it.

Now that we have built a foundation for this app, we can accelerate from here. I know it’s been a little bit boring so far, so thank you for your patience. I would not structure my tutorial in this manner for beginners. For beginners, it’s best to ignore backend and create UI first so that you quickly get an idea of what the app looks like. But you are not a beginner, right?

In part 4, we are going to create workplace where you can create folders and tasks.

if you liked this post, please give it some claps! It motivates me to write the next part sooner.