Building Full Stack Application

A step by step guide to building a Full Stack Application using Qusar, Vue, VueX, Axios, Express, Node JS, MySQL.

1. Project Set-up
1.1. Installing Quasar CLI

Make sure you have Node JS version 8 or above and NPM version 5 or above installed on your machine. You will need to have node and npm installed. If you do not, you can do so here:https://nodejs.org/en/

You will alson need Quasar CLI. To install Quasar CLI globally type:

npm install -g @quasar/cli

1.2. Creating GitHub repository

Create a new GitHub repo and copy its address. Then go to your projects directory at command prompt and clone your new repo. This creates your new project directory locally, eg.

git clone https://github.com/CriptoGirl/myFullStackApp.git

Create .gitignore file in the project directory as following:

*node_modules/

1.3. Seting up Client Front End
Initiate the client with Quasar CLI

At command prompt go to your project directory and type:

quasar create client

Choose vuex, axios and npm options using the space and arrow keys.

This creates a client folder in your project directory with a store subdirectory.

Change the Client Server

If desired change the Client Server. Open quasar.conf.js file and search for port 8080 line and change the port number.

Start the Client Server

At command prompt, go to the client folder and type the following command to start the Client Server in dev mode:

quasar dev

Test

Test by typing http://localhost:8080/ in the browser.

Commit to GitHub

At command prompt go to the project directory and execute following commands to commit your local code to GitHub:

git add .
git commit -m "1st commit client code"
git push

1.4. Seting up Back End Server
Create Server Directory

Create a server folder in your project directory.

Initilise node.js environment

Go to this directory and create a new node.js package by running the following command at the command prompt:

npm init

Install Express and other libraries

Express is a lightweight web application framework for Node.js, which provides a robust set of features for writing web apps. These features include route handling, template engine integration and a middleware framework. Type following command at the command line:

npm install --save express body-parser cors morgan

--save option, adds these libraries to the dependencies section of the package.json file.

Install nodemon

nodemon is a convenience tool. It will watch the files in the directory it was started in, and if it detects any changes, it will automatically restart Node application. Type the following at the command prompt:

npm install --save-dev nodemon

Creating the Basic Node js Application

In your application folder create server.js file, then add the following code to it:

const app = require('./startup/app');

const server = app.listen(process.env.PORT || 8081, () => {
  console.log(`Express is running on port ${server.address().port}`);
});

Create startup folder under the server folder and add app.js file to it containing the following code:

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const morgan = require('morgan');

const app = express();
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(cors());

// routes

module.exports = app;

Start the Server

At command prompt, go to the server folder and type the following command to start the Server:

nodemon

This should start the Server.

Add Routes

Create a routes folder under the server folder. Add home.js, saveData.js and searchData.js files to the routes folder.

Add the following code to the home.js file:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  //res.send('Home page');
  res.send( { message: 'Home page'} );
});

module.exports = router;

Add the same code to the other files replacing the page name.

Add routes code to the app.js file as follows:

// routes
const home = require('../routes/home');
const saveData = require('../routes/saveData');
const search = require('../routes/search');

app.use('/', home);
app.use('/home', home);
app.use('/saveData', saveData);
app.use('/search', search);

Test navigation in the browser.

Test GET request using Postman.

Commit to GitHub

At command prompt go to the project directory and execute following commands to commit your local code to GitHub:

git add .
git commit -m "server code"
git push

2. Client Side Code
2.1. Layout
Rename MyLayout file

Rename MyLayout.vue file in the client/src/layouts folder as Layout.vue.

Update routes.js file in the client/src/router folder, replacing reference to MyLayout.vue with Layout.vue.


Change application name and home page

Update Layout.vue file in the client/src/layouts folder, changing application name.

Update Index.vue file in the client/src/pages folder, removing Quasar Logo and adding following code in its place:

<p>Welcome to My Full Stack App!</p>


Change menu position (optional)

Update Layout.vue file in the client/src/layouts folder, replacing:

<q-layout view="lHh Lpr lFf">

with:

<q-layout view="hHh lpR fFf">

Add footer (optional)

Add footer to the Layout.vue file in the client/src/layouts folder:

<q-footer>
  <div class="q-pa-sm"> Created by Whistling Frogs </div>
</q-footer>

2.2. Adding Pages
Add new pages

Create SaveData.vue and Search.vue files in the client/src/pages directory as a copy of the client/src/pages/Index.vue file.

Replace <p>Welcome to My Full Stack App!</p> with <p>Save Data page</p> and <p>Search page</p> respectively.


Add new routes

Add new routes to the routes.js file in the client/src/router folder. Your code should look like this:

children: [
  { path: '', component: () => import('pages/Index.vue') },
  { path: '/saveData', component: () => import('pages/SaveData.vue') },
  { path: '/search', component: () => import('pages/Search.vue') }
]

Add new menu options

Add new menu options to the Layout.vue file in the client/src/layouts folder as following:

<q-item clickable exact to="/">
  <q-item-section avatar>
    <q-icon name="home" />
  </q-item-section>
  <q-item-section>
    <q-item-label>Home</q-item-label>
    <q-item-label caption>Home page</q-item-label>
  </q-item-section>
</q-item>

<q-item clickable exact to="/saveData">
  <q-item-section avatar>
    <q-icon name="save" />
  </q-item-section>
  <q-item-section>
    <q-item-label>Save Data</q-item-label>
    <q-item-label caption>Save Data page</q-item-label>
  </q-item-section>
</q-item>

<q-item clickable exact to="/search">
  <q-item-section avatar>
    <q-icon name="search" />
  </q-item-section>
  <q-item-section>
    <q-item-label>Search</q-item-label>
    <q-item-label caption>Search page</q-item-label>
  </q-item-section>
</q-item>

<hr/>

2.3. Data Store
Add new data store

Create store-myData.js file in the client/src/store folder as following:

const state = {
  myData_view: [
    { name: 'code', required: true, label: 'Code', align: 'left', field: 'code' },
    { name: 'name', label: 'Name', field: 'name', align: 'left' },
    { name: 'description', label: 'Description', field: 'description', align: 'left' }
  ],
  myData: [
    { code: 'CD1', name: 'First', description: 'First Data Item', status: 'current' },
    { code: 'CD2', name: 'Second', description: 'Second Data Item', status: 'current' },
    { code: 'CD3', name: 'Third', description: 'Third Data Item', status: 'current' }
  ]
}
const mutations = { }
const actions = { }
const getters = {
  myData: (state) => { return state.myData },
  myData_view: (state) => { return state.myData_view }
}
export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

Update index.js file in the client/src/store folder to contain the following code:

import Vue from 'vue'
import Vuex from 'vuex'
import myData from './store-myData'
Vue.use(Vuex)
export default function () {
  const Store = new Vuex.Store({
    modules: {
      myData
    },
    strict: process.env.DEV
  })
  return Store
}

2.4. Display data from the store

Replace content of the Search.vue file in the client/src/pages folder with the following code:

<template>
  <q-page >
    <div class="flex flex-center q-pa-md">
      <q-table
        title="My Data"
        :data=' myData '
        :columns=' myData_view '
        row-key="cd"
      >
      </q-table>
    </div>
  </q-page>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters('myData', ['myData_view', 'myData'])
  }
}
</script>

2.5. SaveData.vue Page
Add form to the page

Add form to the SaveData.vue file in the client/src/pages folder as following:

<template>
  <q-page class="flex flex-center q-pa-md">
    <q-card>
      <q-card-section>
        <div class="text-h6">Save Data Page</div>
      </q-card-section>
      <q-card-section>
        <q-card>
          <q-form @submit.prevent.stop='submitForm' >

          </q-form >
        </q-card>
      </q-card-section>
    </q-card>
  </q-page>
</template>

Add the following fields and buttons to the form:

<q-form @submit.prevent.stop='submitForm' >
  <q-card-section>
    <q-input outlined dense required class='q-ma-sm' label="Code"
      type='text' maxlength="4" v-model='myDataItem.code'/>
    <q-input outlined dense required class='q-ma-sm' label="Name"
      type='text' maxlength="24" v-model='myDataItem.name'/>
    <q-input outlined dense required class='q-ma-sm' label="Description"
      type='text' v-model='myDataItem.description' />
    <q-input outlined dense required class='q-ma-sm' label="Status"
      type='text' maxlength="12" v-model='myDataItem.status' />
  </q-card-section>
  <q-card-actions align="right">
    <q-btn color="primary" dense label="Save" type='submit' class='q-mb-sm'/>
  </q-card-actions>
