A step by step guide to building a Full Stack Application using Qusar, Vue, VueX, Axios, Express, Node JS, MySQL.
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
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/
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.
If desired change the Client Server. Open quasar.conf.js
file and search for port 8080
line and change the port number.
At command prompt, go to the client
folder and type the following command to start the Client Server in dev mode:
quasar dev
Test by typing http://localhost:8080/
in the browser.
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
Create a server
folder in your project directory.
Go to this directory and create a new node.js
package by running the following command at the command prompt:
npm init
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.
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
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;
At command prompt, go to the server
folder and type the following command to start the Server:
nodemon
This should start the Server.
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
.
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
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
.
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>
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 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>
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 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 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/>
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 }
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>
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 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.
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) } }
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.
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" }
If you have not selected Axios
when setting up Quasar
client application, you can install it at the command prompt, go to the client
directory and run:
npm install --save axios
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) },
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) } }
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 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.
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).
Go to mysql.com and download mysql and MYSQL workbench. Make a note of root password.
add MYSQLserver/bin directory to your PATH.
Login at the command prompt as root:
mysql -u root -p
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
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 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;
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;
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.
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.
Create the readDataService.js
file in the client/src/services
directory as follows:
import api from './api' export default { readData () { return api().get('search') } }
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'
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.
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.vue
file, replacing:
<div>Quasar v{{ $q.version }}</div>
with:
<q-btn flat dense to='/auth' label="Login" icon-right="account_circle" />
Test.
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.
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.
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.
Click on Users
option and add a user for testing.
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
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 }) }
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 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 }
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() } }
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}` } }) }
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'
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.
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.
Click here for more info.
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.
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.
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" });
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.
Click on Users
option and add a user.
In the command prompt window go to the server
directory and run the following command:
npm install firebase-admin --save
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.
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" }
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.
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)
For Role based solution, add Login logic which will get user's menu on login(after user's token has been validated)
See https://www.youtube.com/watch?v=RE2PLyFqCzE for a complete tutorial.
Download Putty and PuttyGen onto your Windows machine if you do not have them already.
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.
Open Digital Ocean account.
Login into your new DO account.
Create a project, eg. My Full Stack App
Create Droplet for your project as following:
Ubuntu
$5/mo
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. 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
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.
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
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.
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.
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
.
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.
Install git
with the following command:
sudo apt-get install git
Check git
version:
git --version
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"
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.
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.
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.
To test connection to your GitHub
type the following command on your server:
ssh -vT git@github.com
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).
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
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.
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.
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)
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
Build SPA by going to client directory and typing:
quasar build
This creates a new dist/spa
folder under our client
folder.
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
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
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
Then install server dependencies:
cd FullStackApp cd server npm install
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.
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
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
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
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
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' } }] };
Start the server as a background process in test mode
with pm2
run:
pm2 start ecosystem.config.js --env test
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.
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!
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.
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
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.
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
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
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
If at any point you need to disable the firewall:
sudo ufw disable sudo ufw status
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.
To enable HTTPS
on your website, you need to get a certificate from a Certificate Authority
, such as 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.
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
At the remote server command prompt open nginx config file
in the nano
editor 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
Allow port #### to the Firewall:
sudo ufw allow ####
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.
Refresh the page in the browser to test using your domain name and https protocol.
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.
Click here for some info on deployment.
... follow standard steps to install Sequelize
and add it to your project.
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.
A model is an abstraction that represents a table in your database.
Following datatypes are supported:
DataTypes.STRING
VARCHAR(255) DataTypes.TEXT
DataTypes.BOOLEAN
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:
It is also possible to define unique against multiple columns.
For example:
sequelize.define('User', { name: { type: DataTypes.STRING, defaultValue: "John Doe" } });
Add method to a model, e.g. getFullname() { return [this.firstname, this.lastname].join(' '); }
And call it like so: onsole.log(user.getFullname());
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.
Sequelize
provides the Model.bulkCreate
method to allow creating multiple records at once, with only one query.
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 } });
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!
It is possible to define which attributes should be saved when calling save, by passing an array of column names.
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.
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
Delete queries also accept the where option:
// Delete everyone named "Jane" await User.destroy({ where: { firstName: "Jane" } });
To destroy everything the TRUNCATE SQL
can be used:
// Truncate the table await User.destroy({ truncate: true });
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.
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.
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.
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.
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.
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 ..
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';
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 ) )
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')) ] })
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);
The count
method simply counts the occurrences of elements in the database.
Sequelize
also provides the max
, min
and sum
convenience methods.
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
Sequelize
.
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.
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.
Getters and setters can be both defined in the same field. E.g. to encript, store, retrive and decript data.
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'
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.
Following constrains can be defined:
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!'); } } } })
Click here for more information.
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.
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.
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.
Following constrains can be defined:
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!'); } } } })
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.
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