├── template ├── client │ ├── static │ │ ├── main.css │ │ └── images │ │ │ └── logo.png │ ├── README.md │ ├── components │ │ └── HelloWorld │ │ │ ├── HelloWorld.vue │ │ │ └── HelloWorld.spec.js │ ├── store │ │ ├── modules │ │ │ ├── auth │ │ │ │ ├── mutations.js │ │ │ │ ├── index.js │ │ │ │ ├── mutations.spec.js │ │ │ │ └── actions.js │ │ │ └── async │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ └── index.js │ ├── index.html │ ├── view │ │ ├── mixins │ │ │ └── notification.js │ │ ├── Dashboard.vue │ │ ├── containers │ │ │ └── HeaderContainer.vue │ │ ├── Profile.vue │ │ └── Login.vue │ ├── style │ │ ├── app.scss │ │ ├── _spinner.scss │ │ └── global.scss │ ├── App.vue │ ├── services │ │ ├── loopback.spec.js │ │ └── loopback.js │ ├── main.js │ ├── router.js │ └── App.spec.js ├── .eslintignore ├── jest.plugins.js ├── gulp-tasks │ ├── default.js │ ├── tasks.js │ ├── clear-cache.js │ ├── config.js │ ├── compilers.js │ ├── copy.js │ ├── serve.js │ └── build.js ├── server │ ├── config.production.json │ ├── component-config.json │ ├── initial-data │ │ └── maintenance-account.json │ ├── boot │ │ ├── authentication.js │ │ ├── add-initial-data.js │ │ ├── root.js │ │ ├── create-admin.js │ │ └── __tests__ │ │ │ ├── create-admin.spec.js │ │ │ └── boot.spec.js │ ├── middleware.development.json │ ├── datasources.production.json │ ├── config.json │ ├── datasources.json │ ├── models │ │ ├── account.js │ │ ├── account.json │ │ └── account.spec.js │ ├── middleware.json │ ├── server.js │ ├── model-config.json │ └── server.spec.js ├── gulpfile.babel.js ├── .gitignore ├── common │ └── models │ │ ├── message.js │ │ └── message.json ├── .editorconfig ├── index.spec.js ├── test │ └── utils │ │ ├── create-loopback.js │ │ └── create-loopback.spec.js ├── .babelrc ├── index.js ├── .eslintrc ├── README.md ├── jest.config.js └── package.json ├── .eslintignore ├── .gitignore ├── .travis.yml ├── .editorconfig ├── .eslintrc ├── .release-it.json ├── package.json ├── LICENSE ├── meta.js ├── README.md ├── test.js └── CHANGELOG.md /template/client/static/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test-project 2 | template 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test-project 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /template/jest.plugins.js: -------------------------------------------------------------------------------- 1 | require('jest-plugins')([ 2 | 'loopback-jest', 3 | ]); 4 | -------------------------------------------------------------------------------- /template/gulp-tasks/default.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | gulp.task('default', ['serve']); 4 | -------------------------------------------------------------------------------- /template/client/README.md: -------------------------------------------------------------------------------- 1 | ## Client 2 | 3 | This is the place for your application front-end files. 4 | -------------------------------------------------------------------------------- /template/server/config.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiHost": "123.123.123.123", 3 | "port": 80 4 | } 5 | -------------------------------------------------------------------------------- /template/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | // Search for gulp tasks in gulp-tasks directory 2 | import './gulp-tasks/tasks'; 3 | -------------------------------------------------------------------------------- /template/client/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InCuca/vue-loopback/HEAD/template/client/static/images/logo.png -------------------------------------------------------------------------------- /template/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /template/server/initial-data/maintenance-account.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "admin@mydomain.com", 3 | "password": "h4ckme" 4 | } 5 | -------------------------------------------------------------------------------- /template/server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | export default function enableAuthentication(server) { 2 | // enable authentication 3 | server.enableAuth(); 4 | } 5 | -------------------------------------------------------------------------------- /template/gulp-tasks/tasks.js: -------------------------------------------------------------------------------- 1 | // This file just import the entire contents of this directory 2 | import './serve'; 3 | import './default'; 4 | import './build'; 5 | import './copy'; 6 | -------------------------------------------------------------------------------- /template/server/boot/add-initial-data.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | /** 4 | * Create the initial data of the system 5 | */ 6 | export default function addInitialData(server) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /template/server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /template/client/components/HelloWorld/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /template/server/boot/root.js: -------------------------------------------------------------------------------- 1 | export default function(server) { 2 | // Install a `/api` route that returns server status 3 | const router = server.loopback.Router(); 4 | router.get('/api', server.loopback.status()); 5 | server.use(router); 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | node_js: 4 | - "8" 5 | - "node" 6 | addons: 7 | chrome: stable 8 | before_script: 9 | - "export DISPLAY=:99.0" 10 | - "sh -e /etc/init.d/xvfb start" 11 | - sleep 3 # give xvfb some time to start 12 | -------------------------------------------------------------------------------- /template/client/store/modules/auth/mutations.js: -------------------------------------------------------------------------------- 1 | export function setAccessToken(state, token) { 2 | // eslint-disable-next-line camelcase 3 | state.access_token = token; 4 | } 5 | 6 | export function setAccount(state, account) { 7 | state.account = account; 8 | } 9 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.iml 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | *.sublime-* 9 | *.swo 10 | *.swp 11 | *.tgz 12 | *.xml 13 | .DS_Store 14 | .idea 15 | .project 16 | .strong-pm 17 | coverage 18 | node_modules 19 | npm-debug.log 20 | build 21 | -------------------------------------------------------------------------------- /template/gulp-tasks/clear-cache.js: -------------------------------------------------------------------------------- 1 | export default function(path) { 2 | const paths = Object.keys(require.cache); 3 | paths.forEach((p) => { 4 | if (p.includes(path)) { 5 | // console.log('clearing', p); 6 | delete require.cache[p]; 7 | } 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /template/server/datasources.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "host": "localhost", 5 | "port": 27017, 6 | "url": "", 7 | "database": "databaseName", 8 | "password": "", 9 | "user": "", 10 | "connector": "mongodb" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /template/common/models/message.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | export default function(Message) { 3 | Message.greet = (msg, cb) => { 4 | process.nextTick(() => { 5 | msg = msg || 'hello'; 6 | cb(null, `Sender says ${msg} to receiver`); 7 | }); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /template/client/store/modules/auth/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import * as mutations from './mutations'; 3 | 4 | export default { 5 | namespaced: true, 6 | state: { 7 | // eslint-disable-next-line camelcase 8 | access_token: null, 9 | account: null, 10 | }, 11 | actions, 12 | mutations, 13 | }; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /template/client/components/HelloWorld/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import {mount} from 'vue-test-utils'; 2 | import HelloWorld from './HelloWorld.vue'; 3 | 4 | describe('HelloWorld.vue', () => { 5 | const wrapper = mount(HelloWorld); 6 | 7 | it('should render correct content', () => { 8 | expect(wrapper.html()).toContain( 9 | 'Hello World! This content is restricted.' 10 | ); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /template/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{name}} 6 | {{#extended}} 7 | 8 | {{/extended}} 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /template/client/view/mixins/notification.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export default { 3 | methods: { 4 | notifySuccess(msg) { 5 | // TODO: show success message 6 | }, 7 | notifyError(error) { 8 | // TODO: show error message 9 | throw error; 10 | }, 11 | notifyWhenSuccess(msg) { 12 | return () => { 13 | // TODO: show success message 14 | }; 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /template/index.spec.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import server from './index'; 3 | 4 | describe('Project Index', () => { 5 | beforeEach((done) => { 6 | server.once('started', done); 7 | server.start(); 8 | }); 9 | 10 | afterEach(() => server.close()); 11 | 12 | it('should serve client files', async() => { 13 | const res = await request(server).get('/index.html'); 14 | expect(res.statusCode).toBe(200); 15 | }, 10000); 16 | }); 17 | -------------------------------------------------------------------------------- /template/client/store/modules/auth/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import * as mutations from './mutations'; 2 | 3 | describe('auth mutations', () => { 4 | it('set access_token', () => { 5 | const state = {}; 6 | mutations.setAccessToken(state, 'foo'); 7 | expect(state.access_token).toEqual('foo'); 8 | }); 9 | 10 | it('set account', () => { 11 | const state = {}; 12 | mutations.setAccount(state, 'foo'); 13 | expect(state.account).toEqual('foo'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "babel-eslint" 5 | }, 6 | "env": { 7 | "browser": true 8 | }, 9 | "extends": [ 10 | "plugin:vue/recommended", 11 | "airbnb-base", 12 | "loopback" 13 | ], 14 | "plugins": [ 15 | "vue" 16 | ], 17 | "globals": { 18 | "expect": true, 19 | "assert": true, 20 | "require": true, 21 | "request": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template/client/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import async from './modules/async'; 5 | import auth from './modules/auth'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | state: { 11 | breadcrumb: [{ 12 | text: 'Home', 13 | to: '/dashboard', 14 | }, { 15 | text: 'Dashboard', 16 | active: true, 17 | }], 18 | }, 19 | modules: { 20 | async, // async namespaced 21 | auth, // auth namespaced 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /template/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "restApiHost": "localhost", 4 | "host": "0.0.0.0", 5 | "port": 8000, 6 | "remoting": { 7 | "context": false, 8 | "rest": { 9 | "handleErrors": false, 10 | "normalizeHttpPath": false, 11 | "xml": false 12 | }, 13 | "json": { 14 | "strict": false, 15 | "limit": "100kb" 16 | }, 17 | "urlencoded": { 18 | "extended": true, 19 | "limit": "100kb" 20 | }, 21 | "cors": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template/common/models/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Message", 3 | "base": "Model", 4 | "properties": {}, 5 | "methods": { 6 | "greet": { 7 | "isStatic": true, 8 | "accepts": [{ 9 | "arg": "msg", 10 | "type": "string", 11 | "http": { 12 | "source": "query" 13 | } 14 | }], 15 | "returns": { 16 | "arg": "greeting", 17 | "type": "string" 18 | }, 19 | "http": { 20 | "verb": "get" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /template/test/utils/create-loopback.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import loopback from 'loopback'; 3 | import boot from 'loopback-boot'; 4 | 5 | export default function(options) { 6 | const defaultOptions = { 7 | appRootDir: path.resolve(__dirname, '../../server'), 8 | scriptExtensions: ['.js', '.json', '.node', '.ejs'], 9 | }; 10 | 11 | return new Promise((resolve) => { 12 | const server = loopback(); 13 | boot( 14 | server, 15 | Object.assign(defaultOptions, options), 16 | () => resolve(server) 17 | ); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /template/client/view/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /template/client/style/app.scss: -------------------------------------------------------------------------------- 1 | @import "global"; 2 | @import "bootstrap/scss/bootstrap"; 3 | 4 | // Customize Bootstrap Components Style 5 | // (copy from node_modules/bootstrap/scss folder 6 | // @import "_breadcrumb"; 7 | // @import "_buttons"; 8 | // @import "_dropdown"; 9 | // @import "_forms"; 10 | // @import "_fullcalendar"; 11 | // @import "_modal"; 12 | @import "_spinner"; 13 | // @import "_tables"; 14 | 15 | $fa-font-path: "../static/fonts"; 16 | @import "font-awesome/scss/font-awesome"; 17 | 18 | #app { 19 | width: 100vw; 20 | height: 100vh; 21 | background-color: darken($light, 40%); 22 | } 23 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgFiles": [ 3 | "package.json" 4 | ], 5 | "src": { 6 | "tagName": "v%s", 7 | "tagAnnotation": "Release v%s", 8 | "commitMessage": "Release v%s" 9 | }, 10 | "github": { 11 | "release": true, 12 | "releaseName": "Release v%s" 13 | }, 14 | "buildCommand": "npx conventional-changelog -p angular -i CHANGELOG.md -s", 15 | "increment": "conventional:angular", 16 | "beforeChangelogCommand": "npx conventional-changelog -p angular -i CHANGELOG.md -s", 17 | "changelogCommand": "npx conventional-changelog -p angular -u | tail -n +3", 18 | "safeBump": false 19 | } 20 | -------------------------------------------------------------------------------- /template/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | }{{#extended}}, 6 | "transient": { 7 | "name": "transient", 8 | "connector": "transient" 9 | }, 10 | "email": { 11 | "name": "email", 12 | "connector": "mail", 13 | "transports": [ 14 | { 15 | "type": "smtp", 16 | "host": "smtp.mailtrap.io", 17 | "secure": false, 18 | "port": 2525, 19 | "auth": { 20 | "user": "bc6242e46c4abb", 21 | "pass": "6dcc9b55c677be" 22 | } 23 | } 24 | ] 25 | } 26 | {{/extended}} 27 | } 28 | -------------------------------------------------------------------------------- /template/client/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /template/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": [ 6 | "> 1%", 7 | "last 2 versions", 8 | "not ie <= 8" 9 | ], 10 | "node": "8" 11 | } 12 | }], 13 | "stage-2" 14 | ], 15 | "plugins": [ 16 | [ 17 | "babel-plugin-root-import", 18 | [ 19 | { 20 | "rootPathPrefix": "~", 21 | "rootPathSuffix": "." 22 | }, 23 | { 24 | "rootPathPrefix": "@", 25 | "rootPathSuffix": "client" 26 | } 27 | ] 28 | ], 29 | [ 30 | "transform-runtime", 31 | { 32 | "polyfill": false, 33 | "regenerator": true 34 | } 35 | ] 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /template/client/style/_spinner.scss: -------------------------------------------------------------------------------- 1 | // Loading Spinner 2 | .fa.fa-spinner { 3 | -webkit-animation-name: rotate; 4 | animation-name: rotate; 5 | -webkit-animation-duration: 2s; 6 | animation-duration: 2s; 7 | -webkit-animation-iteration-count: infinite; 8 | animation-iteration-count: infinite; 9 | -webkit-animation-timing-function: ease-in-out; 10 | animation-timing-function: ease-in-out; 11 | } 12 | 13 | @-webkit-keyframes rotate { 14 | from {-webkit-transform: rotate(0deg);transform: rotate(0deg);} 15 | to {-webkit-transform: rotate(360deg);transform: rotate(360deg);} 16 | } 17 | 18 | @keyframes rotate { 19 | from {-webkit-transform: rotate(0deg);transform: rotate(0deg);} 20 | to {-webkit-transform: rotate(360deg);transform: rotate(360deg);} 21 | } 22 | -------------------------------------------------------------------------------- /template/server/models/account.js: -------------------------------------------------------------------------------- 1 | import {host} from '../config.json'; 2 | 3 | export default function(Account) { 4 | Account.on('resetPasswordRequest', (info) => { 5 | const url = `https://${host}/#!/profile`; 6 | const html = `Hello!