</q-form >

Add data and methods to script section of the SaveData.vue page

Add following code to the script section

export default {
  name: 'SaveDataPage',
  data () {
    return {
      myDataItem: {
        code: '',
        name: '',
        description: '',
        status: 'Current'
      }
    }
  },
  methods: {
    submitForm () {
      console.log('submit form')
    }
  }
}

Test form.

2.6. Vuex mutations: adding data to data store
Add action and mutation to data store

Update the store-myData.js file in the client/src/store folder as following:

const mutations = {
  addMyDataItem (state, newDataItem) {
    state.myData.push(newDataItem)
  }
}
const actions = {
  addMyDataItem ({ commit }, newDataItem) {
    commit('addMyDataItem', newDataItem)
  }
}

Call addMyDataItem action from the SaveData.vue page

Import actions from the store at the top of the script section:

import { mapActions } from 'vuex'

update methods section as follows:

methods: {
  ...mapActions('myData', ['addMyDataItem']),
  submitForm () {
    this.addMyDataItem(this.myDataItem)
  }
}

Test saving the data. You should see it on the Search Page.

3. Integration
3.1. Add POST route to the server

Open the saveData.js file in the server/routes folder and add the following POST route:

router.post('/', (req, res) => {
  console.log({ message: `Data received by the server. Code: ${req.body.code} Name: ${req.body.name}` })
  res.send({ message: `Data received by the server. Code: ${req.body.code} Name: ${req.body.name}` })
})

Test with Postman using POST: http://localhost:8081/saveData with a raw JSON body:

{
  "code": "001",
  "name": "001 - name",
  "description": "001 - description"
}

3.2. Client Side integration - axios
Client: Axios

If you have not selected Axios when setting up Quasar client application, you can install it at the command prompt, go to the clientdirectory and run:

npm install --save axios

Client: Services

Create services folder under client/src directory.

Then, create the api.js file in the client/src/services folder as follows:

import axios from 'axios'
export default () => {
  return axios.create({
    baseURL: process.env.FSA_SERVER_URL
  })
}

Add env object containing SERVER_URL env variable to the build section of the quasar.conf.js file in client directory:

build: {
  env: ctx.dev
    ? { // so on dev we'll have
      SERVER_URL: JSON.stringify(`http://localhost:8088/`)
    }
    : { // and on build (production): NSAPI: JSON.stringify('https://prod.'+ process.env.FSA_SERVER_URL)
      SERVER_URL: JSON.stringify(process.env.FSA_SERVER_URL)
    },

3.3. Client Side integration - saveDataService

Create the saveDataService.js file in the client/src/services folder as follows:

import api from './api'
export default {
  saveData (myDataItem) {
    return api().post('saveData', myDataItem)
  }
}

Call saveDataService from the SaveData Page

Import saveDataService inot the script section of the client/src/pages/SaveData.vue page as follows:

import saveDataService from '../services/saveDataService'

Then replace the submitForm function with following code:

async submitForm () {
  try {
    const response = await saveDataService.saveData(this.myDataItem)
    console.log(response.data)
    if (response.data) {
      this.addMyDataItem(this.myDataItem)
      console.log('Data added')
    }
  } catch (error) {
    const errorMessage = error.response.data.error
    console.log(errorMessage)
  }
}

Test

Test Saving the data from your application front end, you should see the data being received by the Server and displayed in the Server Console as well as the message from the Server being displayed at the Client Console.

4. Database
4.0 Deploying SQLite for Obyte projects

On a server go to to your project's /server directory and run the following command:

node db_import.js

This will create SQLite obyte db and your custom tables (if any).

4.1. MYSQL & MYSQL Workbench Set-up
Download MYSQL

Go to mysql.com and download mysql and MYSQL workbench. Make a note of root password.

Add PATH and login

add MYSQLserver/bin directory to your PATH.

Login at the command prompt as root:

mysql -u root -p

Create admin user

If you have not set-up db Admin user before, add new db admin user at command prompt as follows:

mysql> CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'mypassword';

To see all db users, type:

mysql> SELECT user, host FROM mysql.user;

Grant new user ALL privileges to your database:

mysql> GRANT ALL PRIVILEGES ON myfullstackapp.* TO 'myuser'@'localhost';

Or to create admin user:

mysql> GRANT ALL PRIVILEGES ON *.* TO 'adminuser'@'localhost';

??? Then:

mysql> FLUSH PRIVILEGES

To see user privileges:

mysql> SHOW GRANTS FOR 'myuser'@'localhost';

Exit by typing:

mysql> exit

4.2. Create database

Login as a new admin user:

mysql -u myuser -p

Create new database:

mysql> CREATE DATABASE myfullstackapp;

To see all databases:

mysql> SHOW DATABASES;

To use the new database:

mysql> USE myfullstackapp;

Create table

Create new table:

mysql> CREATE TABLE mydata(
        code CHAR(3),
        name VARCHAR(50),
        description VARCHAR(255),
        status VARCHAR(12),
        PRIMARY KEY(code)
        );

You can also specify other field types, eg.:

id INT AUTO_INCREMENT,
my_flag TINYINT(1),
created_on DATETIME,

To see all tables, type:

mysql> SHOW TABLES;

To delete table:

mysql> DROP TABLE mydata;

To delete database:

mysql> DROP DATABASE myfullstackapp;

4.3. MYSQL Workbench
5. Integrate your Database with your Node Server
5.1. Connect to db from the Server

In the server folder install mysql library:

npm install --save mysql

Create the db.js file in the server/startup folder as follows:

const mysql = require('mysql');

const db = mysql.createConnection({
  host: 'localhost',
  user: 'myuser',
  password: process.env.MYSQL_PW || 'mypassword',
  database: 'myfullstackapp'
});

db.connect();

module.exports = db;

5.2. Read from the db table

Update the server/routes/search.js file as follows:

const express = require('express');
const router = express.Router();
const db = require('../startup/db.js');

router.get('/', (req, res) => {
  const sql = 'SELECT * FROM mydata';
  db.query(sql, (err, result) => {
    if (err) throw err;
    res.send( result );
  })
});

module.exports = router;

Test in the browser or using Postman.

5.3. Write to the database table.

Update the server/routes/saveData.js file as follows:

const express = require('express');
const router = express.Router();
const db = require('../startup/db.js');

router.post('/', (req, res) => {
  const sql = `INSERT INTO mydata (code, name, description, status) VALUES ('${req.body.code}', '${req.body.name}', '${req.body.description}', 'current')`
  db.query(sql, (err, result) => {
    if (err) {
      console.log(err);
      res.send( err);
    } else {
      console.log(result);
      res.send( result );
    }
  })
})
module.exports = router;

Test with the Postman tool and from the application Front End.

5.4. Update client to display status of the db insert.
6. Update Vuex State from the database
6.1. Create readDataService

Create the readDataService.js file in the client/src/services directory as follows:

import api from './api'
export default {
  readData () {
    return api().get('search')
  }
}

6.2. Add action and mutation to the Vuex store

Update the store-myData.js file in the client/src/store folder. First add a new action:

async loadMyData ({ commit }) {
  try {
    const response = await readDataService.readData()
    if (response.data) {
      const myData = response.data
      commit('loadMyData', myData)
    }
  } catch (error) {
    console.log(error.response.data.error)
  }
}

Then add a new mutation:

loadMyData (state, myData) {
  state.myData = []
  state.myData = myData
}

In the state section, replace:

myData: [
  { code: 'CD1', name: 'First', description: 'First Data Item', status: 'current' },
  { code: 'CD2', name: 'Second', description: 'Second Data Item', status: 'current' },
  { code: 'CD3', name: 'Third', description: 'Third Data Item', status: 'current' }
]

with:

myData: []

Finally, import readDataService:

import readDataService from '../services/readDataService'

6.3. Make changes to the Search.vue page

Make a number of changes to the server/routes/search.js file. First replce:

import { mapGetters } from 'vuex'

with:

import { mapGetters, mapActions } from 'vuex'

Then, add methods and mounted sections as follows:

methods: {
  ...mapActions('myData', ['loadMyData'])
},
mounted () {
  this.loadMyData()
},

Test.

7. User Authentication: Firebase set-up and Client End
7.1. Login and Register pages

Create the Auth.vue file in the client/src/pages directory as follows:

<template>
  <q-page class="flex flex-center q-pa-md">
    <q-card>
      <q-card-section>
        <div class="text-h6">{{tab}} Page</div>
      </q-card-section>
      <q-card-section>
        <q-card>
          <q-tabs v-model="tab"
            align="justify" dense narrow-indicator
            class="q-mb-sm text-grey" active-color="primary"
          >
            <q-tab name="Login" label="Login" />
            <q-tab name="Register" label="Register" />
          </q-tabs>
          <q-tab-panel name="Login" v-if=" tab=='Login' ">
            <p>Login Form</p>
          </q-tab-panel>
          <q-tab-panel name="Register" v-if=" tab=='Register' ">
            <p>Register Form</p>
          </q-tab-panel>
        </q-card>
      </q-card-section>
    </q-card>
  </q-page>
</template>

<script>
export default {
  name: 'AuthPage',
  data () {
    return {
      tab: 'Login'
    }
  }
}
</script>

Update routes.js file in the client/src/router directory, adding:

{ path: '/auth', component: () => import('pages/Auth.vue') }

Test in a browser by typing:

http://localhost:8080/#/auth

Add Login button to the client/src/layout/Layout.vuefile, replacing:

<div>Quasar v{{ $q.version }}</div>

with:

<q-btn
  flat dense
  to='/auth'
  label="Login"
  icon-right="account_circle"
/>

Test.

7.2. Auth Component to Login and Register

Create the Auth folder under the client/src/components directory and add AuthForm.vue file to it containing the following code:

import { Notify } from 'quasar'
<template>
  <q-form @submit.prevent='submitForm' ref="registerForm">
    <div class='row q-mt-md'>
      <q-input
        outlined dense clearable type='email' bottom-slots lazy-rules label="Email *"
        v-model=user.email ref='email'
        :rules="[
          val => !!val || 'Email is required',
          val => validateEmailAddress(val) || 'Invalid email address'
        ]"
      />
    </div>
    <div class='row q-mt-md'>
      <q-input
        outlined dense clearable type='password' bottom-slots lazy-rules label="Password *"
        v-model=user.password ref='password'
        :rules="[
          val => !!val || 'Password is required',
          val => val.length >= 6 || 'Password must be at least 6 characters'
        ]"
      />
    </div>
    <q-card-actions align="right" >
      <q-btn push color="primary" dense :ripple="{ center: true }" :label=tab type='submit' />
    </q-card-actions>
  </q-form>
</template>

<script>
export default {
  name: 'AuthForm',
  props: ['tab'],
  data () {
    return {
      user: {
        email: '',
        password: '',
        status: 'current'
      }
    }
  },
  methods: {
    validateEmailAddress (email) {
      var re = /^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
      return re.test(String(email).toLowerCase())
    },
    submitForm () {
      this.$refs.email.validate()
      this.$refs.password.validate()
      if (!this.$refs.email.hasError && !this.$refs.password.hasError) {
        if (this.tab === 'Register') this.add()
        if (this.tab === 'Login') this.login()
      }
    },
    add () {
      this.$q.notify({
        color: 'green-4', textColor: 'white', icon: 'cloud_done', message: 'User Registered'
      })
    },
    login () {
      this.$q.notify({
        color: 'green-4', textColor: 'white', icon: 'cloud_done', message: 'User Logged In'
      })
    }
  }
}
</script>

Import AuthForm.vue component into the client/src/pages/Auth.vue page:

components: {
  'my-auth-form': require('components/Auth/AuthForm.vue').default
}

Replace

<p>Register Form</p>

and

<p>Login Form</p>

with:

<my-auth-form :tab=tab />

Test, including validation.

7.3. Authenticate with Google's Firebase
Create a Firebase Project

In your browser go to firebase.google.com click Get Started. Next Add Project provide your project name (e.g. My Full Stack App) and accept defaults.

Setup Firebase Authentication

Click on Authentication option from the Firebase menu. Then click on Set up sign-in method and select email/password authentication method and Enable it.

Add Firebase users

Click on Users option and add a user for testing.

Add Firebase to your app using BOOT file

Boot file runs code before the app starts. Use Quasar CLI to generate a new boot file.

At command prompt go to the client folder and type:

quasar new boot firebase

This will create a new boot file called firebase.js in the client/src/boot folder.

Update the boot section of the client/quasar.conf.js file:

boot: [
  'axios',
  'firebase'
],

Open client/src/boot/firebase.js file and replace its content with following:

var firebase = require('firebase/app')
require('firebase/auth')

Install firebase. At the terminal go to the client directory and run:

npm install --save firebase

Restart Quasar server:

quasar dev

In your browser go to console.firebase.google.com select your project and click on the Add Firebase to your app web icon. Specify app's nickname and click Register app button. Copy firebaseConfig object, e.g.

var firebaseConfig = {
  apiKey: "AIzaSyBL_eSmsmYZGFF-tWnnCLiHvH7WVOSB8HU",
  authDomain: "my-full-stack-app.firebaseapp.com",
  databaseURL: "https://my-full-stack-app.firebaseio.com",
  projectId: "my-full-stack-app",
  storageBucket: "my-full-stack-app.appspot.com",
  messagingSenderId: "470194054235",
  appId: "1:470194054235:web:113eb439efed41ad0bcb7e",
  measurementId: "G-G0L975FF7F"
}

Click on the Continue to the console button.

Update client/src/boot/firebase.js file, adding the firebaseConfig variable, connecting to Firebase as shown in the following example:

var firebaseConfig = {
  apiKey: 'AIzaSyBL_eSmsmYZGFF-tWnnCLiHvH7WVOSB8HU',
  authDomain: 'my-full-stack-app.firebaseapp.com',
  databaseURL: 'https://my-full-stack-app.firebaseio.com',
  projectId: 'my-full-stack-app',
  storageBucket: 'my-full-stack-app.appspot.com',
  messagingSenderId: '470194054235',
  appId: '1:470194054235:web:113eb439efed41ad0bcb7e',
  measurementId: 'G-G0L975FF7F'
}

let firebaseApp = firebase.initializeApp(firebaseConfig)
let firebaseAuth = firebaseApp.auth()

export { firebaseAuth }

Replacing "" with ''

For more info go to https://firebase.google.com/docs/web/setup

7.4. Add Auth Vuex store
Add message function to display errors

Create new folder functions in the client/src directory.

In this folder create errorPopUp.js file as follows:

import { Dialog, Loading } from 'quasar'

export function errorPopUp (errorMessage) {
  Loading.hide()
  Dialog.create({
    title: 'Error',
    message: errorMessage
  })
}

Add message function to show info messages

Create the client/src/functions/infoPopUp.js file as follows:

import { Dialog, Loading } from 'quasar'
export function infoPopUp (message) {
  Loading.hide()
  Dialog.create({
    title: 'Info',
    message: message
  })
}

Add Vuex store

Add the store-auth.js file to the client/src/store folder as follows:

import { firebaseAuth } from 'boot/firebase.js'
import { errorPopUp } from 'src/functions/errorPopUp'
import { infoPopUp } from 'src/functions/infoPopUp'

const state = {
  loggedIn: false,
  authToken: null
}

const mutations = {
  setLoggedIn (state, value) {
    state.loggedIn = value
  },
  setToken (state, value) {
    state.authToken = value
  }
}

const actions = {
  registerUser ({ commit }, user) {
    firebaseAuth.createUserWithEmailAndPassword(user.email, user.password)
      .then(response => {
        infoPopUp('User registered')
      })
      .catch(error => {
        errorPopUp(error.message)
      })
  },
  loginUser ({ commit }, user) {
    firebaseAuth.signInWithEmailAndPassword(user.email, user.password)
      .then(response => {
        console.log('user signed in')
      })
      .catch(error => {
        errorPopUp(error.message)
      })
  },
  logoutUser () {
    firebaseAuth.signOut()
  },
  authStateChange ({ commit, dispatch }) {
    firebaseAuth.onAuthStateChanged(user => {
      if (user) {
        user.getIdToken().then(function (idToken) {
          commit('setLoggedIn', true)
          commit('setToken', idToken)
          dispatch('goToHomePage')
        })
      } else {
        commit('setLoggedIn', false)
        commit('setToken', null)
        this.$router.replace('/auth')
      }
    })
  },
  goToHomePage () {
    this.$router.push('/')
  }
}

const getters = { }

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

Add store-auth.js to the client/src/store.index.js file, so it looks like following:

import Vue from 'vue'
import Vuex from 'vuex'
import myData from './store-myData'
import auth from './store-auth'
Vue.use(Vuex)
export default function () {
  const Store = new Vuex.Store({
    modules: {
      myData,
      auth
    },
    strict: process.env.DEV
  })
  return Store
}