Click here to create a new password.` + 8 | '

If you have not requested a password change,' + 9 | 'please ignore this email.

Webmaster'; 10 | Account.app.models.Email.send({ 11 | to: info.email, 12 | from: '{{name}} ', 13 | subject: '[{{name}}] Create a new password', 14 | html, 15 | }, (err) => { 16 | if (err) return console.log(err); 17 | console.log('> sending password reset email to:', info.email); 18 | return null; 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /template/client/services/loopback.spec.js: -------------------------------------------------------------------------------- 1 | import {DateString} from 'loopback'; 2 | import lb, {DateString as DS} from './loopback'; 3 | 4 | describe('loopback', () => { 5 | it('exports DateString with parse', () => { 6 | expect(DS).toEqual(DateString); 7 | expect(DS.parse).toBeDefined(); 8 | }); 9 | 10 | it('parses a DateString object', () => { 11 | const date = {when: '2017-01-01'}; 12 | const orignParsed = new DateString(date.when); 13 | expect(DS.parse(date)).toEqual(orignParsed); 14 | expect(DS.parse(date.when)).toEqual(orignParsed); 15 | }); 16 | 17 | it('export extended axios', () => { 18 | expect(lb).toEqual(expect.any(Function)); 19 | expect(lb.find).toEqual(expect.any(Function)); 20 | expect(lb.removeToken).toEqual(expect.any(Function)); 21 | expect(lb.setToken).toEqual(expect.any(Function)); 22 | expect(lb.setLoadingFunction).toEqual(expect.any(Function)); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /template/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fallback from 'express-history-api-fallback'; 3 | import path from 'path'; 4 | import app from './server/server'; 5 | 6 | const client = path.resolve(__dirname, 'client'); 7 | 8 | const unless = function(paths, middleware) { 9 | return function(req, res, next) { 10 | if (paths.some(p => req.path.indexOf(p) > -1)) { 11 | return next(); 12 | } 13 | 14 | return middleware(req, res, next); 15 | }; 16 | }; 17 | 18 | // add static route for client 19 | app.use(express.static(client)); 20 | 21 | // enable redirect urls to index 22 | app.use(unless( 23 | ['/api', '/explorer'], 24 | fallback('index.html', {root: client}), 25 | )); 26 | 27 | // start app 28 | app.on('started', () => { 29 | const baseUrl = app.get('url').replace(/\/$/, ''); 30 | console.log('Browse your CLIENT files at %s', baseUrl); 31 | }); 32 | 33 | if (require.main === module) { app.start(); } 34 | 35 | export default app; 36 | -------------------------------------------------------------------------------- /template/client/main.js: -------------------------------------------------------------------------------- 1 | {{#extended}} 2 | /* Required by BootstrapVue */ 3 | import 'babel-polyfill'; 4 | 5 | {{/extended}} 6 | /* Global Components */ 7 | import Vue from 'vue'; 8 | {{#extended}} 9 | import {sync} from 'vuex-router-sync'; 10 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 11 | import BootstrapVue from 'bootstrap-vue'; 12 | // import 'vue-awesome/icons'; 13 | import Icon from 'vue-awesome'; 14 | {{/extended}} 15 | 16 | /* Local Components and modules */ 17 | import App from './App.vue'; 18 | {{#extended}} 19 | import router from './router'; 20 | import store from './store'; 21 | 22 | Vue.use(BootstrapVue); 23 | Vue.component('icon', Icon); 24 | 25 | // Add router state to store 26 | sync(store, router); 27 | {{/extended}} 28 | {{#unless extended}} 29 | import './static/main.css'; 30 | {{/unless}} 31 | 32 | // Instance Application 33 | export default new Vue({ 34 | el: '#app', 35 | render: r => r(App), 36 | {{#extended}} 37 | router, 38 | store, 39 | {{/extended}} 40 | }); 41 | -------------------------------------------------------------------------------- /template/client/store/modules/async/index.js: -------------------------------------------------------------------------------- 1 | import getUID from 'uid'; 2 | import lb from '@/services/loopback'; 3 | 4 | export default { 5 | namespaced: true, 6 | state: { 7 | ajaxCommands: [], 8 | }, 9 | getters: { 10 | loadingAjax(state) { 11 | if (state.ajaxCommands.length > 0) return true; 12 | return false; 13 | }, 14 | }, 15 | actions: { 16 | syncLoopback({commit}) { 17 | lb.setLoadingFunction( 18 | (isLoading, uid = getUID()) => { 19 | if (isLoading) { 20 | commit('addAjaxCommand', uid); 21 | } else { 22 | commit('removeAjaxCommand', uid); 23 | } 24 | return uid; 25 | } 26 | ); 27 | }, 28 | }, 29 | mutations: { 30 | addAjaxCommand(state, uid) { 31 | state.ajaxCommands.push(uid); 32 | }, 33 | removeAjaxCommand(state, uid) { 34 | state.ajaxCommands.splice( 35 | state.ajaxCommands.indexOf(uid), 36 | 1 37 | ); 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /template/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "babel-eslint" 5 | }, 6 | "env": { 7 | "browser": true, 8 | "jest/globals": true 9 | }, 10 | "extends": [ 11 | "plugin:vue/recommended", 12 | "airbnb-base", 13 | "loopback" 14 | ], 15 | "plugins": [ 16 | "vue", 17 | "jest" 18 | ], 19 | "rules": { 20 | "import/no-unresolved": "off", 21 | "import/no-extraneous-dependencies": [ 22 | "error", 23 | { 24 | "devDependencies": [ 25 | "**/*.spec.js", 26 | "__tests__/*.js", 27 | "__mocks__/*.js", 28 | "__snapshots__/*.js", 29 | "test/**/*.js", 30 | "gulp-tasks/**/*.js", 31 | "jest.config.js", 32 | "jest.plugins.js" 33 | ] 34 | } 35 | ], 36 | "import/extensions": "off", 37 | "import/prefer-default-export": "off", 38 | "no-param-reassign": "off", 39 | "one-var-declaration-per-line": "off" 40 | }, 41 | "globals": { 42 | "require": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /template/gulp-tasks/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import path from 'path'; 3 | import pkg from '../package.json'; 4 | 5 | // true if this template was generated with extended option 6 | export const extended = pkg.extended || false; 7 | 8 | export const dirs = {}; 9 | dirs.root = path.resolve(__dirname, '../'); 10 | dirs.modules = path.resolve(dirs.root, 'node_modules'); 11 | dirs.build = path.resolve(dirs.root, 'build'); 12 | dirs.buildTest = path.resolve(dirs.root, dirs.build, 'test'); 13 | dirs.buildClient = path.resolve(dirs.root, dirs.build, 'client'); 14 | dirs.buildServer = path.resolve(dirs.root, dirs.build, 'server'); 15 | dirs.buildCommon = path.resolve(dirs.root, dirs.build, 'common'); 16 | dirs.test = path.resolve(dirs.root, 'test'); 17 | dirs.testClient = path.resolve(dirs.test, 'client'); 18 | dirs.testServer = path.resolve(dirs.test, 'server'); 19 | dirs.srcClient = path.resolve(dirs.root, 'client'); 20 | dirs.srcCommon = path.resolve(dirs.root, 'common'); 21 | dirs.srcServer = path.resolve(dirs.root, 'server'); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-loopback", 3 | "version": "5.2.1", 4 | "description": "A Vue project template with Loopback framework featuring ES6, Gulp, and Mocha for unit tests", 5 | "main": "meta.js", 6 | "scripts": { 7 | "test": "node test.js", 8 | "lint": "npx eslint --ext vue,js ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/InCuca/vue-loopback.git" 13 | }, 14 | "keywords": [ 15 | "vuejs" 16 | ], 17 | "author": "Walker Leite", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/InCuca/vue-loopback/issues" 21 | }, 22 | "homepage": "https://github.com/InCuca/vue-loopback#readme", 23 | "devDependencies": { 24 | "babel-eslint": "^8.2.2", 25 | "conventional-changelog-cli": "^1.3.22", 26 | "eslint": "^4.15.0", 27 | "eslint-config-airbnb-base": "^11.3.0", 28 | "eslint-config-loopback": "^10.0.0", 29 | "eslint-plugin-import": "^2.11.0", 30 | "eslint-plugin-vue": "^4.0.0", 31 | "release-it": "^7.4.3", 32 | "vue-cli": "^2.9.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /template/server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {}, 7 | "cors": { 8 | "params": { 9 | "origin": true, 10 | "credentials": true, 11 | "maxAge": 86400 12 | } 13 | }, 14 | "helmet#xssFilter": {}, 15 | "helmet#frameguard": { 16 | "params": [ 17 | "deny" 18 | ] 19 | }, 20 | "helmet#hsts": { 21 | "params": { 22 | "maxAge": 0, 23 | "includeSubdomains": true 24 | } 25 | }, 26 | "helmet#hidePoweredBy": {}, 27 | "helmet#ieNoOpen": {}, 28 | "helmet#noSniff": {}, 29 | "helmet#noCache": { 30 | "enabled": false 31 | } 32 | }, 33 | "session": {}, 34 | "auth": {}, 35 | "parse": {}, 36 | "routes": { 37 | "loopback#rest": { 38 | "paths": [ 39 | "${restApiRoot}" 40 | ] 41 | } 42 | }, 43 | "files": {}, 44 | "final": { 45 | "loopback#urlNotFound": {} 46 | }, 47 | "final:after": { 48 | "strong-error-handler": {} 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /template/gulp-tasks/compilers.js: -------------------------------------------------------------------------------- 1 | /* eslint no-useless-escape: 0, import/prefer-default-export: 0 */ 2 | import path from 'path'; 3 | import sass from 'vueify/lib/compilers/sass'; 4 | import {dirs} from './config'; 5 | 6 | function replaceCuringas(content) { 7 | // FIXME: Not working 8 | const replacer = `@import "${dirs.srcClient}/$2";`; 9 | 10 | let replaced = content.replace(/@import.+(\@)(.*?)[\"\']/, replacer); 11 | replaced = content.replace(/@import.+(\~)(.*?)[\"\']/, replacer); 12 | return replaced; 13 | } 14 | 15 | export function customSass(content, callback, compiler, filePath) { 16 | let myContent = content; 17 | const relativePath = path.relative( 18 | path.dirname(filePath), 19 | path.resolve(dirs.srcClient, 'style/global.scss') 20 | ).replace(/\\/g, '/'); // Replace backslashes with forward slashes (windows) 21 | 22 | // Global SCSS 23 | // 24 | myContent = `@import "${relativePath}";${myContent}`; 25 | 26 | myContent = replaceCuringas(myContent); 27 | 28 | sass( 29 | myContent, 30 | callback, 31 | compiler, 32 | filePath 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /template/client/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import store from '@/store'; 4 | import Login from './view/Login.vue'; 5 | import Dashboard from './view/Dashboard.vue'; 6 | import Profile from './view/Profile.vue'; 7 | 8 | Vue.use(VueRouter); 9 | 10 | const router = new VueRouter({ 11 | mode: 'history', 12 | routes: [ 13 | { 14 | path: '/', 15 | name: 'home', 16 | redirect: {name: 'dashboard'}, 17 | }, { 18 | path: '/dashboard', 19 | name: 'dashboard', 20 | component: Dashboard, 21 | meta: {requiresAuth: true}, 22 | }, { 23 | path: '/login', 24 | name: 'login', 25 | component: Login, 26 | props: true, 27 | }, { 28 | path: '/profile', 29 | name: 'profile', 30 | component: Profile, 31 | // eslint-disable-next-line camelcase 32 | props: route => ({access_token: route.query.access_token}), 33 | meta: {requiresAuth: true}, 34 | }, 35 | ], 36 | }); 37 | 38 | // Sync routes with auth module 39 | store.dispatch('auth/syncRouter', router); 40 | 41 | export default router; 42 | -------------------------------------------------------------------------------- /template/client/store/modules/async/index.spec.js: -------------------------------------------------------------------------------- 1 | import {createLocalVue} from 'vue-test-utils'; 2 | import Vuex from 'vuex'; 3 | import async from './index'; 4 | 5 | const Vue = createLocalVue(); 6 | Vue.use(Vuex); 7 | 8 | describe('async module', () => { 9 | const initialState = JSON.stringify(async.state); 10 | const store = new Vuex.Store(async); 11 | 12 | beforeEach(() => { 13 | store.replaceState(JSON.parse(initialState)); 14 | }); 15 | 16 | it('has ajaxCommands state', () => { 17 | expect(store.state.ajaxCommands).toEqual([]); 18 | }); 19 | 20 | it('adds command to ajaxCommands', () => { 21 | const cmd = 'foo'; 22 | store.commit('addAjaxCommand', cmd); 23 | expect(store.state.ajaxCommands).toContain(cmd); 24 | expect(store.getters.loadingAjax).toBeTruthy(); 25 | }); 26 | 27 | it('remove command from ajaxCommands', () => { 28 | const cmd = 'foo'; 29 | store.commit('addAjaxCommand', cmd); 30 | store.commit('removeAjaxCommand', cmd); 31 | expect(store.state.ajaxCommands).not.toContain(cmd); 32 | expect(store.getters.loadingAjax).not.toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 InCuca Tecnologia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # {{ name }} 2 | 3 | {{ description }} 4 | 5 | ## Folder structure 6 | 7 | 1. `client`: Vue client files 8 | 2. `common`: Common client and server model files 9 | 3. `server`: Loopback server files 10 | 4. `test`: Testing helpers (do not place unit tests here, but as .spec.js files in client/common/server directories) 11 | 12 | ## Installation 13 | 14 | ``` 15 | $ npm install 16 | ``` 17 | 18 | ## Linting 19 | 20 | ``` 21 | $ npm run lint 22 | ``` 23 | 24 | ## Testing 25 | 26 | ``` 27 | $ npm test 28 | ``` 29 | 30 | ## Running the development server (API and Client) 31 | 32 | ``` 33 | $ npm run dev 34 | ``` 35 | 36 | ## Debug 37 | 38 | ``` 39 | $ DEBUG=loopback npm run dev 40 | ``` 41 | 42 | [More info...](https://loopback.io/doc/en/lb3/Setting-debug-strings.html) 43 | 44 | ## Build to ./build 45 | 46 | ``` 47 | $ npm run build 48 | ``` 49 | 50 | ## Executing built files 51 | 52 | Please remember to update `server/*.production.json` files to match your enviroment. 53 | 54 | ```bash 55 | $ cd build 56 | $ npm run start 57 | ``` 58 | 59 | You can run only the server with: 60 | 61 | ```bash 62 | $ cd build/server 63 | $ node . 64 | ``` 65 | -------------------------------------------------------------------------------- /template/server/models/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Account", 3 | "plural": "Accounts", 4 | "base": "User", 5 | "idInjection": true, 6 | "options": { 7 | "validateUpsert": true 8 | }, 9 | "mixins": { 10 | "TimeStamp": { 11 | "required" : false, 12 | "validateUpsert": true, 13 | "silenceWarnings": false 14 | } 15 | }, 16 | "properties": {}, 17 | "validations": [], 18 | "relations": { 19 | "roleMapping": { 20 | "type": "hasMany", 21 | "model": "RoleMapping", 22 | "foreignKey": "principalId" 23 | }, 24 | "role": { 25 | "type": "hasMany", 26 | "model": "Role", 27 | "through": "RoleMapping", 28 | "foreignKey": "principalId" 29 | } 30 | }, 31 | "acls": [{ 32 | "accessType": "*", 33 | "principalType": "ROLE", 34 | "principalId": "admin", 35 | "permission": "ALLOW" 36 | }, 37 | { 38 | "accessType": "EXECUTE", 39 | "principalType": "ROLE", 40 | "principalId": "$owner", 41 | "permission": "ALLOW", 42 | "property": "update" 43 | } 44 | ], 45 | "methods": {} 46 | } 47 | -------------------------------------------------------------------------------- /template/server/server.js: -------------------------------------------------------------------------------- 1 | import loopback from 'loopback'; 2 | import boot from 'loopback-boot'; 3 | 4 | const app = loopback(); 5 | 6 | const options = { 7 | appRootDir: __dirname, 8 | // File Extensions for jest (strongloop/loopback#3204) 9 | scriptExtensions: ['.js', '.json', '.node', '.ejs'], 10 | }; 11 | 12 | let httpServer; 13 | 14 | app.start = function() { 15 | // start the web server 16 | httpServer = app.listen(() => { 17 | app.emit('started'); 18 | const baseUrl = app.get('url').replace(/\/$/, ''); 19 | console.log('API server listening at: %s', `${baseUrl}/api`); 20 | if (app.get('loopback-component-explorer')) { 21 | const explorerPath = app.get('loopback-component-explorer').mountPath; 22 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 23 | } 24 | }); 25 | return httpServer; 26 | }; 27 | 28 | app.close = function() { 29 | httpServer.close(); 30 | }; 31 | 32 | // Bootstrap the application, configure models, datasources and middleware. 33 | // Sub-apps like REST API are mounted via boot scripts. 34 | boot(app, options, (err) => { 35 | if (err) throw err; 36 | 37 | // start the server if `$ node server.js` 38 | if (require.main === module) { app.start(); } 39 | }); 40 | 41 | // export app 42 | export default app; 43 | -------------------------------------------------------------------------------- /template/client/view/containers/HeaderContainer.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 47 | 48 | 54 | -------------------------------------------------------------------------------- /template/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: [ 4 | 'client/**/*.{js,vue}', 5 | 'server/**/*.js', 6 | 'common/**/*.js', 7 | 'test/**/*.js', 8 | '!**/*.spec.js', 9 | '!**/node_modules/**', 10 | ], 11 | projects: [ 12 | { 13 | displayName: 'test helpers', 14 | testMatch: ['/test/**/*.spec.js'], 15 | }, 16 | { 17 | displayName: 'server', 18 | testMatch: [ 19 | '/server/**/*.spec.js', 20 | '/index.spec.js', 21 | ], 22 | preset: 'jest-preset-loopback', 23 | moduleFileExtensions: [ 24 | 'js', 25 | 'json', 26 | ], 27 | transform: { 28 | '^.+\\.js$': 'babel-jest', 29 | }, 30 | setupTestFrameworkScriptFile: './jest.plugins.js', 31 | }, 32 | { 33 | displayName: 'client', 34 | testMatch: ['/client/**/*.spec.js'], 35 | moduleFileExtensions: [ 36 | 'js', 37 | 'json', 38 | 'vue', 39 | ], 40 | moduleNameMapper: { 41 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', 42 | }, 43 | transform: { 44 | '^.+\\.js$': 'babel-jest', 45 | '^.+\\.vue$': 'vue-jest', 46 | }, 47 | }, 48 | { 49 | displayName: 'common', 50 | testMatch: ['/common/**/*.spec.js'], 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /template/client/App.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | {{#extended}} 3 | import Vuex from 'vuex'; 4 | {{/extended}} 5 | import App from '@/App.vue'; 6 | 7 | describe('App.vue', () => { 8 | let Constructor, vm; 9 | {{#extended}} 10 | const routerView = { 11 | render: r => r('div', 'mocked component'), 12 | }; 13 | {{/extended}} 14 | 15 | beforeEach((done) => { 16 | {{#extended}} 17 | Vue.use(Vuex); 18 | {{/extended}} 19 | Constructor = Vue.extend(App); 20 | vm = new Constructor({ 21 | mounted: () => done(), 22 | {{#extended}} 23 | components: {routerView}, 24 | store: new Vuex.Store({ 25 | modules: { 26 | async: { 27 | namespaced: true, 28 | actions: { 29 | syncLoopback() {}, 30 | }, 31 | }, 32 | }, 33 | }), 34 | {{/extended}} 35 | }); 36 | vm.$mount(); 37 | }); 38 | 39 | afterEach(() => vm.$destroy()); 40 | 41 | {{#extended}} 42 | it('should render router component', () => { 43 | expect(vm.$el.innerHTML).toEqual('mocked component'); 44 | expect(vm.$el.getAttribute('id')).toEqual('app'); 45 | }); 46 | {{else}} 47 | it('should render correct content', () => { 48 | const newVm = new Constructor().$mount(); 49 | return Vue.nextTick().then(() => { 50 | expect(newVm.$el.innerHTML).toEqual('Hello World!'); 51 | }); 52 | }); 53 | {{/extended}} 54 | }); 55 | -------------------------------------------------------------------------------- /meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prompts: { 3 | name: { 4 | type: 'string', 5 | required: true, 6 | message: 'Project name', 7 | }, 8 | description: { 9 | type: 'string', 10 | required: false, 11 | message: 'Project description', 12 | default: 'A Vue.js project', 13 | }, 14 | author: { 15 | type: 'string', 16 | message: 'Author', 17 | }, 18 | extended: { 19 | type: 'confirm', 20 | message: `Add basic Login and Admin views with Vuex, 21 | Vue-router and Bootstrap-vue?`, 22 | }, 23 | }, 24 | filters: { 25 | 'client/router.js': 'extended', 26 | 'client/static/main.css': 'extended === false', 27 | 'client/static/images/**/*': 'extended', 28 | 'client/components/**/*': 'extended', 29 | 'client/services/**/*': 'extended', 30 | 'client/store/**/*': 'extended', 31 | 'client/style/**/*': 'extended', 32 | 'client/view/**/*': 'extended', 33 | 'server/boot/add-initial-data.js': 'extended', 34 | 'server/boot/create-admin.js': 'extended', 35 | 'server/boot/__tests__/create-admin.spec.js': 'extended', 36 | 'server/initial-data/**/*': 'extended', 37 | 'server/models/**/*': 'extended', 38 | 'test/client/components/**/*': 'extended', 39 | 'test/server/account.spec.js': 'extended', 40 | }, 41 | complete(data, {logger}) { 42 | logger.log('To get started:'); 43 | logger.log('1. Install dependencies: npm install'); 44 | logger.log('2. Build with: npm run build'); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /template/test/utils/create-loopback.spec.js: -------------------------------------------------------------------------------- 1 | import loopback from 'loopback'; 2 | import boot from 'loopback-boot'; 3 | import createLoopback from './create-loopback'; 4 | 5 | jest.mock('loopback'); 6 | jest.mock('loopback-boot'); 7 | 8 | describe('createLoopback dev util', () => { 9 | it('exports a promise', () => { 10 | expect(createLoopback()).toBeInstanceOf(Promise); 11 | }); 12 | 13 | it('calls loopback-boot with default options', () => { 14 | const mockServer = {}; 15 | const optsMatcher = { 16 | appRootDir: expect.any(String), 17 | scriptExtensions: expect.any(Array), 18 | }; 19 | loopback.mockReturnValue(mockServer); 20 | createLoopback(); 21 | expect(boot).toBeCalledWith( 22 | mockServer, 23 | expect.objectContaining(optsMatcher), 24 | expect.any(Function), 25 | ); 26 | }); 27 | 28 | it('calls loopback-boot with custom options', () => { 29 | const mockServer = {}; 30 | const myOpts = {myOpt: true}; 31 | loopback.mockReturnValue(mockServer); 32 | createLoopback(myOpts); 33 | expect(boot).toBeCalledWith( 34 | mockServer, 35 | expect.objectContaining(myOpts), 36 | expect.any(Function), 37 | ); 38 | }); 39 | 40 | it('resolves with loopback server', async() => { 41 | const mockServer = {}; 42 | loopback.mockReturnValue(mockServer); 43 | // just call callback fn 44 | boot.mockImplementation((s, o, r) => r()); 45 | const server = await createLoopback(); 46 | expect(server).toEqual(mockServer); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /template/gulp-tasks/copy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | import gulp from 'gulp'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import {argv} from 'yargs'; 6 | import {dirs} from './config'; 7 | import production from '../server/config.production.json'; 8 | 9 | gulp.task('copy:client:fa', () => { 10 | return gulp 11 | .src(path.resolve(dirs.modules, 'font-awesome/fonts/*')) 12 | .pipe(gulp.dest(path.resolve(dirs.buildClient, 'static/fonts'))); 13 | }); 14 | 15 | gulp.task('copy:client', ['copy:client:fa'], () => { 16 | return gulp 17 | .src(`${dirs.srcClient}/**/!(*.vue|*.js)`) 18 | .pipe(gulp.dest(path.resolve(dirs.buildClient))); 19 | }); 20 | 21 | gulp.task('copy:package', () => { 22 | return gulp.src(`${dirs.root}/package.json`) 23 | .pipe(gulp.dest(path.resolve(dirs.build))); 24 | }); 25 | 26 | gulp.task('copy:server', () => { 27 | return gulp.src(`${dirs.srcServer}/**/*.json`) 28 | .pipe(gulp.dest(path.resolve(dirs.buildServer))); 29 | }); 30 | 31 | gulp.task('copy:config:server', ['copy:server'], (done) => { 32 | if (argv.production) { 33 | const configPath = path.resolve( 34 | dirs.srcServer, 35 | 'config.json' 36 | ); 37 | 38 | fs.readFile(configPath, (err, data) => { 39 | if (err) done(err); 40 | 41 | fs.writeFile( 42 | path.resolve(dirs.buildServer, 'config.json'), 43 | JSON.stringify({ 44 | ...JSON.parse(data), 45 | ...production, 46 | }), 47 | done 48 | ); 49 | }); 50 | } else { 51 | done(); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /template/gulp-tasks/serve.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import gutil from 'gulp-util'; 3 | import connect from 'gulp-connect'; 4 | import historyApiFallback from 'connect-history-api-fallback'; 5 | import clearCache from './clear-cache'; 6 | import {dirs} from './config'; 7 | 8 | let server = require('../server/server').default; 9 | 10 | gulp.task('reload:server', ['build:server'], () => { 11 | gutil.log('Reloading server'); 12 | server.close(); 13 | clearCache(dirs.srcServer); 14 | /* eslint-disable-next-line global-require */ 15 | server = require('../server/server').default; 16 | server.start(); 17 | }); 18 | 19 | gulp.task('watch:server', () => { 20 | gulp.watch([ 21 | `${dirs.srcServer}/**/*.js`, 22 | `${dirs.srcServer}/**/*.json`, 23 | `${dirs.srcCommon}/**/*.js`, 24 | ], ['reload:server']); 25 | }); 26 | 27 | gulp.task('reload:client', ['build:client'], () => { 28 | gutil.log('Reloading client'); 29 | gulp.src(dirs.buildClient) 30 | .pipe(connect.reload()); 31 | }); 32 | 33 | gulp.task('watch:client', () => { 34 | gulp.watch([ 35 | `${dirs.srcClient}/**/*`, 36 | `${dirs.srcCommon}/**/*.js`, 37 | ], ['reload:client']); 38 | }); 39 | 40 | gulp.task('serve:server', ['build:server', 'watch:server'], () => { 41 | server.start(); 42 | }); 43 | 44 | gulp.task('serve:client', ['build:client', 'watch:client'], () => { 45 | connect.server({ 46 | name: 'Client App', 47 | root: dirs.buildClient, 48 | livereload: true, 49 | middleware: () => [historyApiFallback()], 50 | }); 51 | }); 52 | 53 | gulp.task('serve', ['serve:server', 'serve:client']); 54 | -------------------------------------------------------------------------------- /template/server/boot/create-admin.js: -------------------------------------------------------------------------------- 1 | import initialAccount from '../initial-data/maintenance-account.json'; 2 | 3 | /** 4 | * Create the first admin user if there are not users in the system 5 | */ 6 | export default function createAdmin(server) { 7 | const Account = server.models.Account; 8 | const Role = server.models.Role; 9 | const RoleMapping = server.models.RoleMapping; 10 | 11 | return Account 12 | .find() 13 | .then((accounts) => { 14 | if (accounts.length < 1) { 15 | return Account.create(initialAccount); 16 | } 17 | return null; 18 | }) 19 | .then((account) => { 20 | if (account) { 21 | return Role 22 | .find({name: 'admin'}) 23 | .then((roles) => { 24 | if (roles.length < 1) { 25 | return Role.create({ 26 | name: 'admin', 27 | }); 28 | } 29 | 30 | return roles[0]; 31 | }) 32 | .then(role => 33 | // resolve with a payload 34 | ({account, role}) 35 | ); 36 | } 37 | return null; 38 | }) 39 | .then((payload) => { // get account and role from payload 40 | if (payload && payload.account && payload.role) { 41 | const myPayload = {...payload}; 42 | return myPayload.role.principals.create({ 43 | principalType: RoleMapping.USER, 44 | principalId: myPayload.account.id, 45 | }).then((principal) => { 46 | myPayload.principal = principal; 47 | return myPayload; 48 | }); 49 | } 50 | return null; 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /template/client/style/global.scss: -------------------------------------------------------------------------------- 1 | // CAUTION: This file will be duplicated in every componentscss style scope 2 | //; HINT: Use this for global variables and mixins 3 | // 4 | // 5 | // Color system 6 | //; 7 | // $primary: #5478c0; 8 | // $secondary: #7c8386; 9 | // $success: #44b6ae; 10 | // $info: #444b4e; 11 | // $warning: #ffc85f; 12 | // $danger: #fc5e66; 13 | // $light: #e9edef; 14 | // $light2: #a2a6a8; 15 | // $dark: #1e2123; 16 | // $blue: #5478c0; 17 | // $purple: #944fc0; 18 | 19 | // Fonts 20 | // $font-size-base: .875rem; 21 | // $font-weight-bold: 700; 22 | // $font-family-sans-serif: "OpenSans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 23 | // $font-family-monospace: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 24 | 25 | // Links 26 | // $link-color: $secondary; 27 | 28 | // Components 29 | // $border-radius: 0px; 30 | // $border-radius-lg: 0px; 31 | // $border-radius-sm: 0px; 32 | 33 | // Buttons 34 | // $btn-border-radius: 1.250em; 35 | // $btn-border-radius-lg: 1.250em; 36 | // $btn-border-radius-sm: 1.250em; 37 | // $btn-font-weight: $font-weight-bold; 38 | 39 | // Forms 40 | // $input-color: $secondary; 41 | // $input-border-color: $light; 42 | 43 | // Breadcrumbs 44 | // $breadcrumb-bg: transparent; 45 | // $breadcrumb-divider-color: $light2; 46 | // $breadcrumb-active-color: $light2; 47 | 48 | // Body 49 | // $body-color: $secondary; 50 | 51 | // Import Global Bootstrap Variables 52 | // (the previous variables will not be overitten) 53 | @import "bootstrap/scss/_functions"; 54 | @import "bootstrap/scss/_variables"; 55 | -------------------------------------------------------------------------------- /template/server/boot/__tests__/create-admin.spec.js: -------------------------------------------------------------------------------- 1 | import createLoopback from '~/test/utils/create-loopback'; 2 | import {email} from '~/server/initial-data/maintenance-account'; 3 | import createAdmin from '~/server/boot/create-admin'; 4 | 5 | async function prepareLoopback(undoBoot = false) { 6 | const server = await createLoopback(); 7 | const Account = server.models.Account; 8 | const Role = server.models.Role; 9 | const RoleMapping = server.models.RoleMapping; 10 | if (undoBoot) { 11 | let info = await Account.deleteAll({email}); 12 | if (info.count < 1) throw Error('account not deleted'); 13 | info = await Role.deleteAll({name: 'admin'}); 14 | if (info.count < 1) throw Error('role not deleted'); 15 | } 16 | 17 | return {server, Account, Role, RoleMapping}; 18 | } 19 | 20 | describe('boot create-admin', () => { 21 | it('adds admin, role and roleMapping', async() => { 22 | const {server, RoleMapping} = await prepareLoopback(true); 23 | const result = await createAdmin(server); 24 | 25 | expect(result).not.toBe(null); 26 | expect(result.account).toMatchObject({email}); 27 | expect(result.role).toMatchObject({name: 'admin'}); 28 | expect(result.principal).toMatchObject({ 29 | principalType: RoleMapping.USER, 30 | }); 31 | }); 32 | 33 | it('do not add account if already exists', async() => { 34 | const {server} = await prepareLoopback(); 35 | const result = await createAdmin(server); 36 | expect(result).toBe(null); 37 | }); 38 | 39 | it('do not add role if already exists', async() => { 40 | const {server, Role} = await prepareLoopback(true); 41 | const created = await Role.create({name: 'admin'}); 42 | const result = await createAdmin(server); 43 | 44 | expect(result).not.toBe(null); 45 | expect(result.role).toMatchObject(created); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-loopback 2 | [![Travis](https://img.shields.io/travis/InCuca/vue-loopback/master.svg)](https://travis-ci.org/InCuca/vue-loopback/branches) 3 | 4 | A Vue project template with [Loopback](http://loopback.io/) framework featuring ES6, Gulp, and Jest for unit tests 5 | 6 | > This template is for Vue 2.x **only** with [vue-cli](https://github.com/vuejs/vue-cli). 7 | 8 | ## Features 9 | 10 | * Loopback service using [axios](https://github.com/axios/axios) at `client/services/loopback`; 11 | * Full authentication support, by default the account listed in `server/initial-data/maintenance-account.json` is created; 12 | * Ajax Async queue module in `client/modules/async` (useful to see if and how many requests are being made to the server); 13 | * [CSS Modules](https://github.com/css-modules/css-modules), [Sass](https://sass-lang.com/) and [Bootstrap Vue](https://bootstrap-vue.js.org). 14 | ## Usage 15 | 16 | ``` 17 | $ npm install -g vue-cli 18 | $ vue init InCuca/vue-loopback project-name 19 | $ npm install 20 | ``` 21 | 22 | ## Folder structure 23 | 24 | 1. `client`: Vue client files 25 | 2. `common`: Common client and server model files 26 | 3. `server`: Loopback server files 27 | 4. `test`: Unit test 28 | 29 | ## Linting 30 | 31 | ``` 32 | $ npm run lint 33 | ``` 34 | 35 | ## Testing 36 | 37 | ``` 38 | $ npm test 39 | ``` 40 | 41 | ## Running the development server (API and Client) 42 | 43 | ``` 44 | $ npm run dev 45 | ``` 46 | 47 | ## Debug 48 | 49 | ``` 50 | $ DEBUG=loopback npm run dev 51 | ``` 52 | 53 | [More info...](https://loopback.io/doc/en/lb3/Setting-debug-strings.html) 54 | 55 | ## Build to ./build 56 | 57 | ``` 58 | $ npm run build 59 | ``` 60 | 61 | ## Executing built files 62 | 63 | Please remember to update `server/*.production.json` files to match your enviroment. 64 | 65 | ```bash 66 | $ cd build 67 | $ npm run start 68 | ``` 69 | -------------------------------------------------------------------------------- /template/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | {{#extended}} 13 | "../node_modules/loopback-ds-timestamp-mixin", 14 | {{/extended}} 15 | "../common/mixins", 16 | "./mixins" 17 | ] 18 | }, 19 | "User": { 20 | "dataSource": "db"{{#extended}}, 21 | "public": false{{/extended}} 22 | }, 23 | "AccessToken": { 24 | "dataSource": "db", 25 | "public": false{{#extended}}, 26 | "relations": { 27 | "account": { 28 | "type": "belongsTo", 29 | "model": "Account", 30 | "foreignKey": "userId" 31 | } 32 | }{{/extended}} 33 | }, 34 | "ACL": { 35 | "dataSource": "db", 36 | "public": false 37 | }, 38 | "RoleMapping": { 39 | "dataSource": "db", 40 | "public": false, 41 | "options": { 42 | "strictObjectIDCoercion": true 43 | }{{#extended}}, 44 | "relations": { 45 | "account": { 46 | "type": "belongsTo", 47 | "model": "Account", 48 | "foreignKey": "userId" 49 | }, 50 | "role": { 51 | "type": "belongsTo", 52 | "model": "Role", 53 | "foreignKey": "roleId" 54 | } 55 | }{{/extended}} 56 | }, 57 | "Role": { 58 | "dataSource": "db", 59 | "public": false{{#extended}}, 60 | "relations": { 61 | "account": { 62 | "type": "hasMany", 63 | "model": "Account", 64 | "through": "RoleMapping", 65 | "foreignKey": "roleId" 66 | } 67 | } 68 | }, 69 | "Account": { 70 | "dataSource": "db", 71 | "public": true 72 | }, 73 | "Email": { 74 | "dataSource": "email"{{/extended}} 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /template/client/services/loopback.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {restApiHost, restApiRoot, port} from '~/server/config.json'; 3 | import {DateString} from 'loopback'; 4 | 5 | DateString.parse = function(dateString) { 6 | const date = dateString.when || dateString; 7 | return new DateString(date); 8 | }; 9 | 10 | export {DateString}; 11 | 12 | const Storage = window.localStorage; 13 | 14 | /** 15 | * Add a token in the local storage 16 | * */ 17 | function exportTokenToLocalStorage(token) { 18 | if (Storage) { 19 | Storage.setItem('loopback-token', JSON.stringify(token)); 20 | } 21 | } 22 | 23 | /** 24 | * Remove token from local storage 25 | */ 26 | function removeTokenFromLocalStorage() { 27 | if (Storage) { 28 | Storage.removeItem('loopback-token'); 29 | } 30 | } 31 | 32 | function addTokenFromLocalStorage(http) { 33 | const token = Storage && Storage.getItem('loopback-token'); 34 | if (token) http.setToken(JSON.parse(token), false); 35 | } 36 | 37 | const http = axios.create({ 38 | baseURL: `http://${restApiHost}:${port}${restApiRoot}`, 39 | }); 40 | 41 | // Current setLoading function 42 | let setLoading = () => { 43 | console.warn('[loopback service] setLoadingFunction is not defined'); 44 | }; 45 | 46 | http.setLoadingFunction = (fn) => { 47 | setLoading = fn; 48 | }; 49 | 50 | http.setToken = (token, save = true) => { 51 | http.token = token; 52 | http.defaults.headers.common.Authorization = token.id; 53 | if (save) exportTokenToLocalStorage(token); 54 | }; 55 | 56 | http.removeToken = () => { 57 | delete http.defaults.headers.common.Authorization; 58 | removeTokenFromLocalStorage(); 59 | }; 60 | 61 | http.find = (endpoint, filter) => http.get(endpoint, {params: {filter}}); 62 | 63 | /* Response Interceptors */ 64 | const interceptResErrors = (err) => { 65 | // console.log('error', err); 66 | try { 67 | setLoading( 68 | false, 69 | err.config.uid || err.response.config.uid 70 | ); 71 | err = Object.assign(new Error(), err.response.data.error); 72 | } catch (e) { 73 | // Will return err if something goes wrong 74 | } 75 | return Promise.reject(err); 76 | }; 77 | const interceptResponse = (res) => { 78 | // console.log('response', res.config); 79 | setLoading(false, res.config.uid); 80 | try { 81 | return res.data; 82 | } catch (e) { 83 | return res; 84 | } 85 | }; 86 | http.interceptors.response.use(interceptResponse, interceptResErrors); 87 | 88 | // Set storage Token in http if exists 89 | addTokenFromLocalStorage(http); 90 | 91 | /* Request Interceptors */ 92 | const interceptReqErrors = err => Promise.reject(err); 93 | const interceptRequest = (config) => { 94 | config.uid = setLoading(true, config.uid); 95 | // console.log('request', config); 96 | return config; 97 | }; 98 | http.interceptors.request.use(interceptRequest, interceptReqErrors); 99 | 100 | export default http; 101 | 102 | // Documentation: https://github.com/axios/axios 103 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const {spawn} = require('child_process'); 2 | 3 | process.setMaxListeners(20); // increase listeners limit 4 | 5 | const initQuestions = [ 6 | {search: /^\?.+/, response: '\n'}, 7 | ]; 8 | 9 | const initNoExtQuestions = [ 10 | {search: /^\? Add basic Login and Admin.+/, response: 'No\n'}, 11 | {search: /^\?.+/, response: '\n'}, 12 | ]; 13 | 14 | /* eslint-disable max-len */ 15 | const commands = [ 16 | {cmd: 'rm', args: ['-r', 'test-project'], ignoreErrors: true}, 17 | {cmd: './node_modules/.bin/vue', args: ['init', '.', 'test-project'], responses: initQuestions}, 18 | {cmd: 'npm', args: ['install'], cwd: 'test-project'}, 19 | {cmd: 'npm', args: ['run', 'lint'], cwd: 'test-project'}, 20 | {cmd: 'npm', args: ['run', 'test'], cwd: 'test-project'}, 21 | {cmd: 'npm', args: ['run', 'build'], cwd: 'test-project'}, 22 | {cmd: 'rm', args: ['-r', 'test-project'], ignoreErrors: true}, 23 | {cmd: './node_modules/.bin/vue', args: ['init', '.', 'test-project'], responses: initNoExtQuestions}, 24 | {cmd: 'npm', args: ['install'], cwd: 'test-project'}, 25 | {cmd: 'npm', args: ['run', 'lint'], cwd: 'test-project'}, 26 | {cmd: 'npm', args: ['run', 'test'], cwd: 'test-project'}, 27 | {cmd: 'npm', args: ['run', 'build'], cwd: 'test-project'}, 28 | ]; 29 | /* eslint-enable max-len */ 30 | 31 | function executeCommand(command, index) { 32 | return new Promise((resolve, reject) => { 33 | const cp = spawn(command.cmd, command.args, {cwd: command.cwd}); 34 | process.on('exit', cp.kill); 35 | cp.stdout.setEncoding('utf-8'); 36 | cp.stdin.setEncoding('utf-8'); 37 | cp.stderr.setEncoding('utf-8'); 38 | 39 | // Ignore pipe errors 40 | cp.stdin.on('error', () => {}); 41 | cp.stdout.on('error', () => {}); 42 | 43 | cp.stdout.pipe(process.stdout); 44 | cp.stderr.pipe(process.stderr); 45 | 46 | let rejected = false; 47 | if (!rejected && command.responses) { 48 | const registerResponse = (q) => { 49 | cp.stdout.on('data', (output) => { 50 | if (q.search.test(output)) { 51 | // console.log('sending', q); 52 | cp.stdin.write(q.response); 53 | } 54 | }); 55 | }; 56 | 57 | command.responses.forEach(registerResponse); 58 | } 59 | cp.once('error', (code) => { 60 | if (!rejected) { 61 | reject(code); 62 | rejected = true; 63 | } 64 | }); 65 | 66 | cp.once('exit', (code) => { 67 | if (code) { 68 | reject(code); 69 | rejected = true; 70 | } else { 71 | console.log( 72 | '=> process', 73 | (index + 1), 74 | 'of', 75 | commands.length, 76 | 'exit with status', 77 | code 78 | ); 79 | resolve(code); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | commands.reduce( 86 | (prev, next, index) => 87 | prev.then(() => 88 | executeCommand(next, index).catch((code) => { 89 | console.log('child process exit with', code); 90 | if (!next.ignoreErrors) process.exit(code); 91 | })), 92 | Promise.resolve() 93 | ); 94 | -------------------------------------------------------------------------------- /template/server/models/account.spec.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import createLoopback from '~/test/utils/create-loopback'; 3 | import AccountFactory from './account'; 4 | import {host} from '../config.json'; 5 | 6 | describe('Account unit', () => { 7 | const AccountMock = { 8 | on: jest.fn(), 9 | app: {models: {Email: {send: jest.fn()}}}, 10 | }; 11 | 12 | beforeEach(() => { 13 | jest.resetAllMocks(); 14 | }); 15 | 16 | it('add resetPasswordRequest remote method', () => { 17 | AccountFactory(AccountMock); 18 | expect(AccountMock.on).toBeCalledWith( 19 | 'resetPasswordRequest', 20 | expect.any(Function), 21 | ); 22 | }); 23 | 24 | it('calls send method from Email model', () => { 25 | const infoMock = { 26 | email: 'foo@bar.net', 27 | accessToken: { 28 | id: 'foobar', 29 | }, 30 | }; 31 | AccountMock.on.mockImplementation((_, fn) => { 32 | fn(infoMock); 33 | }); 34 | console.log = jest.fn(); 35 | AccountMock.app.models.Email.send.mockImplementation((_, errFn) => { 36 | errFn(); 37 | }); 38 | AccountFactory(AccountMock); 39 | expect(AccountMock.app.models.Email.send).toBeCalledWith( 40 | expect.objectContaining({ 41 | to: infoMock.email, 42 | from: expect.stringContaining( 43 | 'noreply@mydomain.com' 44 | ), 45 | subject: expect.stringContaining( 46 | 'Create a new password' 47 | ), 48 | html: expect.stringMatching( 49 | new RegExp(`${host}.*${infoMock.accessToken.id}`) 50 | ), 51 | }), 52 | expect.any(Function), 53 | ); 54 | expect(console.log).toBeCalledWith( 55 | expect.any(String), 56 | expect.stringContaining(infoMock.email) 57 | ); 58 | }); 59 | 60 | it('console log when Email send throws', () => { 61 | const infoMock = { 62 | email: 'foo@bar.net', 63 | accessToken: { 64 | id: 'foobar', 65 | }, 66 | }; 67 | AccountMock.on.mockImplementation((_, fn) => { 68 | fn(infoMock); 69 | }); 70 | console.log = jest.fn(); 71 | AccountMock.app.models.Email.send.mockImplementation((_, errFn) => { 72 | errFn('foo'); 73 | }); 74 | AccountFactory(AccountMock); 75 | expect(console.log).toBeCalledWith('foo'); 76 | }); 77 | }); 78 | 79 | describe('Account e2e', () => { 80 | const email = '936ue5+4bnywbeje42pw@sharklasers.com'; 81 | let server, testAccount, Account; 82 | 83 | beforeEach(async() => { 84 | server = await createLoopback(); 85 | Account = server.models.Account; 86 | testAccount = await Account.create({ 87 | email, 88 | password: 'IuhEW7HI#&HUH3', 89 | }); 90 | }); 91 | 92 | afterEach(() => Account.destroyById(testAccount.id)); 93 | 94 | it('has been correctly declared', () => { 95 | expect(Account).toInherits(server.models.User); 96 | }); 97 | 98 | it( 99 | 'should send reset email to test user', 100 | () => request(server) 101 | .post('/api/Accounts/reset') 102 | .send({email}) 103 | .expect(204), 104 | 30000, 105 | ); 106 | }); 107 | -------------------------------------------------------------------------------- /template/server/boot/__tests__/boot.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | import request from 'supertest'; 3 | import createLoopback from '~/test/utils/create-loopback'; 4 | {{#extended}} 5 | import initialAccount from '~/server/initial-data/maintenance-account.json'; 6 | {{/extended}} 7 | 8 | describe('boot process', () => { 9 | let server; 10 | 11 | beforeEach(() => { 12 | return createLoopback().then((s) => { 13 | server = s; 14 | }); 15 | }); 16 | 17 | afterEach((done) => { 18 | // Clear memory database 19 | server.dataSources.db.automigrate(done); 20 | }); 21 | 22 | describe('root.js', () => { 23 | it('should return server status by root.js', (done) => { 24 | const conn = server.listen(8000, () => { 25 | request(server).get('/api').then((res) => { 26 | expect(res.statusCode).toBe(200); 27 | expect(res.body).toHaveProperty('started'); 28 | expect(res.body).toHaveProperty('uptime'); 29 | conn.close(done); 30 | }); 31 | }); 32 | }); 33 | }); 34 | {{#extended}} 35 | describe('authentication.js', () => { 36 | it('should enable authentication by authentication.js', () => { 37 | expect(server.isAuthEnabled).toEqual(true); 38 | }); 39 | }); 40 | 41 | describe('email configuration', () => { 42 | it('should have Email model', () => { 43 | expect(server.models).toHaveProperty('Email'); 44 | }); 45 | 46 | it('Email model should send email', (done) => { 47 | server.models.Email.send({ 48 | from: 'noreply@fakeserver.mailtrap.io', 49 | to: '92y0zm+7xhtk2ni75mas@grr.la', 50 | subject: 'Testing email', 51 | text: 'Testing email text', 52 | html: 'Testing email text', 53 | }, done); 54 | }, 30000); 55 | }); 56 | 57 | describe('create-admin.js', () => { 58 | it('should have Account model', () => { 59 | expect(server.models).toHaveProperty('Account'); 60 | }); 61 | 62 | it('should create a default admin user', () => { 63 | return server.models.Account.find().then((res) => { 64 | expect(res).toHaveLength(1); 65 | expect(res[0]).toHaveProperty('createdAt'); 66 | expect(res[0]).toHaveProperty('updatedAt'); 67 | expect(res[0].id).toEqual(1); 68 | expect(res[0].email).toEqual(initialAccount.email); 69 | expect(res[0].password).toBeDefined(); 70 | }); 71 | }); 72 | 73 | it('should create a default admin role', () => { 74 | return server.models.Role.find().then((res) => { 75 | expect(res).toHaveLength(1); 76 | expect(res[0]).toHaveProperty('created'); 77 | expect(res[0]).toHaveProperty('modified'); 78 | expect(res[0].id).toEqual(1); 79 | expect(res[0].name).toEqual('admin'); 80 | }); 81 | }); 82 | 83 | it('should create RoleMapping entry for admin', () => { 84 | const RoleMapping = server.models.RoleMapping; 85 | return RoleMapping.find().then((res) => { 86 | expect(res).toHaveLength(1); 87 | expect(res[0].id).toEqual(1); 88 | expect(res[0].roleId).toEqual(1); 89 | expect(res[0].principalId).toEqual(1); 90 | expect(res[0].principalType).toEqual(RoleMapping.USER); 91 | }); 92 | }); 93 | }); 94 | {{/extended}} 95 | }); 96 | -------------------------------------------------------------------------------- /template/server/server.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | describe('server', () => { 3 | let loopback, boot; 4 | const mockApp = {}; 5 | const getApp = () => require('./server').default; 6 | 7 | beforeEach(() => { 8 | jest.resetModules(); 9 | jest.mock('loopback'); 10 | jest.mock('loopback-boot'); 11 | loopback = require('loopback'); 12 | boot = require('loopback-boot'); 13 | loopback.mockReturnValue(mockApp); 14 | }); 15 | 16 | it('instances loopback', () => { 17 | getApp(); 18 | expect(loopback).toBeCalled(); 19 | }); 20 | 21 | it('adds start function', () => { 22 | expect(getApp().start).toBeInstanceOf(Function); 23 | }); 24 | 25 | it('adds close function', () => { 26 | expect(getApp().close).toBeInstanceOf(Function); 27 | }); 28 | 29 | it('boot application', () => { 30 | getApp(); 31 | expect(boot).toBeCalledWith( 32 | mockApp, 33 | expect.objectContaining({ 34 | appRootDir: __dirname, 35 | scriptExtensions: expect.arrayContaining([ 36 | '.js', '.json', '.node', '.ejs', 37 | ]), 38 | }), 39 | expect.any(Function), 40 | ); 41 | }); 42 | 43 | it('throws when application boot fails', () => { 44 | boot.mockImplementation((a, o, done) => done('foo')); 45 | expect(getApp).toThrowError('foo'); 46 | }); 47 | 48 | it('not throws when application boot success', () => { 49 | boot.mockImplementation((a, o, done) => done()); 50 | expect(getApp).not.toThrow(); 51 | }); 52 | 53 | it('calls express listen when start is called', () => { 54 | const listen = jest.fn(); 55 | loopback.mockReturnValue({listen}); 56 | getApp().start(); 57 | expect(listen).toBeCalledWith(expect.any(Function)); 58 | }); 59 | 60 | it('emit started when start is called', () => { 61 | const get = jest.fn(() => 'foo'); 62 | const listen = jest.fn(cb => cb()); 63 | const emit = jest.fn(); 64 | loopback.mockReturnValue({listen, get, emit}); 65 | getApp().start(); 66 | expect(emit).toBeCalledWith('started'); 67 | }); 68 | 69 | it('log api endpoint when start is called', () => { 70 | const get = jest.fn(() => 'foo'); 71 | const listen = jest.fn(cb => cb()); 72 | const emit = jest.fn(); 73 | console.log = jest.fn(); 74 | loopback.mockReturnValue({listen, get, emit}); 75 | getApp().start(); 76 | expect(console.log).toBeCalledWith( 77 | expect.any(String), 78 | 'foo/api', 79 | ); 80 | }); 81 | 82 | it('log explorer url when start is called', () => { 83 | const get = jest.fn( 84 | term => (term === 'url' ? 'url' : {mountPath: 'foo'}) 85 | ); 86 | const listen = jest.fn(cb => cb()); 87 | const emit = jest.fn(); 88 | console.log = jest.fn(); 89 | loopback.mockReturnValue({listen, get, emit}); 90 | getApp().start(); 91 | expect(console.log).toBeCalledWith( 92 | expect.any(String), 93 | 'url', 94 | 'foo', 95 | ); 96 | }); 97 | 98 | it('close server when close is called', () => { 99 | const close = jest.fn(); 100 | const listen = jest.fn(() => ({close})); 101 | loopback.mockReturnValue({listen}); 102 | getApp().start(); 103 | getApp().close(); 104 | expect(close).toBeCalled(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "engines": { 6 | "node": ">=8.9.4" 7 | }, 8 | "scripts": { 9 | "lint": "npx eslint --ext vue,js .", 10 | "dev": "gulp serve", 11 | "build": "gulp build --production && npm run afterbuild", 12 | "afterbuild": "cd build && npm install --production", 13 | "test": "npx jest", 14 | "start": "NODE_ENV=production node ." 15 | }, 16 | "dependencies": { 17 | "compression": "^1.0.3", 18 | "cors": "^2.5.2", 19 | "helmet": "^1.3.0", 20 | "http-server": "^0.11.1", 21 | "express": "^4.16.2", 22 | "express-history-api-fallback": "^2.2.1", 23 | "loopback": "^3.16.2", 24 | "loopback-boot": "github:klarkc/loopback-boot#master", 25 | "loopback-component-explorer": "^4.0.0", 26 | "serve-favicon": "^2.0.1", 27 | "strong-error-handler": "^2.0.0", 28 | "vue": "^2.5.3"{{#extended}}, 29 | "axios": "^0.17.1", 30 | "babel-polyfill": "^6.26.0", 31 | "bootstrap": "^4.0.0-beta.2", 32 | "bootstrap-vue": "^1.2.0", 33 | "font-awesome": "^4.7.0", 34 | "install": "^0.10.1", 35 | "lodash.defaultsdeep": "^4.6.0", 36 | "loopback-ds-timestamp-mixin": "^3.4.1", 37 | "uid": "0.0.2", 38 | "vue-awesome": "^2.3.4", 39 | "vue-router": "^3.0.1", 40 | "vue-template-compiler": "^2.5.3", 41 | "vuex": "^3.0.1", 42 | "vuex-router-sync": "^5.0.0" 43 | {{/extended}} 44 | }, 45 | "devDependencies": { 46 | "aliasify": "^2.1.0", 47 | "babel-core": "^6.26.3", 48 | "babel-eslint": "^8.2.2", 49 | "babel-jest": "^23.0.1", 50 | "regenerator-runtime": "^0.11.1", 51 | "supertest": "^3.1.0", 52 | "babel-plugin-transform-runtime": "^6.23.0", 53 | "babel-preset-env": "^1.6.1", 54 | "babel-preset-stage-2": "^6.24.1", 55 | "babelify": "^7.3.0", 56 | "browserify": "^14.4.0", 57 | "browserify-global-shim": "^1.0.3", 58 | "css-modulesify": "^0.28.0", 59 | "eslint": "^4.15.0", 60 | "eslint-config-airbnb-base": "^11.3.0", 61 | "eslint-plugin-import": "^2.9.0", 62 | "eslint-config-loopback": "^8.0.0", 63 | "eslint-plugin-vue": "^4.0.0", 64 | "eslint-plugin-jest": "^21.17.0", 65 | "gulp": "^3.9.1", 66 | "gulp-autoprefixer": "^4.0.0", 67 | "gulp-babel": "^6.1.2", 68 | "gulp-connect": "^5.0.0", 69 | "gulp-sourcemaps": "^2.6.0", 70 | "identity-obj-proxy": "^3.0.0", 71 | "jest": "^22.4.4", 72 | "jest-plugins": "^2.9.0", 73 | "jest-preset-loopback": "^1.0.0", 74 | "loopback-jest": "^1.3.0", 75 | "supertest": "^3.1.0", 76 | "tfilter": "^1.0.1", 77 | "through2": "^2.0.3", 78 | "tmp": "0.0.33", 79 | "vinyl-buffer": "^1.0.0", 80 | "vinyl-source-stream": "^1.1.0", 81 | "vue-jest": "^2.6.0", 82 | "vue-test-utils": "^1.0.0-beta.11", 83 | "vueify": "^9.4.1", 84 | "yargs": "^10.0.3", 85 | "babel-plugin-root-import": "^5.1.0", 86 | "connect-history-api-fallback": "^1.5.0", 87 | "node-sass": "^4.6.0", 88 | "gulp-util": "^3.0.8", 89 | "babel-register": "^6.26.0" 90 | }, 91 | "repository": { 92 | "type": "", 93 | "url": "" 94 | }, 95 | "license": "UNLICENSED", 96 | "description": "{{ description }}"{{#extended}}, 97 | "extended": true 98 | {{/extended}} 99 | } 100 | -------------------------------------------------------------------------------- /template/gulp-tasks/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | import gulp from 'gulp'; 3 | import sourcemaps from 'gulp-sourcemaps'; 4 | import babel from 'gulp-babel'; 5 | import path from 'path'; 6 | import browserify from 'browserify'; 7 | import vueify from 'vueify'; 8 | import babelify from 'babelify'; 9 | import tfilter from 'tfilter'; 10 | import modulesify from 'css-modulesify'; 11 | import aliasify from 'aliasify'; 12 | import source from 'vinyl-source-stream'; 13 | import buffer from 'vinyl-buffer'; 14 | import {argv} from 'yargs'; 15 | import {dirs, extended} from './config'; 16 | import {customSass} from './compilers'; 17 | 18 | gulp.task('build:client', ['copy:client', 'copy:config:server'], () => { 19 | // Node modules to be included in babel transpilation 20 | // Use this with ES6 modules for example 21 | const bModules = []; 22 | 23 | if (extended) bModules.push('bootstrap-vue/es'); 24 | 25 | vueify.compiler.applyConfig({ 26 | sass: { 27 | includePaths: [ 28 | dirs.modules, 29 | ], 30 | }, 31 | customCompilers: { 32 | scss: customSass, 33 | }, 34 | }); 35 | 36 | const b = browserify({ 37 | entries: [ 38 | path.resolve(dirs.srcClient, 'main.js'), 39 | ...bModules.map(mod => path.resolve(dirs.modules, mod)), 40 | ], 41 | debug: true, 42 | }); 43 | 44 | // Transpile .vue 45 | b.transform(vueify); 46 | 47 | // Babel Settings with module filter 48 | const bSettings = { 49 | plugins: ['transform-runtime'], 50 | }; 51 | 52 | b.transform(tfilter( 53 | babelify, 54 | { 55 | filter: (filename) => { 56 | const exception = bModules.some( 57 | mod => filename.includes( 58 | path.resolve(dirs.modules, mod) 59 | ) 60 | ); 61 | if (exception) { 62 | // node module 63 | // console.log('Transpiling module', filename); 64 | return true; 65 | } else if (!filename.includes(dirs.modules)) { 66 | // filter not in node_modules 67 | // console.log('Transpiling src', filename); 68 | return true; 69 | } 70 | // console.log('NOT transpiling', filename); 71 | return false; 72 | }, 73 | }, 74 | { 75 | ...bSettings, 76 | global: true, 77 | presets: ['env'], 78 | } 79 | )); 80 | 81 | b.plugin(modulesify, { 82 | output: path.resolve( 83 | dirs.buildClient, 84 | 'bundle.css' 85 | ), 86 | global: true, 87 | generateScopedName(name, filename) { 88 | const matches = filename.match(/^\/node_modules/); 89 | if (matches) return name; 90 | if (process.env.NODE_ENV === 'production') { 91 | return modulesify.generateShortName(name, filename); 92 | } 93 | return modulesify.generateLongName(name, filename); 94 | }, 95 | }); 96 | 97 | if (argv.production) { 98 | b.transform(aliasify, { 99 | replacements: { 100 | '(.+)server/config.json$': path.resolve( 101 | dirs.buildServer, 102 | 'config.json', 103 | ), 104 | }, 105 | verbose: true, 106 | }); 107 | } 108 | 109 | return b.bundle() 110 | .pipe(source('bundle.js')) 111 | .pipe(buffer()) 112 | .pipe(sourcemaps.init({loadMaps: true})) 113 | .pipe(sourcemaps.write('.')) 114 | .pipe(gulp.dest(dirs.buildClient)); 115 | }); 116 | 117 | gulp.task('build:common', () => { 118 | return gulp.src(path.resolve(dirs.srcCommon, '**/*.js')) 119 | .pipe(sourcemaps.init()) 120 | .pipe(babel()) 121 | .pipe(sourcemaps.write('.')) 122 | .pipe(gulp.dest(dirs.buildCommon)); 123 | }); 124 | 125 | gulp.task('build:server', ['copy:server', 'copy:config:server'], () => { 126 | return gulp.src(path.resolve(dirs.srcServer, '**/*.js')) 127 | .pipe(sourcemaps.init()) 128 | .pipe(babel()) 129 | .pipe(sourcemaps.write('.')) 130 | .pipe(gulp.dest(dirs.buildServer)); 131 | }); 132 | 133 | gulp.task('build:index', () => { 134 | return gulp.src(path.resolve(dirs.root, 'index.js')) 135 | .pipe(sourcemaps.init()) 136 | .pipe(babel()) 137 | .pipe(sourcemaps.write('.')) 138 | .pipe(gulp.dest(dirs.build)); 139 | }); 140 | 141 | gulp.task('build', [ 142 | 'build:client', 143 | 'build:common', 144 | 'build:server', 145 | 'build:index', 146 | 'copy:package', 147 | ]); 148 | -------------------------------------------------------------------------------- /template/client/view/Profile.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 132 | 133 | 160 | -------------------------------------------------------------------------------- /template/client/store/modules/auth/actions.js: -------------------------------------------------------------------------------- 1 | import loopback from '@/services/loopback'; 2 | import router from '@/router'; 3 | 4 | /** 5 | * Sync loopback token with current state 6 | */ 7 | export function syncToken({commit, dispatch}) { 8 | if (loopback.token) { 9 | commit('setAccessToken', loopback.token); 10 | return dispatch( 11 | 'loadAccount', 12 | loopback.token.userId, 13 | ).catch((err) => { 14 | commit('setAccessToken', null); 15 | return err; 16 | }); 17 | } 18 | return Promise.resolve(); 19 | } 20 | 21 | function evaluateRoute(state, to, from, next) { 22 | return (sessionError) => { 23 | if (to.matched.some(record => record.meta.requiresAuth)) { 24 | // this route requires auth, check if logged in 25 | // if not, redirect to login page (except when it's profile route and 26 | // there is an access_token). 27 | if (to.name === 'profile' && to.query.access_token) { 28 | next(); 29 | } else if (!state.access_token) { 30 | next({ 31 | name: 'login', 32 | params: { 33 | sessionError, 34 | }, 35 | }); 36 | } else { 37 | next(); 38 | } 39 | } else { 40 | next(); // make sure to always call next()! 41 | } 42 | }; 43 | } 44 | 45 | /** 46 | * Sync router for auth 47 | */ 48 | export function syncRouter({state, dispatch}, myRouter) { 49 | myRouter.beforeEach((to, from, next) => { 50 | dispatch('syncToken').then( 51 | evaluateRoute(state, to, from, next) 52 | ); 53 | }); 54 | } 55 | 56 | /** 57 | * Sign-in account with email and password (stores in state.account) 58 | * @param {Function} commit commit mutations function 59 | * @param {String} email user email 60 | * @param {String} password user password 61 | * @return {Promise} promise of logged user 62 | */ 63 | export function signIn({commit, dispatch, state}, {email, password}) { 64 | return loopback 65 | .post('/Accounts/login', { 66 | email, 67 | password, 68 | }) 69 | .then((token) => { 70 | commit('setAccessToken', token); 71 | 72 | // Update Loopback Token 73 | if (state.access_token !== null) { 74 | loopback.setToken(state.access_token); 75 | } else { 76 | loopback.removeToken(); 77 | } 78 | 79 | return dispatch('loadAccount', state.access_token.userId); 80 | }); 81 | } 82 | 83 | /** 84 | * Sign-out current logged-in account 85 | * (assumes that current state.account is not null) 86 | * @param {Function} commit commit mutations function 87 | * @return {Promise} promise of the logout 88 | */ 89 | export function signOut({commit, state}) { 90 | return loopback 91 | .post('/Accounts/logout', { 92 | // eslint-disable-next-line dot-notation 93 | accessToken: state['access_token'], 94 | }) 95 | .then(() => { 96 | commit('setAccessToken', null); 97 | loopback.removeToken(); 98 | router.push({name: 'login'}); 99 | }); 100 | } 101 | 102 | /** 103 | * Load an account by userId in state.account 104 | * @param {Function} commit commit mutations function 105 | * @param {Number} userId account id 106 | * @return {Promise} promise of the account 107 | */ 108 | export function loadAccount({commit}, userId) { 109 | return loopback 110 | .get(`/Accounts/${userId}`) 111 | .then(acc => commit('setAccount', acc)) 112 | .catch((err) => { 113 | loopback.removeToken(); 114 | return Promise.reject(err); 115 | }); 116 | } 117 | 118 | /** 119 | * Reset the password of the current logged-in account 120 | * (assumes that current state.account is not null) 121 | * (assumes that current state.access_token is not null) 122 | * @param {Function} commit commit mutations function 123 | * @param {Object} state Vuex state 124 | * @param {String} oldPassword old password 125 | * @param {String} newPassword new password 126 | * @return {Promise} promise of the password reset 127 | */ 128 | export function resetPassword(ctx, {oldPassword, newPassword}) { 129 | return loopback 130 | .post( 131 | '/Accounts/change-password', 132 | {oldPassword, newPassword} 133 | ); 134 | } 135 | 136 | /** 137 | * Send a email to the account user to reset the password 138 | * @param {Function} commit commit mutations function 139 | * @param {String} email user email 140 | * @return {Promise} promise of the sent email 141 | */ 142 | export function rememberPassword(ctx, email) { 143 | return loopback 144 | .post('/Accounts/reset', {email}); 145 | } 146 | -------------------------------------------------------------------------------- /template/client/view/Login.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 180 | 181 | 217 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [5.2.1](https://github.com/InCuca/vue-loopback/compare/v5.2.0...v5.2.1) (2018-07-12) 3 | 4 | 5 | ### Features 6 | 7 | * **template:** fix deploy and add settings ([37fef59](https://github.com/InCuca/vue-loopback/commit/37fef59)) 8 | 9 | 10 | 11 | 12 | # [5.2.0](https://github.com/InCuca/vue-loopback/compare/v5.1.0...v5.2.0) (2018-07-12) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **lint:** add jest support ([bc62f54](https://github.com/InCuca/vue-loopback/commit/bc62f54)) 18 | * **template:** filter only testable directories ([9664336](https://github.com/InCuca/vue-loopback/commit/9664336)) 19 | 20 | 21 | ### Features 22 | 23 | * **eslint:** add more test folders ([82f4654](https://github.com/InCuca/vue-loopback/commit/82f4654)) 24 | 25 | 26 | 27 | 28 | # [5.1.0](https://github.com/InCuca/vue-loopback/compare/v5.0.0...v5.1.0) (2018-06-28) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * **package:** fix test script ([1984f65](https://github.com/InCuca/vue-loopback/commit/1984f65)) 34 | 35 | 36 | 37 | 38 | # [5.0.0](https://github.com/InCuca/vue-loopback/compare/v4.2.0...v5.0.0) (2018-06-28) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **babel:** add stage2 support ([c95f47c](https://github.com/InCuca/vue-loopback/commit/c95f47c)) 44 | * **gulp-tasks:** add exit in mocha to close mocha when test pass ([0963212](https://github.com/InCuca/vue-loopback/commit/0963212)) 45 | * **gulp-tasks:** fix backslashs in win fixes [#8](https://github.com/InCuca/vue-loopback/issues/8) ([b721305](https://github.com/InCuca/vue-loopback/commit/b721305)) 46 | * **jest:** fix jest plugins path ([6632ad6](https://github.com/InCuca/vue-loopback/commit/6632ad6)) 47 | * **lint:** fix eslint errors ([dc2a6c7](https://github.com/InCuca/vue-loopback/commit/dc2a6c7)) 48 | * **loopback:** pass previous uid to loading fn ([39093da](https://github.com/InCuca/vue-loopback/commit/39093da)) 49 | * **loopback-boot:** add support for es6 modules in boot scripts ([2d813fe](https://github.com/InCuca/vue-loopback/commit/2d813fe)), closes [strongloop/loopback-boot#280](https://github.com/strongloop/loopback-boot/issues/280) 50 | * **package:** run server tests in band ([4d45501](https://github.com/InCuca/vue-loopback/commit/4d45501)) 51 | * **package:** update loopback-boot to 3.1 to fix boot ([200b615](https://github.com/InCuca/vue-loopback/commit/200b615)) 52 | * **server:** add option for compatibility with jest ([fbb68b2](https://github.com/InCuca/vue-loopback/commit/fbb68b2)) 53 | * **tasks:** remove tests tasks ([e889a3c](https://github.com/InCuca/vue-loopback/commit/e889a3c)) 54 | * **tests:** remove client and server separation ([cd4ac8c](https://github.com/InCuca/vue-loopback/commit/cd4ac8c)) 55 | 56 | 57 | ### Features 58 | 59 | * **babel:** add node 8 add regenerator ([94cd1a4](https://github.com/InCuca/vue-loopback/commit/94cd1a4)) 60 | * **jest:** add jest config file ([044a3eb](https://github.com/InCuca/vue-loopback/commit/044a3eb)) 61 | * **jest:** add jest plugins config ([3021667](https://github.com/InCuca/vue-loopback/commit/3021667)) 62 | * **jest:** add jest-preset-loopback ([575873c](https://github.com/InCuca/vue-loopback/commit/575873c)) 63 | * **Login:** add sessionError with authorization error redirection ([a05a9eb](https://github.com/InCuca/vue-loopback/commit/a05a9eb)) 64 | * **loopback:** add DateString type support ([5ba568d](https://github.com/InCuca/vue-loopback/commit/5ba568d)) 65 | * **package:** add babel-jest ([29533ac](https://github.com/InCuca/vue-loopback/commit/29533ac)) 66 | * **package:** add jest dependency ([1b82ba3](https://github.com/InCuca/vue-loopback/commit/1b82ba3)) 67 | * **package:** add loopback-jest ([2bc6cc8](https://github.com/InCuca/vue-loopback/commit/2bc6cc8)) 68 | * **package:** add vue-jest ([33c6d57](https://github.com/InCuca/vue-loopback/commit/33c6d57)) 69 | * **package:** run afterbuild after build, fixes [#9](https://github.com/InCuca/vue-loopback/issues/9) ([77c6fcf](https://github.com/InCuca/vue-loopback/commit/77c6fcf)) 70 | 71 | 72 | 73 | 74 | # [4.2.0](https://github.com/InCuca/vue-loopback/compare/v4.1.1...v4.2.0) (2018-05-17) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * **loopback:** fix lint errors ([ea23c48](https://github.com/InCuca/vue-loopback/commit/ea23c48)) 80 | 81 | 82 | ### Features 83 | 84 | * **gulp-tasks:** add support to es6 modules, fixes [#7](https://github.com/InCuca/vue-loopback/issues/7) ([0c70326](https://github.com/InCuca/vue-loopback/commit/0c70326)) 85 | * **loopback:** add aditional response check in interceptResErrors ([66d1b59](https://github.com/InCuca/vue-loopback/commit/66d1b59)) 86 | 87 | 88 | 89 | 90 | ## [4.1.1](https://github.com/InCuca/vue-loopback/compare/v4.1.0...v4.1.1) (2018-05-15) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * **App:** fix lint error ([5cc2c97](https://github.com/InCuca/vue-loopback/commit/5cc2c97)) 96 | * **project:** fix lint errors ([04d852a](https://github.com/InCuca/vue-loopback/commit/04d852a)) 97 | * **template:** fix lint errors ([ea1c31e](https://github.com/InCuca/vue-loopback/commit/ea1c31e)) 98 | * **template:** fix package lint command ([34b3a0b](https://github.com/InCuca/vue-loopback/commit/34b3a0b)) 99 | 100 | 101 | ### Features 102 | 103 | * **App:** add some style to not extended App ([9f8d380](https://github.com/InCuca/vue-loopback/commit/9f8d380)) 104 | 105 | 106 | 107 | 108 | # [4.1.0](https://github.com/InCuca/vue-loopback/compare/4.0.1...4.1.0) (2018-05-10) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * **App:** only dispatch on extended ([f45a002](https://github.com/InCuca/vue-loopback/commit/f45a002)) 114 | * **karma:** fix App errors and consoleAppendfy ([1ef9df4](https://github.com/InCuca/vue-loopback/commit/1ef9df4)) 115 | * **karma:** throws when vue warns ([7907f28](https://github.com/InCuca/vue-loopback/commit/7907f28)) 116 | * **release-it:** fix buildCommand ([655954a](https://github.com/InCuca/vue-loopback/commit/655954a)) 117 | * **template:** fix lint errors ([3ed71f8](https://github.com/InCuca/vue-loopback/commit/3ed71f8)) 118 | 119 | 120 | ### Features 121 | 122 | * **Account:** add declaration tests and loopback-chai support ([903d1ef](https://github.com/InCuca/vue-loopback/commit/903d1ef)) 123 | * **gulp:** add optional -t parameter to test files ([08e5ece](https://github.com/InCuca/vue-loopback/commit/08e5ece)) 124 | * **project:** add release-it support ([eaf6f1c](https://github.com/InCuca/vue-loopback/commit/eaf6f1c)) 125 | 126 | 127 | 128 | 129 | ## [4.0.1](https://github.com/InCuca/vue-loopback/compare/4.0.0...4.0.1) (2018-04-27) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * **gulp:** fix lint errors ([4545089](https://github.com/InCuca/vue-loopback/commit/4545089)) 135 | 136 | 137 | 138 | 139 | # [4.0.0](https://github.com/InCuca/vue-loopback/compare/3.2.0...4.0.0) (2018-04-27) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * **auth:** fix eslint errors ([154897e](https://github.com/InCuca/vue-loopback/commit/154897e)) 145 | * **gulp:** clear modules cache to enable hot reloading ([c1c8f80](https://github.com/InCuca/vue-loopback/commit/c1c8f80)) 146 | * **gulp:** fix hot reload ([661639d](https://github.com/InCuca/vue-loopback/commit/661639d)) 147 | * **gulp:** fix server hot reloading not restarting ([57609bb](https://github.com/InCuca/vue-loopback/commit/57609bb)) 148 | * **loopback:** add accessToken to logout method ([4bac1b6](https://github.com/InCuca/vue-loopback/commit/4bac1b6)) 149 | * **loopback:** change error to warning in console ([3413b45](https://github.com/InCuca/vue-loopback/commit/3413b45)) 150 | * **loopback:** fix lint errors ([3086f12](https://github.com/InCuca/vue-loopback/commit/3086f12)) 151 | 152 | 153 | ### Features 154 | 155 | * **async:** add async module ([7b17fc5](https://github.com/InCuca/vue-loopback/commit/7b17fc5)) 156 | 157 | 158 | 159 | 160 | # [3.2.0](https://github.com/InCuca/vue-loopback/compare/3.1.1...3.2.0) (2018-03-25) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * **boot:** fix boot scripts ([33e3461](https://github.com/InCuca/vue-loopback/commit/33e3461)) 166 | * **lint:** fix lint errors ([cb3dd31](https://github.com/InCuca/vue-loopback/commit/cb3dd31)) 167 | * **loopback:** setLoading to false when errored ([49f48c5](https://github.com/InCuca/vue-loopback/commit/49f48c5)) 168 | * **server:** fix index server ([0df8443](https://github.com/InCuca/vue-loopback/commit/0df8443)) 169 | 170 | 171 | ### Features 172 | 173 | * **build:** add copy:config:server ([661ee6f](https://github.com/InCuca/vue-loopback/commit/661ee6f)) 174 | 175 | 176 | 177 | 178 | ## [3.1.1](https://github.com/InCuca/vue-loopback/compare/3.1.0...3.1.1) (2018-03-07) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * **auth:** only change route after login ([19eb3e9](https://github.com/InCuca/vue-loopback/commit/19eb3e9)) 184 | * **auth:** only change router after account exists ([624fff2](https://github.com/InCuca/vue-loopback/commit/624fff2)) 185 | * **lint:** fix lint errors ([e1df64d](https://github.com/InCuca/vue-loopback/commit/e1df64d)) 186 | 187 | 188 | 189 | 190 | # [3.1.0](https://github.com/InCuca/vue-loopback/compare/3.0.0...3.1.0) (2018-02-28) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * **auth:** redirect to login if token is expired ([fa3abbc](https://github.com/InCuca/vue-loopback/commit/fa3abbc)) 196 | 197 | 198 | ### Features 199 | 200 | * **lint:** disable rule ([06108f4](https://github.com/InCuca/vue-loopback/commit/06108f4)) 201 | * **loopback:** add setLoading fn ([92da75f](https://github.com/InCuca/vue-loopback/commit/92da75f)) 202 | 203 | 204 | 205 | 206 | # [3.0.0](https://github.com/InCuca/vue-loopback/compare/2.1.0...3.0.0) (2018-02-28) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * **eslint:** disable resolver ([112395b](https://github.com/InCuca/vue-loopback/commit/112395b)) 212 | * **eslint:** remove modules from ignore ([c9d0f1a](https://github.com/InCuca/vue-loopback/commit/c9d0f1a)) 213 | * **eslintrc:** remove not existent plugin ([7534446](https://github.com/InCuca/vue-loopback/commit/7534446)) 214 | * **gulp-tasks:** write sourcemaps to files instead of embed ([f4e2af3](https://github.com/InCuca/vue-loopback/commit/f4e2af3)) 215 | * **lint:** fix auto-fixable errors ([ff283cc](https://github.com/InCuca/vue-loopback/commit/ff283cc)) 216 | * **lint:** fix lint errors ([b7d673a](https://github.com/InCuca/vue-loopback/commit/b7d673a)) 217 | * **lint:** fix lint errors ([b550d32](https://github.com/InCuca/vue-loopback/commit/b550d32)) 218 | * **lint:** fix lint errors ([02dc21d](https://github.com/InCuca/vue-loopback/commit/02dc21d)) 219 | * **lint:** fix lint errors ([982f0eb](https://github.com/InCuca/vue-loopback/commit/982f0eb)) 220 | * **lint:** fix lint errors ([aa7df98](https://github.com/InCuca/vue-loopback/commit/aa7df98)) 221 | * **lint:** fix lint errors ([348f1ad](https://github.com/InCuca/vue-loopback/commit/348f1ad)) 222 | * **lint:** fix lint errors ([e8eb0b9](https://github.com/InCuca/vue-loopback/commit/e8eb0b9)) 223 | * **lint:** fix lint errors and remove no-unresolved rule ([97a2062](https://github.com/InCuca/vue-loopback/commit/97a2062)) 224 | * **lint:** fix linting errors ([d353c74](https://github.com/InCuca/vue-loopback/commit/d353c74)) 225 | * **lint:** remove import settings ([ffd1574](https://github.com/InCuca/vue-loopback/commit/ffd1574)) 226 | * **travis:** apply workaround travis-ci/travis-ci[#8836](https://github.com/InCuca/vue-loopback/issues/8836) ([9f5285b](https://github.com/InCuca/vue-loopback/commit/9f5285b)) 227 | 228 | 229 | ### Features 230 | 231 | * **eslint:** add eslint to this repo itself ([3b20ee4](https://github.com/InCuca/vue-loopback/commit/3b20ee4)) 232 | * **eslint:** ignore template directory ([8bcc688](https://github.com/InCuca/vue-loopback/commit/8bcc688)) 233 | * **gulp:** add build:index ([31840ed](https://github.com/InCuca/vue-loopback/commit/31840ed)) 234 | * **gulp:** build also index.js ([d23d144](https://github.com/InCuca/vue-loopback/commit/d23d144)) 235 | * **lint:** add resolve script ([ed7ed68](https://github.com/InCuca/vue-loopback/commit/ed7ed68)) 236 | * **lint:** update settings to airbnb - webpack template based ([df30173](https://github.com/InCuca/vue-loopback/commit/df30173)) 237 | * **package:** update eslint ([3e350ae](https://github.com/InCuca/vue-loopback/commit/3e350ae)) 238 | * **server:** add client server ([44bfb23](https://github.com/InCuca/vue-loopback/commit/44bfb23)) 239 | * **travis:** remove node 6 and 7 ([90cc789](https://github.com/InCuca/vue-loopback/commit/90cc789)) 240 | 241 | 242 | 243 | 244 | # [2.1.0](https://github.com/InCuca/vue-loopback/compare/2.0.1...2.1.0) (2018-02-20) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * **gulp:** fix gulp not reloading client ([f1aed3a](https://github.com/InCuca/vue-loopback/commit/f1aed3a)) 250 | * **package:** fix font-awesome dependency ([8b21fc5](https://github.com/InCuca/vue-loopback/commit/8b21fc5)) 251 | * **template:** add missing package copy ([5f1c62b](https://github.com/InCuca/vue-loopback/commit/5f1c62b)) 252 | * **template:** fix lint errors ([f589041](https://github.com/InCuca/vue-loopback/commit/f589041)) 253 | 254 | 255 | ### Features 256 | 257 | * **gulp:** add package to built file and add start script ([527e061](https://github.com/InCuca/vue-loopback/commit/527e061)) 258 | * **loopback:** add token in local storage ([34d1613](https://github.com/InCuca/vue-loopback/commit/34d1613)) 259 | 260 | 261 | 262 | 263 | ## [2.0.1](https://github.com/InCuca/vue-loopback/compare/2.0.0...2.0.1) (2017-12-15) 264 | 265 | 266 | ### Features 267 | 268 | * **babel:** change babel configuration ([81903ec](https://github.com/InCuca/vue-loopback/commit/81903ec)) 269 | 270 | 271 | 272 | 273 | # [2.0.0](https://github.com/InCuca/vue-loopback/compare/1.0.2...2.0.0) (2017-12-13) 274 | 275 | 276 | ### Bug Fixes 277 | 278 | * **lint:** fix lint errors ([47475f1](https://github.com/InCuca/vue-loopback/commit/47475f1)) 279 | * **meta:** fix wrong path name ([131f2ec](https://github.com/InCuca/vue-loopback/commit/131f2ec)) 280 | * **readme:** add usage instructions ([8f5c4f5](https://github.com/InCuca/vue-loopback/commit/8f5c4f5)) 281 | * **tempalte:** fix hello world component ([b6711ab](https://github.com/InCuca/vue-loopback/commit/b6711ab)) 282 | * **template:** add missing dependencies and missing compilers file ([de88643](https://github.com/InCuca/vue-loopback/commit/de88643)) 283 | * **template:** escape curly braces and translate strings to en ([e9cbf52](https://github.com/InCuca/vue-loopback/commit/e9cbf52)) 284 | * **template:** fix css and translate strings to english ([0f31e93](https://github.com/InCuca/vue-loopback/commit/0f31e93)) 285 | * **template:** fix misspeling ([8fa8ae3](https://github.com/InCuca/vue-loopback/commit/8fa8ae3)) 286 | * **template:** fix template errors ([5d1e022](https://github.com/InCuca/vue-loopback/commit/5d1e022)) 287 | * **template:** Fix wrong comonente filename ([b02518f](https://github.com/InCuca/vue-loopback/commit/b02518f)) 288 | * **template:** fix wrong route and add create-admin test ([0942b36](https://github.com/InCuca/vue-loopback/commit/0942b36)) 289 | * **template:** remove gitlab file ([92c2550](https://github.com/InCuca/vue-loopback/commit/92c2550)) 290 | * **test:** add extended tests ([1897096](https://github.com/InCuca/vue-loopback/commit/1897096)) 291 | * **test:** fix boot test ([f6b4a4a](https://github.com/InCuca/vue-loopback/commit/f6b4a4a)) 292 | * **test:** fix lint error ([d865b79](https://github.com/InCuca/vue-loopback/commit/d865b79)) 293 | * **test:** fix test commands ([1eb749c](https://github.com/InCuca/vue-loopback/commit/1eb749c)) 294 | * **tests:** fix app test ([070b19e](https://github.com/InCuca/vue-loopback/commit/070b19e)) 295 | * **tests:** add karma modulesify settings ([0ba8e18](https://github.com/InCuca/vue-loopback/commit/0ba8e18)) 296 | * **tests:** add vuefy compiler to karma ([3e9e45e](https://github.com/InCuca/vue-loopback/commit/3e9e45e)) 297 | * **tests:** fix test errors ([d8ae843](https://github.com/InCuca/vue-loopback/commit/d8ae843)) 298 | * **tests:** fix test errors ([2f0a2e5](https://github.com/InCuca/vue-loopback/commit/2f0a2e5)) 299 | * **tests:** fix tests for no extended build ([e9bb6de](https://github.com/InCuca/vue-loopback/commit/e9bb6de)) 300 | 301 | 302 | ### Features 303 | 304 | * **template:** add extended option ([f08bb6f](https://github.com/InCuca/vue-loopback/commit/f08bb6f)) 305 | * **tests:** rename and add more tests ([a71e8a6](https://github.com/InCuca/vue-loopback/commit/a71e8a6)) 306 | 307 | 308 | 309 | 310 | ## [1.0.2](https://github.com/InCuca/vue-loopback/compare/1.0.1...1.0.2) (2017-10-29) 311 | 312 | 313 | ### Bug Fixes 314 | 315 | * **App:** fix databinding not working ([de7f8e6](https://github.com/InCuca/vue-loopback/commit/de7f8e6)) 316 | 317 | 318 | 319 | 320 | ## [1.0.1](https://github.com/InCuca/vue-loopback/compare/722097d...1.0.1) (2017-10-29) 321 | 322 | 323 | ### Bug Fixes 324 | 325 | * **build:** add xvfb to travis config ([4442c6c](https://github.com/InCuca/vue-loopback/commit/4442c6c)) 326 | * **build:** fix syntax errors ([2d4bb0f](https://github.com/InCuca/vue-loopback/commit/2d4bb0f)) 327 | * **build:** remove start scripts ([8f9661e](https://github.com/InCuca/vue-loopback/commit/8f9661e)) 328 | * **package:** add vue-cli as dependency ([722097d](https://github.com/InCuca/vue-loopback/commit/722097d)) 329 | * **project:** add npm-debug to gitignore ([3043ea6](https://github.com/InCuca/vue-loopback/commit/3043ea6)) 330 | * **test:** fix task finishing before test ([80d23fe](https://github.com/InCuca/vue-loopback/commit/80d23fe)) 331 | * **test:** remove not used polyfill ([fa0eefd](https://github.com/InCuca/vue-loopback/commit/fa0eefd)) 332 | * **test:** show tests output ([49573ef](https://github.com/InCuca/vue-loopback/commit/49573ef)) 333 | 334 | 335 | ### Features 336 | 337 | * **App:** change prop to data ([825cc69](https://github.com/InCuca/vue-loopback/commit/825cc69)) 338 | * **lint:** split eslint files ([7b1c31f](https://github.com/InCuca/vue-loopback/commit/7b1c31f)) 339 | * **package:** add test scripts ([47c8f21](https://github.com/InCuca/vue-loopback/commit/47c8f21)) 340 | * **template:** add hello prop ([5659f7e](https://github.com/InCuca/vue-loopback/commit/5659f7e)) 341 | * **test:** add promise polyfill in client tests ([55afe8a](https://github.com/InCuca/vue-loopback/commit/55afe8a)) 342 | * **test:** add support for client tests with karma ([a488810](https://github.com/InCuca/vue-loopback/commit/a488810)) 343 | * **test:** change test platform to chrome ([b6af4f5](https://github.com/InCuca/vue-loopback/commit/b6af4f5)) 344 | * **test:** split tests ([c079e2c](https://github.com/InCuca/vue-loopback/commit/c079e2c)) 345 | * **travis:** remove travis from template and add chrome ([ea911fc](https://github.com/InCuca/vue-loopback/commit/ea911fc)) 346 | 347 | 348 | 349 | --------------------------------------------------------------------------------