7.5. Incorporate Auth Store into the Client Front End
Call authStateChange action when the app starts or reloads

Replace the script section of the client/src/App.vue file as follows:

import { mapActions } from 'vuex'
export default {
  name: 'App',
  methods: {
    ...mapActions('auth', ['authStateChange'])
  },
  mounted () {
    this.authStateChange()
  }
}

Path token in axios header

Update client/src/services/api.js file as follows:

import axios from 'axios'
import storeAuth from '../store/store-auth'

export default () => {
  return axios.create({
    baseURL: `http://localhost:8081/`,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${storeAuth.state.authToken}`
    }
  })
}

Add Logout button

Update the script section of the client/src/layouts/Layout.vue file as follows:

import { mapState, mapActions } from 'vuex'
export default {
  name: 'MyLayout',
  computed: {
    ...mapState('auth', ['loggedIn'])
  },
  data () {
    return {
      leftDrawerOpen: false
    }
  },
  methods: {
    ...mapActions('auth', ['logoutUser'])
  }
}

Add v-if condition to the Login button as follows:

v-if='!loggedIn'

Add Log out button as follows:

<q-btn v-if='loggedIn'
  @click='logoutUser'
  flat dense label="Log out" icon-right="account_circle" />

Disable menu when user loges out by adding v-if='loggedIn' to the q-drawer as follows:

>q-drawer  v-if='loggedIn'

Update AuthForm.vue component

Modify script section of the client/src/componets/Auth/AuthForm.vue file as follows:

import { mapActions } from 'vuex'

export default {
  name: 'AuthForm',
  props: ['tab'],
  data () {
    return {
      user: {
        email: '',
        password: ''
      }
    }
  },
  methods: {
    ...mapActions('auth', ['registerUser', 'loginUser']),
    validateEmailAddress (email) {
      var re = /^(([^<>()\\.,;:\s@"]+(\.[^<>()\\.,;:\s@"]+)*)|(".+"))@(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
      return re.test(String(email).toLowerCase())
    },
    submitForm () {
      this.$refs.email.validate()
      this.$refs.password.validate()
      if (!this.$refs.email.hasError && !this.$refs.password.hasError) {
        if (this.tab === 'Register') this.add()
        if (this.tab === 'Login') this.login()
      }
    },
    add () {
      this.registerUser(this.user).then((res) => {
        console.log('user registered')
      })
    },
    login () {
      this.loginUser(this.user)
      this.$q.notify({ color: 'green-4', textColor: 'white', icon: 'cloud_done', message: 'User Logged In' })
    }
  }
}

Test registering user and logging in and out.

7.6. Navigation Guards
Boot file

Boot file runs code before the app starts. Use Quasar CLI to generate a new boot file.

At command prompt go to the client folder and type:

quasar new boot routerAuth

This will create a new boot file called routerAuth.js in the client/src/boot folder.

Update the boot section of the client/quasar.conf.js file:

boot: [
  'axios',
  'firebase',
  'routerAuth'
],

Open client/src/boot/routerAuth.js file and replace its content with following:

import storeAuth from '../store/store-auth'

export default ({ router }) => {
  router.beforeEach((to, from, next) => {
    let loggedIn = storeAuth.state.loggedIn
    if (loggedIn || to.path === '/auth') next()
    else next('/auth')
  })
}

Restart Quasar server:

quasar dev

Test.

8. Server Side User Authentication with Google Firebase
8.1. Firebase set-up

Click here for more info.

Create a Firebase Project

In your browser go to firebase.google.com click Get Started. Next Add Project provide your project name (e.g. My Full Stack App) and accept defaults.

Setup Firebase Authentication

Click on Authentication option from the Firebase menu. Then click on Set up sign-in method and select email/password authentication method and Enable it.

Generate a private key

To authenticate a service account and authorize it to access Firebase services, you must generate a private key file in JSON format.

In the Firebase console, open Settings > Service Accounts. Click Generate New Private Key, then confirm by clicking Generate Key.

Securely store the JSON file containing the key.

You will also be provided with the Admin SDK config snippet, e.g.:

var admin = require("firebase-admin");

var serviceAccount = require("path/to/serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://my-full-stack-app.firebaseio.com"
});

Saving the path to the key in the ENV variable

Set the GOOGLE_APPLICATION_CREDENTIALS environment variable with the pass to the JSON file above.

Close and open command prompt window. Type set and you should see GOOGLE_APPLICATION_CREDENTIALS env variable.

??? Add users

Click on Users option and add a user.

8.2. Server authentication code
Install Firebase SDK

In the command prompt window go to the server directory and run the following command:

npm install firebase-admin --save

Firebase Authentication Service

Create a services folder under the server directory.

Create the firebase-service.js file in the server/services directory as follows:

var admin = require("firebase-admin");

var serviceAccount = require(process.env.GOOGLE_APPLICATION_CREDENTIALS);

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://my-full-stack-app.firebaseio.com"
});

module.exports = admin

Remember to update databaseURL with the name of your database as was provided by firebase snippet.

8.3. Add User API to Register and Login Users

Create the server/routes/auth.js file as follows:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.send('Auth page: get request');
});

module.exports = router;

Add the following to the server/startup/app.js file:

const auth = require('../routes/auth');
...
app.use('auth', auth );

Test.

Import firebase-service.js file into the /server/routes/auth.js as follows:

const admin = require('../services/firebase-service.js');

Add POST router to the /server/routes/auth.js file as follows:

router.post('/', async (req, res) => {
  const payload = {
    email: req.body.email,
    password: req.body.password
  }
  const user = await admin.auth().createUser(payload);
  // add logic to check for errors during firebase create
  // if firebase user was created ok, extract uid and save to the dB
  // if db user not saved, delete firebase user and send error,
  // else send FB user object to the client:
  res.send(user);
})

Test using Postman url: http://localhost:8081/auth with Body Raw JSON e.g.:

{
  "email": "test80@test.com",
  "password": "123456"
}

To Do:

For role base security, add routers for defining & assigning roles & menus, dB Tables, etc.

Consider creating a separate application to manage user security (possibly accessing same dB), so the only thing that the main app will let someone do, is to change password.

8.4. Auth Middleware on the Server (validating user Token)

We will need to make sure that requests coming in are from authenticated users. We can achieve this by creating an auth middleware to protect routes that we want to keep private.

Create an auth middleware to ensure there is a valid firebase token in the request header. Add the middleware folder under the server folder. Then add the auth-middleware.js file as follows:

const admin = require('../services/firebase-service.js');

const getAuthToken = (req, res, next) => {
  if (
    req.headers.authorization &&
    req.headers.authorization.split(' ')[0] === 'Bearer'
  ) {
    req.authToken = req.headers.authorization.split(' ')[1];
  } else {
    req.authToken = null;
  }
  next();
};

const checkIfAuthenticated = (req, res, next) => {
 getAuthToken(req, res, async () => {
    try {
      const { authToken } = req;
      const userInfo = await admin
        .auth()
        .verifyIdToken(authToken);
      req.authId = userInfo.uid;
      return next();
    } catch (e) {
      return res
        .status(401)
        .send({ error: 'You are not authorized to make this request' });
    }
  });
};

module.exports = checkIfAuthenticated;

With this middleware in place, the user gets an 'unauthorized' error every time they try to access a private resource without being authenticated.

Now that we have created our middleware, let's use it to protect our private route.

Import above middleware function into the server/startup/app.js as follows:

const checkIfAuthenticated = require('../middleware/auth-middleware.js');

Update ALL router calls to be :

app.use('/', checkIfAuthenticated)

To Do:

For Role based solution, add Login logic which will get user's menu on login(after user's token has been validated)

Useful links:
https://dev.to/emeka/securing-your-express-node-js-api-with-firebase-auth-4b5f
https://www.youtube.com/watch?v=4Rv6KSIsiMo
https://itnext.io/how-to-use-firebase-auth-with-a-custom-node-backend-99a106376c8a
9. Digital Ocean Deployment from Windows
9.1. Digital Ocean - server setup

See https://www.youtube.com/watch?v=RE2PLyFqCzE for a complete tutorial.

1. Download Putty & PuttyGen

Download Putty and PuttyGen onto your Windows machine if you do not have them already.

2. Generate SSH keys

Open PuttyGen and click on the Generate button.

Click on the Save public key button and save the file locally, e.g. DOFSApublickey1.txt

Then enter a passphrase (optional) and click on the Save private key button and save the file locally, e.g. DOFSAprivatekey1.ppk

Keep PuttyGen open.

3. Sign up with Digital Ocean

Open Digital Ocean account.

4. Setup a DO Droplet

Login into your new DO account.

Create a project, eg. My Full Stack App

Create Droplet for your project as following:

  • Image: Ubuntu
  • Size: $5/mo
  • Choose a datacenter closest to you
  • Authentication: click on the New SSH Key button. Go to the PuttyGen Window and copy the entire content of the Public Key text box. Paste it into the Add Public SSH key window on the Digital Ocean page. Give it a name: e.g. FSApublicKey1 and click on the Add SSH Key button.
  • Host name: e.g. myFullStackApp

Click Create. This will set-up your server. Make a note of the IP Address. E.g. 167.172.59.209

Click on the droplet name to get to the DO panel

5. Connect to server via Putty

Open Putty.

Add IP address of your new server.

Create connection profile for your new server on Putty:

Click on Connection --> Data menu option and enter root in the Auto-login user name

Click on SSH --> Auth menu options and upload your private key file DOFSApublickey1.ppk

Go back to the Session tab and enter IP Address 167.172.59.209 of your new Server. Then enter the name of your session, e.g. My Full Stack App at DIGITAL OCEAN and save it by clicking on the Save button.

Click Open. You will see a warning message, click Yes. Enter your passphrase. You are now connected to your new server.

9.2. Digital Ocean - setting up new user and disabling root access
1. Create a new user

Using Putty connect to your server and type:

adduser myuser

Give user admin preveliges

usermod -aG sudo myuser

Test that the user is assign to sudo group:

id myuser

Sign in as your new user:

sudo su -myuser

Check by typing:

whoami

2. Authorise SSH key for the new user

Create a .ssh directory under home directory:

cd ~
mkdir ~/.ssh
chmod 700 ~/.ssh

Save public SSH key for your new user.

Create authorized_keys file:

nano ~/.ssh/authorized_keys

Copy your public key into it. Note, that if you are copying your key from your public key .txt file , type 'ssh-rsa ' (including space) followed by the key from the .txt public key file. Ignore first 2 lines and the last line of comments in the public key .txt file. Make sure that the key is not split across multiple lines, but is one line.

Change permissions on this file:

chmod 600 ~/.ssh/authorized_keys

Restart SSH service:

sudo service ssh restart

Exit:

exit

Close Putty connection.

3. Configure Putty connect for new user

Open Putty. Select your connection and click Load.

Then go to Connection --> Data and change Auto-login username from root to myuser.Go back to the Session. Click Save followed by Open. You should be able to connect by entering the passphrase.

4. Disable Root and Password Login

Your password login should be disabled based on your droplet creation options.

To check, login as your new user using Putty and open sshd_config file:

sudo nano /etc/ssh/sshd_config

Use ctrl+w to search for the following:

PermitRootLogin
PasswordAuthentication

And set both options to no.

Reload sshd with this command:

sudo systemctl reload sshd

Test by going to Putty and pasting the IP address and clicking Open. You should not be able to connect as a root or as your user.

Test by changing user to root on your saved connection. You still should not be able to connect.

Change the user back to your user and Save.

9.3. Digital Ocean - install Node.js, npm, Git, connect to GitHub using SSH Key
1. Install Node.js and npm

Using Putty connect to your server and type following commands to install latest version of node js and npm:

cd ~
curl -sL https://deb.nodesource.com/setup_10.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install nodejs
sudo apt install build-essential

Check versions of node and nvm:

node -v
npm -v

For more information go to the https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-18-04 for a complete tutorial.

2. Install git

Install git with the following command:

sudo apt-get install git

Check git version:

git --version

3. Create SSH Key for the Github

Using Putty connect to your server and type following commands to generate SSH Key for the Github:

cd ~
ssh-keygen -t rsa -C "natalie.seltzer@gmail.com"

4. Copy SSH Public Key

Copy the path to the newly created ssh public key and use it to display it by typing the following command on the server:

cat /home/myuser/.ssh/id_rsa.pub

This will print the new ssh public key. Highlight and copy the key.

Note, that the key starts with ssh-rsa followed by space, followed by the key, followed by space, followed by your email address.

WinSCP alternative

Alternatively, you can use WinSCP to download the file from the server and copy its content. If you do not have WinSCP already installed locally, download it, accepting the defaults and saying yes to import connections from Putty.

Open WinSCP and connect to your server. If you had WinSCP installed previously, you will need to create a new connection. Enter server IP address as a host name and upload the private key file by going to the SSH / Athenticate section of the Advance settings.

Login. Go to Server window on the right and navigate to the hidden .ssh directory by clicking on the open folder icon and typing /.ssh at the end of the user home directory, e.g.: /home/myuser/.ssh

Download id_rsa.pub file to your local machine.

Open it locally with the Notepad and copy the key.

5. Add SSH Public Key to your GitHub repository

Login to your Github and go to your project repository. Then go to Settings and click on the Deploy Key left hand menu option. Then click on the Add deploy key button add new read-only SSH Key (if the key you have copied has your email address at the end - remove it).

Alternatively, login to your Github and go to your project repository. Then click on Copy button and select SSH option. Click on the link to add SSH Key left . Add key including ssh-rsa, followed by space, by key, by another space and your email address.

6. Test connection to the GitHub from the server

To test connection to your GitHub type the following command on your server:

ssh -vT git@github.com

9.4. Digital Ocean - SQLite or MYSQL
0. Deploying SQLite for Obyte projects

On a server go to to your project's /server directory and run the following command:

node db_import.js

This will create SQLite db and run your script to create custom tables (if any).

1. Install MYSQL

For a detailed info on MYSQL install see https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-18-04

Execute following commands to install the latest version of MYSQL on the server:

cd ~
sudo apt update
sudo apt install mysql-server
sudo mysql_secure_installation
mysqld --initialize

Secure Installation script will ask a number of questions:

Allow root remote login? Yes for test, No for production.
Remove unanimous users? Yes
Remove test db? Yes
Reload privileges? Yes

Login into MYSQL as root:

sudo mysql -u root

2. Create database and database user

Follow instructions in the Section 4 of this Tutorial to create new db user, new db and tabls. Please note, that if you are using Sequelize you will not need to create tables as Sequelize would do it for you when server starts.

3. Optional: Export data for local database

On your local machine open

MySQL Workbench. Click Server menu option and select Data Export submenu.

Click Export tick-box next to your schema.

Choose Dump Data Only option.

Then choose Export to Self-Contained File option and specify file path and name.

Tick Create Dump in a Single Transaction and Include Create Schema tick-boxes and click on the Start Export button.

9.5. Client Build
1. Setting up Quasar Process Env variable with remote server location

Open /client/quasar.conf.js file and go to build: section and add remote server location Process Env Variable, e.g.

build: {
  env: ctx.dev
    ? { // so on dev we'll have
      SERVER_URL: JSON.stringify(`http://localhost:8081/`)
    }
    : { // and on build (production)
      SERVER_URL: JSON.stringify(`http://167.172.59.209:8081/`)
    },

Note that you can defined local env variable (e.g. FSA_SERVER_URL) containing the remote server IP address and use it like so:

SERVER_URL: JSON.stringify(process.env.FSA_SERVER_URL)

2. Point Axios to use Quasar Process Env variable for remote server baseURL

Amend client/src/services/api.js file, replacing:

baseURL: `http://localhost:8081/`,

with:

baseURL: process.env.SERVER_URL,

Test that your app still works in dev mode locally.

quasar dev

3. Build Quasar SPA

Build SPA by going to client directory and typing:

quasar build

This creates a new dist/spa folder under our client folder.

4. Post latest code to Github

Update /client/.gitignore file and remove/comment out /dist directory line.

Then, go to the application directory:

git add .
git commit -m "client build for remote deployment"
git push -u origin master

5. Other useful Git commands:

If you realised that you have added wrong file (e.g. config or notes) to your local git staging area with the git add . command. You can clear (unstage) with the following command:

git reset

To check what is in your git staging area, run the following command:

git status

9.6. Digital Ocean - (re)deploy code, Firebase, populate db
1. Clone your Github project repository onto the remote server

First, make sure that your GitHub Repo is up to date. Then use Putty to connect to your remote server.

Clone your project repository using SSH key:

cd ~
git clone git@github.com:CriptoGirl/FullStackApp.git

You will be asked for a passphrase.

Alternatively, use HTTPS clone method to connect to your remote server and type following commands:

cd ~
git clone https://github.com/CriptoGirl/FullStackApp.git

You will be asked for your GitHub username and password.

If you have first clone with HTTPS but then want to start using SSH key you can change it by navigating to your project directory and typing:

git remote set-url origin git@github.com:CriptoGirl/FullStackApp.git

2. Install server on remote

Then install server dependencies:

cd FullStackApp
cd server
npm install

3. FirebaseAuth Config on the remote server

Use Putty to connect to your remote server, then create Config directory under your user directory:

cd ~
mkdir Config

Use WinSCP to copy C:\Users\natal\node js\FirebaseAuth\my-full-stack-app-firebase-adminsdk-3l3z4-2b81d51460.json to FSA_Firebase_config.json file in the ~/Config directory.

4. Populate database

Connect to your remote server and open MySQL terminal by typing:

mysql -u myuser -p

You will be asked to enter database password.

Then run script:

mysql> source ~/myfullstackapp/server/startup/data/my-data.sql

5. Optional: set up temp Env Variables and Test

Set up temp env variable to store the location of the Firebase config file. Check the name of the variable used by your application as referred to in the /server/services/firebase-service.js file. Execute the following commands to set temp env variable that will help with testing, but will be gone when you close your server connection. We will set this up to last later using pm2.

cd ~
export GOOGLE_APPLICATION_CREDENTIALS="/home/myuser/Config/FSA_Firebase_config.json"
echo $GOOGLE_APPLICATION_CREDENTIALS

Test by going to the /server directory and starting the server:

node server

6. Deploy latest code

If you made any changes to the client front end code, remember to build a new SPA by executing the following command in your local /client directory:

quasar build

Then go to your local project directory and deploy to GitHub:

git add .
git commit -m "some changes"
git push origin master

Use Putty to connect to your remote server, then go to your project directory and type following commands:

git pull origin master

You will be asked for your GitHub username and password.

If you have made any changes to your remote files and do not want to keep them, you can reset server git by typing following command in the project folder:

git reset --hard
git pull origin master

9.7. Digital Ocean - setting up PM2 Process Manager
1. Install PM2 process manager

We are going to use PM2 to run our application as a service in a background. Install it on your server by running:

cd ~
sudo npm install -g pm2

2. Configure PM2 using ecosystem file

To generate a sample ecosystem.config.js file in your user directory type:

cd ~
pm2 ecosystem

Amend it using nano editor:

cd ~
nano ecosystem.config.js

Update ecosystem.config.js file to look like following:

modules.exports ={
  apps: [{
    name: 'FSA-server',
    script: './myfullstackapp/client/server.js',
    autorestart: true,
    watch: true,
    env: {
      NODE_ENV: 'development'
    },
    env_test: {
      NODE_ENV: 'test',
      GOOGLE_APPLICATION_CREDENTIALS: '/home/myuser/Config/FSA_Firebase_config.json'
    },
    env_production: {
      NODE_ENV: 'production'
    }
  }]
};

3. Start the server with PM2

Start the server as a background process in test mode with pm2 run:

pm2 start ecosystem.config.js --env test

4. Other useful PM2 commands

To see which processes are running, to see logs or to stop server type :

pm2 list
pm2 logs
pm2 stop server.js

For more information click here. Click here for a list of PM2 commands.

9.8. Digital Ocean - install and configure Nginx
1. Install Nginx

Install Nginx on the server:

cd ~
sudo apt-get install nginx

Check that Nginx is running:

service nginx status

Use Cntrl-C to exit from the status window.

Test by going to your browser and typing your server's IP Address, e.g.

167.172.59.209

You should see:

Welcome to nginx!

2. Configure Nginx

Add path to your SPA client index.html file to the Nginx config:

nano /etc/nginx/sites-available/default

Amending root path as following:

root /home/myuser/myfullstackapp/client/dist/spa;

You should NOT need to restart Nginx but check that it is running as described above.

If you need to restart Nginx you can do it with the following command:

sudo service nginx restart

To check status of Nginx type:

sudo service nginx status

Type q to exit.

9.9. Digital Ocean - UFW Firewall config

For more info click here.

UFW firewall is installed by default on Ubuntu. To check if it is install, type:

sudo ufw status verbose

If it has been uninstalled for some reason, you can install it with:

sudo apt install ufw

1. Optional: using IPv6 with UFW
2. Setting up Default Policies

The first rules to define are your default policies. These rules control how to handle traffic that does not explicitly match any other rules. By default, UFW is set to deny all incoming connections and allow all outgoing connections. You also want to allow incoming SSH connections so you can connect to and manage your server. To set the defaults used by UFW, use these commands:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh

Please note, that it is very important to allow ssh connection, without it you would not be able to connect to your server.

3. Enabling UFW

To enable UFW, use this command:

sudo ufw enable

You will receive a warning that says the command may disrupt existing SSH connections. Respond to the prompt with y and hit ENTER.

The firewall is now active. Run the following command to see the rules that are set:

sudo ufw status verbose

4. Adding ports

Test that you can nor longer access your application from the browser.

Allow other ports used by your application:

cd ~
sudo ufw allow 80
sudo ufw allow 8081
sudo ufw allow 443

5. Turn on Firewall Logging and Test

Turn on firewall logging:

sudo ufw logging on

Test by running your application in a browser.

To see firewall logs:

sudo tail -f /var/log/ufw.log

6. To disable Firewall:

If at any point you need to disable the firewall:

sudo ufw disable
sudo ufw status

9.10. Domain Name registration

Login to https://uk.godaddy.com/ you will need to create an account if you do not have one.

Purchase domain name you like. Uncheck option 'to create a web site'.

Click on Domains menu option and select All Domains submenu.

Click on ... next to your new domain name and choose Manage DNS option. This should take you to the DNS Managment Page.

Edit A record. Replacing Parked with your server IP Address and Save.

Check that this changes have been propagated. Go to https://network-tools.com/nslookup/ and search to see if your new domain has been propogated.

9.11. SSL Certificate

To enable HTTPS on your website, you need to get a certificate from a Certificate Authority, such as Let’s Encrypt.

1. Get Free SSL Certificate from Let's Encrypt

Follow instructions on cerbot site to generate a free SSL Certificate.

Connect to your server and run the following commands:

cd ~
sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-nginx

Next get certificate for your domain name by executing the following command:

sudo certbot --nginx -d blockhouseservices.com -d www.blockhouseservices.com

Select an option to automatically redirect http requests to https.

For more info from Let's Encrypt read Let's Encrypt Guide.

2. Configure Firewall to allow port 443

Check which ports are currenty open:

sudo ufw status

Allow full access to Nginx and check status again:

sudo ufw allow 'Nginx Full'
sudo ufw status

3. Configure Nginx for SSL

At the remote server command prompt open nginx config file in the nanoeditor with the following command:

nano /etc/nginx/sites-available/default

1. Uncomment listen 443 and listen [::]:443 lines in the SSL Configuration section of the nginx default config file.

2. Add server name to the server_name _; line as following:

server_name blockhouseservices.com www.blockhouseservices.com;

3. Configure nginx to act as a reverse proxy for the back end server, creating a proxy path through. Add a new server block at the end of the config file as following:

server {
  listen #### ssl;
  listen [::]:#### ssl;

  server_name blockhouseservices.com www.blockhouseservices.com;

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_pass http://localhost:8081;
  }

  ssl_certificate /etc/letsencrypt/live/blockhouseservices.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/blockhouseservices.com/privkey.pem; # managed by Certbot
}

Where #### is a new port.

4. Save changes and test that nginx config is valid:

sudo nginx -t

5. Restart nginx nginx:

sudo systemctl reload nginx

4. Add new port to Firewall

Allow port #### to the Firewall:

sudo ufw allow ####

5. Change config to use https

1. Change Quasar Config quasar.config.js to use https & domain name for the server as following:

SERVER_URL: JSON.stringify(`https://blockhouseservices.com:####/`)

2. If using CORS library update app.js file as following:

const cors = require('cors');
...
const corsConfig = {
  origin: true,
  origin: ['https://myDomainName.com', 'https://www.myDomainName.com'],
  credentials: true,
};
app.use(cors(corsConfig));
app.options('*', cors(corsConfig));

3. Rebuild client and redeploy. Remember to restart server using pm2.

5. Test in the browser

Refresh the page in the browser to test using your domain name and https protocol.

Digital Ocean - To Do
1. Pointing to your Domain name

Go to your Domain name registar and set:

Name Server 1: ns1.digitalocean.com
Name Server 2: ns2.digitalocean.com
Name Server 3: ns3.digitalocean.com

This would take up to an hour to take effect.

Go to the Digital Ocean Control Panel and click on the Networking tab.

Enter domain name, e.g. myfullstackapp.com

Create A record:

HOSTNAME: @
WILL DIRECT TO: select your Droplet

Create CNAME record:

HOSTNAME: www
WILL DIRECT TO: @

Use A DNS field to point all domains/subdomains for my apps to my remote server's IP.

4. libcap2-bin ?
5. Flightplan - Server admin ?
4. Usefull videos:

Click here for some info on deployment.

10. Sequelize
10.1. Installing Sequelize

... follow standard steps to install Sequelize and add it to your project.

10.2. Sequelize Config
1. Model synchronization

Add following to the /server/setup/app.js file:

const db = require("../models");
db.sequelize.sync({ alter: true });

This checks what is the current state of each table in the database (which columns it has, what are their data types, etc), and then performs the necessary changes in the table to make it match the model.

10.3. Defining Models

A model is an abstraction that represents a table in your database.

1. Model(table) definition
2. Field definition

Following datatypes are supported:

  • String, e.g. DataTypes.STRING VARCHAR(255)
  • TEXT, e.g. DataTypes.TEXT
  • Boolean, e.g. DataTypes.BOOLEAN
  • Numbers (integers, floats, etc.)
  • Dates
  • UUIDs
  • BLOBs
  • ENUMs, eg. DataTypes.ENUM('active', 'pending')

When defining a column, apart from specifying the type of the column, you can specify the allowNull and defaultValue options

Following options are supported:

  • defaultValue: "John Doe"
  • defaultValue: Sequelize.NOW
  • type: DataTypes.INTEGER, autoIncrement: true
  • unique: true
  • primaryKey: true
  • references: { model: Bar, key: 'id' }
  • comment: 'This is a column name that has a comment'

It is also possible to define unique against multiple columns.

For example:

sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    defaultValue: "John Doe"
  }
});

3. Taking advantage of Models being classes

Add method to a model, e.g. getFullname() { return [this.firstname, this.lastname].join(' '); }

And call it like so: onsole.log(user.getFullname());

10.4. Adding data instance with Create method
1. Inserting data

Add and Save data to the database with Sequelize create method, which combines the build and save methods shown above into a single method:

const result = await User.create({ firstName: "Jane", lastName: "Doe" });
console.log(result instanceof User); // true
console.log(result.name, " Auto-generated ID:", result.id);
console.log(result.toJSON());

It is possible to define which attributes should be saved when calling save, by passing an array of column names.

2. Creating in bulk

Sequelize provides the Model.bulkCreate method to allow creating multiple records at once, with only one query.

10.5. Update
1. Simple UPDATE queries

Update queries also accept the where option, e.g.:

// Change everyone without a last name to "Doe"
await User.update({ lastName: "Doe" }, {
  where: { lastName: null } });

2. Updating an instance

If you change the value of some field of an instance, calling save again will update it accordingly:

const jane = await User.create({ name: "Jane" });
console.log(jane.name); // "Jane"
jane.name = "Ada";
// the name is still "Jane" in the database
await jane.save();
// Now the name was updated to "Ada" in the database!

3. Saving only some fields

It is possible to define which attributes should be saved when calling save, by passing an array of column names.

4. Change-awareness of save

The save method is optimized internally to only update fields that really changed. This means that if you don't change anything and call save, Sequelize will know that the save is superfluous and do nothing, i.e., no query will be generated (it will still return a Promise, but it will resolve immediately).

Also, if only a few attributes have changed when you call save, only those fields will be sent in the UPDATE query, to improve performance.

10.6. Delete / Reload / Increment / Raw queries
1. Deleting data instance with destroy method

You can delete an instance by calling destroy , e.g.:

const jane = await User.create({ name: "Jane" });
await jane.destroy(); // Now this entry was removed from the database

2. Simple delete with where close

Delete queries also accept the where option:

// Delete everyone named "Jane"
await User.destroy({ where: { firstName: "Jane" } });

3. Truncate everything

To destroy everything the TRUNCATE SQL can be used:

// Truncate the table
await User.destroy({ truncate: true });

4. Reloading data instance with reload method

You can reload an instance from the database by calling the reload method:

const jane = await User.create({ name: "Jane" });
jane.name = "Ada"; // the name is still "Jane" in the database
await jane.reload();
console.log(jane.name); // "Jane"

The reload call generates a SELECT query to get the up-to-date data from the database.

5. Incrementing and decrementing integer values

In order to increment/decrement values of an instance without running into concurrency issues use increment and decrement instance methods, e.g.:

const jane = await User.create({ name: "Jane", age: 100 });
const incrementResult = await user.increment('age', { by: 2 });
// Note: to increment by 1 you can omit the `by` option and just do `user.increment('age')`

Decrementing works in the exact same way.

6. Raw Queries

For more information on Raw Queries click here.

If you need to pass arguments, make sure you are using Replacements or Bind Parameters to protect against SQL Injection.

10.7. Select queries, Ordering and Grouping

Finder methods are the ones that generate SELECT queries. Sequelize automatically wraps everything in proper instance objects. When there are too many results, this wrapping can be inefficient. To disable this wrapping and receive a plain response instead, pass { raw: true } as an option to the finder method.

1. Simple SELECT with findAll and findByPk

You can read the whole table from the database with the findAll method, e.g.:

const users = await User.findAll(); // Find all users
console.log(users.every(user => user instanceof User)); // true
console.log("All users:", JSON.stringify(users, null, 2));

The findByPk method obtains only a single entry from the table, using the provided primary key, e.g.:

const project = await Project.findByPk(123);  // primary key is 123
if (project === null) { console.log('Not found!');
} else { console.log(project instanceof Project); }

The findOne method obtains the first entry it finds (that fulfills the optional query options, if provided).

The method findOrCreate will create an entry in the table unless it can find one fulfilling the query options.

2. Specifying attributes for SELECT queries

It is possible to specify which attributes to include or exclude as well as rename attributes and/or use sequelize.fn to do aggregations:

Model.findAll({
  attributes: [ 'foo', [sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats'], 'bar' ] });

will result in:

SELECT foo, COUNT(hats) AS n_hats, bar FROM ..

3. Where close

Multiple checks can be passed:

Post.findAll({
  where: {
    authorId: 12
    status: 'active'
  }
});

This results in the following statement:

SELECT * FROM post WHERE authorId = 12 AND status = 'active';

4. Advanced queries with functions (not just columns)

What if you need something even more complex?

Post.findAll({
  where: {
    [Op.or]: [
      sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7),
      {
        content: {
          [Op.like]: 'Hello%'
        }
      },
      {
        [Op.and]: [
          { status: 'draft' },
          sequelize.where(sequelize.fn('char_length', sequelize.col('content')), {
            [Op.gt]: 10
          })
        ]
      }
    ]
  }
});

The above generates the following SQL:

SELECT ... FROM "posts" AS "post" WHERE (
    char_length("content") = 7
  OR
    "post"."content" LIKE 'Hello%'
  OR (
    "post"."status" = 'draft'
    AND
    char_length("content") > 10 )
  )

5. Ordering and Grouping

The order option takes an array of items to order the query by or a sequelize method, e.g.:

Subtask.findAll({
  order: [
    // Will escape title and validate DESC against a list of valid direction parameters
    ['title', 'DESC'],
    // Will order by max(age)
    sequelize.fn('max', sequelize.col('age'))
  ]
})

6. Limits and Pagination

The limit and offset options allow you to work with limiting / pagination:

// Skip 5 instances and fetch the 5 after that
Project.findAll({ offset: 5, limit: 5 });

Usually these are used alongside the order option.

ThefindAndCountAll method is a convenience method that combines findAll and count. This is useful when dealing with queries related to pagination where you want to retrieve data with a limit and offset but also need to know the total number of records that match the query.

const { count, rows } = await Project.findAndCountAll({
  where: { title: { [Op.like]: 'foo%' } },
  offset: 10,
  limit: 2
});
console.log('Count: ', count, ' Rows: ', rows);

7. Utility methods

The count method simply counts the occurrences of elements in the database.

Sequelize also provides the max, min and sum convenience methods.

10.8. Getters, Setters and Virtuals

Sequelize allows you to define custom getters and setters for the attributes of your models. Sequelize also allows you to specify the so-called virtual attributes, which are attributes on the Sequelize Model that doesn't really exist in the underlying SQL table, but instead are populated automatically by Sequelize.

1. Getters

A getter is a get() function defined for one column in the model definition:

const User = sequelize.define('user', {
  // Let's say we wanted to see every username in uppercase
  username: {
    type: DataTypes.STRING,
    get() {
      const rawValue = this.getDataValue(username);
      return rawValue ? rawValue.toUpperCase() : null;
    }
  }
});

This getter, just like a standard JavaScript getter, is called automatically when the field value is read.

2. Setters

A setter is a set() function defined for one column in the model definition. It receives the value being set:

const User = sequelize.define('user', {
  username: DataTypes.STRING,
  password: {
    type: DataTypes.STRING,
    set(value) {
      // Hashing the value with an appropriate cryptographic hash function and using the username as a salt
      this.setDataValue('password', hash(this.username + value));
    }
  }
});
//
const user = User.build({ username: 'someone', password: 'NotSo§tr0ngP4$SW0RD!' });
console.log(user.password); // '7cfc84b8ea898bb72462e78b4643cfccd77e9f05678ec2ce78754147ba947acc'
console.log(user.getDataValue(password)); // '7cfc84b8ea898bb72462e78b4643cfccd77e9f05678ec2ce78754147ba947acc'

Observe that Sequelize called the setter automatically, before even sending data to the database. The only data the database ever saw was the already hashed value.

3. Combining getters and setters

Getters and setters can be both defined in the same field. E.g. to encript, store, retrive and decript data.

4. Virtual fields

Virtual fields are fields that Sequelize populates under the hood, but in reality they don't even exist in the database.

const { DataTypes } = require("sequelize");
const User = sequelize.define('user', {
  firstName: DataTypes.TEXT,
  lastName: DataTypes.TEXT,
  fullName: {
    type: DataTypes.VIRTUAL,
    get() { return `${this.firstName} ${this.lastName}`; },
    set(value) { throw new Error('Do not try to set the `fullName` value!');
    }
  }
});
const user = await User.create({ firstName: 'John', lastName: 'Doe' });
console.log(user.fullName); // 'John Doe'

10.9. Validations & Constraints

Click here for more information.

Validations are checks performed in the Sequelize level, in pure JavaScript. They can be arbitrarily complex if you provide a custom validator function, or can be one of the built-in validators offered by Sequelize. If a validation fails, no SQL query will be sent to the database at all.

On the other hand, constraints are rules defined at SQL level. The most basic example of constraint is an Unique Constraint.

Model validators allow you to specify format/content/inheritance validations for each attribute of the model. Validations are automatically run on create, update and save. You can also call validate() to manually validate an instance.

1. Constraints

Following constrains can be defined:

  • unique: true
  • allowNull: false
  • primaryKey: true

2. Validations

Validations are automatically run on create , update and save. You can also call validate() to manually validate an instance.

When using custom validator functions the error message will be whatever message the thrown Error object holds.

You can also define a custom function for the logging part.

Example:

({
  name: Sequelize.STRING,
  address: Sequelize.STRING,
  latitude: {
    type: DataTypes.INTEGER,
    validate: {
      min: -90,
      max: 90
    }
  },
  longitude: {
    type: DataTypes.INTEGER,
    validate: {
      min: -180,
      max: 180
    }
  },
  }, {
  sequelize,
  validate: {
    bothCoordsOrNone() {
      if ((this.latitude === null) !== (this.longitude === null)) {
        throw new Error('Either both latitude and longitude, or neither!');
      }
    }
  }
})

10.10. Associations

Click here for more information.

1. One-To-One

The A.hasOne(B) association means that a One-To-One relationship exists between A and B, with the foreign key being defined in the target model (B).

On the other hand, the A.belongsTo(B) association means that a One-To-One relationship exists between A and B, with the foreign key being defined in the source model (A).

Specifying hasOne and/or belongsTo will result in the same db structure (e.g. a foreign key in the target table).

Use allowNull: false constrain on a foreign key field of the target table for mandatory relationship. The main difference between hasOne and belongsTo is on the ORM's layer:

If we want to be able to populate data from both models, we need to define both Associations hasOne and belongsTo. If it is enough to get only, for example, User's Phone, but not Phone's User, we can define just User.hasOne(Phone) relation on the User model.

2. One-To-Many

The A.hasMany(B) association means that a One-To-Many relationship exists between A and B, with the foreign key being defined in the target model ( B).

The main way to do it used a pair of Sequelize associations: hasMany and belongsTo.

Team.hasMany(Player);
Player.belongsTo(Team);

Like One-To-One relationships, ON DELETE defaults to SET NULL and ON UPDATE defaults to CASCADE. Although, this can be changed with options.

3. Many-To-Many

The A.belongsToMany(B, { through: 'C' }) association means that a Many-To-Many relationship exists between A and B, using table C as junction table, which will have the foreign keys (aId and bId, for example). Sequelize will automatically create this model C (unless it already exists) and define the appropriate foreign keys on it.

Note: In the examples above for belongsToMany, a string ('C') was passed to the through option. In this case, Sequelize automatically generates a model with this name. However, you can also pass a model directly, if you have already defined it.

Validations are checks performed in the Sequelize level, in pure JavaScript. They can be arbitrarily complex if you provide a custom validator function, or can be one of the built-in validators offered by Sequelize. If a validation fails, no SQL query will be sent to the database at all.

On the other hand, constraints are rules defined at SQL level. The most basic example of constraint is an Unique Constraint.

Model validators allow you to specify format/content/inheritance validations for each attribute of the model. Validations are automatically run on create, update and save. You can also call validate() to manually validate an instance.

1. Constraints

Following constrains can be defined:

  • unique: true
  • allowNull: false
  • primaryKey: true

2. Validations

Validations are automatically run on create , update and save. You can also call validate() to manually validate an instance.

When using custom validator functions the error message will be whatever message the thrown Error object holds.

You can also define a custom function for the logging part.

Example:

({
  name: Sequelize.STRING,
  address: Sequelize.STRING,
  latitude: {
    type: DataTypes.INTEGER,
    validate: {
      min: -90,
      max: 90
    }
  },
  longitude: {
    type: DataTypes.INTEGER,
    validate: {
      min: -180,
      max: 180
    }
  },
  }, {
  sequelize,
  validate: {
    bothCoordsOrNone() {
      if ((this.latitude === null) !== (this.longitude === null)) {
        throw new Error('Either both latitude and longitude, or neither!');
      }
    }
  }
})

Questions, Notes, To Do
Questions
Deployment, Permissions and Security
  1. How do I change firewall permission that my back end server can only be accessible from my quasar app?
  2. Should I change db root preveliges to authenticate with password?
  3. I am using Firebase Auth. Should I still use Nav Guards in quasar boot file?
  4. What monitoring should I have in place?
GitHub
  1. How to remove unwanted directories from the GitHub?
Unresolved errors
  1. NavigationDuplicated error from store-auth.js file handleAuthStateChange
To Do
  • Firm -Multi-tenant SaaS database tenancy patterns
  • Obyte JSON RPC - https://developer.obyte.org/json-rpc
  • Validation:
  • joi
  • a(href='https://www.joda.org/joda-money/' https://www.joda.org/joda-money/
  • and a(href='https://www.npmjs.com/package/js-money') https://www.npmjs.com/package/js-money
  • Testing: jest framework
  • automatically get new token after 1 hour if user has been active in the last 10 min
  • automatically log user out if user has been inactive for 10 min
  • keep user on the same url when he/she refreshes the page.
  • readOWASP documents and check info onInfoSec StackExchange.
Production

For production deployment consider store passwords, mysql and firebase credentials in a volt or using Ansible

See Ansible library.

Also see Digital Ocean Ansible notes.

Other Notes

First, npm i firebase in our Vue project. Then go to main.js and import it with import firebase from 'firebase'.

Advanced Security: Enforce IP address restrictions A common security mechanism for detecting token theft is to keep track of request IP address origins. For example, if requests are always coming from the same IP address (server making the call), single IP address sessions can be enforced. Or, you might revoke a user's token if you detect that the user's IP address suddenly changed geolocation or you receive a request from a suspicious origin.

To perform security checks based on IP address, for every authenticated request inspect the ID token and check if the request's IP address matches previous trusted IP addresses or is within a trusted range before allowing access to restricted data. For example:

https://firebase.google.com/docs/auth/admin/manage-sessions

HTTP Interception is a popular feature of Axios. With this feature, you can examine and change HTTP requests from your program to the server and vice versa, which is very useful for a variety of implicit tasks, such as logging and authentication.

using Google? https://www.udemy.com/course/quasarframework/learn/lecture/14870105#overview

Linex admin https://www.youtube.com/watch?v=qAMWG86sEm8