├── backend ├── api │ ├── models │ │ ├── .gitkeep │ │ ├── Basket.js │ │ ├── Product.js │ │ └── User.js │ ├── controllers │ │ ├── .gitkeep │ │ ├── AppController.js │ │ ├── RegisterController.js │ │ ├── user │ │ │ ├── shop │ │ │ │ └── BasketController.js │ │ │ ├── UserController.js │ │ │ └── products │ │ │ │ ├── ProductsController.js │ │ │ │ └── product │ │ │ │ └── ProductController.js │ │ ├── ProductsController.js │ │ └── LoginController.js │ ├── services │ │ ├── .gitkeep │ │ ├── TokenService.js │ │ └── CryptographyService.js │ ├── policies │ │ └── isAuthorized.js │ └── responses │ │ ├── ok.js │ │ ├── created.js │ │ ├── badRequest.js │ │ ├── forbidden.js │ │ ├── serverError.js │ │ └── notFound.js ├── test │ ├── mocha.opts │ ├── bootstrap.test.js │ └── functional │ │ └── specs │ │ ├── Help.spec.js │ │ ├── Register.spec.js │ │ ├── Login.spec.js │ │ ├── Product.spec.js │ │ └── Products.spec.js ├── .sailsrc ├── assets │ ├── css │ │ ├── chunk-01c9.4ddb07be.css │ │ ├── chunk-5e44.062decc4.css │ │ └── chunk-58f5.b1ea0394.css │ ├── img │ │ └── baseline-info-24px.svg │ ├── sw.js │ ├── js │ │ ├── 0011.d26555d2.js │ │ ├── 83d0.4eb65a7f.js │ │ ├── chunk-596f.e000c2e0.js │ │ ├── chunk-58f5.62a37769.js │ │ └── chunk-5e44.8377145e.js │ ├── index.html │ └── precache-manifest.28eda1f90a81d9bfde2bfaf166d3f21f.js ├── config │ ├── locales │ │ ├── en.json │ │ ├── de.json │ │ ├── es.json │ │ ├── fr.json │ │ └── _README.md │ ├── env │ │ ├── test.js │ │ ├── development.js │ │ └── production.js │ ├── log.js │ ├── models.js │ ├── globals.js │ ├── i18n.js │ ├── policies.js │ ├── datastores.js │ ├── csrf.js │ ├── bootstrap.js │ └── routes.js ├── .editorconfig ├── tasks │ ├── config │ │ ├── clean.js │ │ ├── uglify.js │ │ ├── coffee.js │ │ ├── watch.js │ │ ├── less.js │ │ ├── cssmin.js │ │ ├── sync.js │ │ ├── concat.js │ │ ├── copy.js │ │ └── jst.js │ ├── register │ │ ├── syncAssets.js │ │ ├── compileAssets.js │ │ ├── linkAssetsBuild.js │ │ ├── linkAssetsBuildProd.js │ │ ├── linkAssets.js │ │ ├── prod.js │ │ ├── buildProd.js │ │ ├── build.js │ │ └── default.js │ ├── pipeline.js │ └── README.md ├── package.json ├── app.js ├── Gruntfile.js └── .gitignore ├── frontend ├── static │ └── .gitkeep ├── .eslintignore ├── src │ ├── state │ │ ├── products │ │ │ ├── products.state.js │ │ │ ├── products.mutations.js │ │ │ ├── products.module.js │ │ │ └── products.actions.js │ │ ├── basket │ │ │ ├── basket.state.js │ │ │ ├── basket.module.js │ │ │ ├── basket.actions.js │ │ │ └── basket.mutations.js │ │ ├── user │ │ │ ├── user.state.js │ │ │ ├── user.module.js │ │ │ ├── user.mutations.js │ │ │ └── user.actions.js │ │ ├── product │ │ │ ├── product.state.js │ │ │ ├── product.module.js │ │ │ ├── product.mutations.js │ │ │ └── product.actions.js │ │ └── index.js │ ├── sw.js │ ├── components │ │ ├── Login.mobile.vue │ │ ├── user │ │ │ ├── shop │ │ │ │ ├── BasketIndex.mixin.js │ │ │ │ ├── BasketIndex.mobile.vue │ │ │ │ ├── ShopIndex.mixin.js │ │ │ │ ├── ShopIndex.desktop.vue │ │ │ │ ├── BasketIndex.desktop.vue │ │ │ │ └── ShopIndex.mobile.vue │ │ │ └── products │ │ │ │ ├── ProductsIndex.desktop.vue │ │ │ │ ├── ProductsIndex.mobile.vue │ │ │ │ ├── product │ │ │ │ ├── ProductPost.mixin.js │ │ │ │ ├── ProductPatch.mixin.js │ │ │ │ ├── ProductPost.mobile.vue │ │ │ │ ├── ProductValidation.mixin.js │ │ │ │ ├── ProductPost.desktop.vue │ │ │ │ ├── ProductPatch.mobile.vue │ │ │ │ └── ProductPatch.desktop.vue │ │ │ │ ├── ProductsGet.mixin.js │ │ │ │ ├── ProductsGet.mobile.vue │ │ │ │ └── ProductsGet.desktop.vue │ │ ├── Register.mixin.js │ │ ├── Home.mixin.js │ │ ├── Register.mobile.vue │ │ ├── Register.desktop.vue │ │ ├── Login.desktop.vue │ │ ├── Login.mixin.js │ │ ├── User.validation.js │ │ ├── Home.mobile.vue │ │ ├── Home.desktop.vue │ │ └── help │ │ │ └── Help.desktop.vue │ ├── bootstrap.mixin.js │ ├── main.js │ ├── App.mixin.js │ ├── bootstrap.mobile.js │ ├── bootstrap.desktop.js │ ├── router │ │ ├── router.mobile.js │ │ └── router.desktop.js │ ├── App.mobile.vue │ └── App.desktop.vue ├── .postcssrc.js ├── babel.config.js ├── cypress.json ├── .gitignore ├── .editorconfig ├── index.html ├── .eslintrc.js ├── public │ ├── img │ │ └── baseline-info-24px.svg │ └── index.html ├── cypress │ ├── plugins │ │ └── index.js │ ├── support │ │ ├── index.js │ │ └── commands.js │ ├── integration │ │ ├── Login.spec.js │ │ ├── Register.spec.js │ │ ├── ProductsGet.js │ │ └── ShopIndex.spec.js │ └── fixtures │ │ └── mocks.js ├── vue.config.js └── package.json ├── .gitignore ├── commitlint.config.js ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /backend/api/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/api/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/api/services/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 10s 2 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | test/**/*.js 4 | -------------------------------------------------------------------------------- /backend/.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "modules": {} 4 | } 5 | } -------------------------------------------------------------------------------- /frontend/src/state/products/products.state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | products: {} 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /backend/assets/css/chunk-01c9.4ddb07be.css: -------------------------------------------------------------------------------- 1 | .mint-popup-1{border-radius:8px;height:85%;padding:20px;width:85%} -------------------------------------------------------------------------------- /backend/config/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Welcome", 3 | "A brand new app.": "A brand new app." 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/state/basket/basket.state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | basket: { 3 | products: [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /backend/assets/css/chunk-5e44.062decc4.css: -------------------------------------------------------------------------------- 1 | .card[data-v-13b6df7c]{max-height:350px;min-height:200px;overflow-y:scroll} -------------------------------------------------------------------------------- /backend/config/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Willkommen", 3 | "A brand new app.": "Eine neue App." 4 | } 5 | -------------------------------------------------------------------------------- /backend/config/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenido", 3 | "A brand new app.": "Una nueva aplicación." 4 | } 5 | -------------------------------------------------------------------------------- /backend/assets/css/chunk-58f5.b1ea0394.css: -------------------------------------------------------------------------------- 1 | .mint-swipe{height:100px;text-align:center}.mint-swipe-item{color:#000;line-height:100px} -------------------------------------------------------------------------------- /backend/config/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenue", 3 | "A brand new app.": "Une toute nouvelle application." 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/state/user/user.state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | user: { 3 | id: '', 4 | name: '', 5 | password: '' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | Dockerfile 4 | frontend/test/unit/coverage 5 | node_modules 6 | package-lock.json -------------------------------------------------------------------------------- /frontend/src/state/products/products.mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_PRODUCTS (state, products) { 3 | state.products = products 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/state/product/product.state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | product: { 3 | title: '', 4 | description: '', 5 | price: null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | test/unit/coverage 7 | test/e2e/reports 8 | selenium-debug.log 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /backend/api/models/Basket.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | attributes: { 3 | products: { 4 | type: 'json', 5 | required: true 6 | }, 7 | 8 | user: { 9 | model: 'user' 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /backend/config/env/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | log: { 3 | level: 'error' 4 | }, 5 | connections: { 6 | localDiskDb: { 7 | adapter: 'sails-disk', 8 | fileName: 'testDB.db' 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/api/controllers/AppController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | serve: (req, res) => { 3 | const fs = require('fs') 4 | 5 | const app = __dirname + '/../../assets/index.html' 6 | fs.createReadStream(app).pipe(res) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/state/user/user.module.js: -------------------------------------------------------------------------------- 1 | import state from './user.state' 2 | import mutations from './user.mutations' 3 | import actions from './user.actions' 4 | 5 | export default { 6 | state, 7 | mutations, 8 | actions 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/state/basket/basket.module.js: -------------------------------------------------------------------------------- 1 | import state from './basket.state' 2 | import actions from './basket.actions' 3 | import mutations from './basket.mutations' 4 | 5 | export default { 6 | state, 7 | actions, 8 | mutations 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/state/product/product.module.js: -------------------------------------------------------------------------------- 1 | import state from './product.state' 2 | import mutations from './product.mutations' 3 | import actions from './product.actions' 4 | 5 | export default { 6 | state, 7 | mutations, 8 | actions 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/state/products/products.module.js: -------------------------------------------------------------------------------- 1 | import state from './products.state' 2 | import actions from './products.actions' 3 | import mutations from './products.mutations' 4 | 5 | export default { 6 | state, 7 | actions, 8 | mutations 9 | } 10 | -------------------------------------------------------------------------------- /backend/api/services/TokenService.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const tokenSecret = 'secretissecret' 3 | 4 | module.exports = { 5 | issue: payload => jwt.sign(payload, tokenSecret, {expiresIn: '7d'}), 6 | verify: token => jwt.verify(token, tokenSecret) 7 | } 8 | -------------------------------------------------------------------------------- /backend/api/controllers/RegisterController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | post: async (req, res) => { 3 | const {name, password} = req.allParams() 4 | 5 | await User 6 | .create({name, password}) 7 | .catch(error => res.serverError(error)) 8 | 9 | return res.ok() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/api/models/Product.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | attributes: { 3 | title: { 4 | type: 'string', 5 | required: true 6 | }, 7 | description: { 8 | type: 'string' 9 | }, 10 | price: { 11 | type: 'float', 12 | required: true 13 | }, 14 | 15 | user: { 16 | model: 'user' 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Product paradise 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /backend/api/controllers/user/shop/BasketController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | postBasket: async (req, res) => { 3 | const products = req.param('products') 4 | 5 | const basket = await Basket 6 | .create({products}) 7 | .catch(error => res.serverError(error)) 8 | 9 | sails.log.info('Created basket', basket) 10 | 11 | return res.ok() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/api/controllers/user/UserController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get: async (req, res) => { 3 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 4 | 5 | const user = await User 6 | .findOne({id: userIdentifier}) 7 | .catch(error => res.serverError(error)) 8 | 9 | delete user.password 10 | 11 | return res.json(user) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/test/bootstrap.test.js: -------------------------------------------------------------------------------- 1 | const sails = require('sails') 2 | const fs = require('fs') 3 | 4 | before(function (done) { 5 | this.timeout(10000) 6 | 7 | sails.lift({}, error => { 8 | if (error) return done(error) 9 | 10 | done(error, sails) 11 | }) 12 | }) 13 | 14 | after(function (done) { 15 | fs.unlinkSync(`${__dirname}/../.tmp/testDB.db`) 16 | sails.lower(done) 17 | }) 18 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': 0, 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-empty-pattern': 0 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/state/user/user.mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_USER (state, user) { 3 | state.user = user 4 | }, 5 | 6 | SET_USER_NAME (state, name) { 7 | state.user.name = name 8 | }, 9 | 10 | SET_USER_PASSWORD (state, password) { 11 | state.user.password = password 12 | }, 13 | 14 | RESET_USER (state) { 15 | state.user.name = '' 16 | state.user.password = '' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/state/basket/basket.actions.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | checkout (context, basket) { 5 | return new Promise((resolve, reject) => { 6 | Vue.http 7 | .post('/api/user/shop/basket/post', {products: basket.products}) 8 | .then(() => { 9 | context.commit('RESET_BASKET') 10 | resolve() 11 | }) 12 | .catch(error => reject(error)) 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/sw.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []) 3 | 4 | workbox.routing.registerRoute( 5 | new RegExp('/'), 6 | workbox.strategies.networkFirst() 7 | ) 8 | 9 | workbox.routing.registerRoute( 10 | new RegExp('.*\.js'), 11 | workbox.strategies.staleWhileRevalidate() 12 | ) 13 | 14 | workbox.routing.registerRoute( 15 | new RegExp('(.*)/api/(.*)$'), 16 | workbox.strategies.networkFirst() 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /backend/assets/img/baseline-info-24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 -------------------------------------------------------------------------------- /frontend/public/img/baseline-info-24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 -------------------------------------------------------------------------------- /backend/test/functional/specs/Help.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const chai = require('chai') 3 | const faker = require('faker') 4 | 5 | describe('Help', function() { 6 | it('should return a non empty string', function(done) { 7 | 8 | request(sails.hooks.http.app) 9 | .post('/api/help') 10 | .send({}) 11 | .expect(200) 12 | .then(response => { 13 | chai.assert.isNotEmpty(response.body.answer) 14 | done() 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'chore', 10 | 'ci', 11 | 'deps', 12 | 'docs', 13 | 'feat', 14 | 'fix', 15 | 'perf', 16 | 'refactor', 17 | 'revert', 18 | 'security', 19 | 'style', 20 | 'test', 21 | 'wip' 22 | ] 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/state/basket/basket.mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PUSH_TO_BASKET (state, product) { 3 | state.basket.products.push(product) 4 | }, 5 | 6 | REMOVE_PRODUCT_FROM_BASKET (state, index) { 7 | state.basket.products.splice(index, 1) 8 | }, 9 | 10 | RESET_BASKET (state) { 11 | state.basket = { 12 | id: Math.random().toString(36).substr(2, 10), 13 | products: [] 14 | } 15 | }, 16 | 17 | SET_BASKET (state, basket) { 18 | state.basket = basket 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/state/product/product.mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_PRODUCT_TITLE (state, title) { 3 | state.product.title = title 4 | }, 5 | 6 | SET_PRODUCT_DESCRIPTION (state, description) { 7 | state.product.description = description 8 | }, 9 | 10 | SET_PRODUCT_PRICE (state, price) { 11 | state.product.price = price 12 | }, 13 | 14 | RESET_PRODUCT (state) { 15 | state.product.title = '' 16 | state.product.description = '' 17 | state.product.price = null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/api/controllers/ProductsController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getProducts: async (req, res) => { 3 | const page = req.param('page') 4 | 5 | const amountOfProducts = await Product 6 | .count() 7 | .catch(error => res.serverError(error)) 8 | 9 | const products = await Product 10 | .find() 11 | .populate('user', { select: ['name'] }) 12 | .paginate({page, limit: 6}) 13 | .catch(error => res.serverError(error)) 14 | 15 | return res.json({ 16 | products, 17 | amountOfProducts 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/tasks/config/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `clean` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Remove the files and folders in your Sails app's web root 7 | * (conventionally a hidden directory called `.tmp/public`). 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-clean 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('clean', { 16 | dev: ['.tmp/public/**'], 17 | build: ['www'] 18 | }); 19 | 20 | grunt.loadNpmTasks('grunt-contrib-clean'); 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/Login.mobile.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /backend/tasks/config/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `uglify` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minify client-side JavaScript files using UglifyJS. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-uglify 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('uglify', { 15 | dist: { 16 | src: ['.tmp/public/concat/production.js'], 17 | dest: '.tmp/public/min/production.min.js' 18 | } 19 | }); 20 | 21 | grunt.loadNpmTasks('grunt-contrib-uglify'); 22 | }; 23 | -------------------------------------------------------------------------------- /backend/tasks/register/syncAssets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `syncAssets` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist is not designed to be used directly-- rather 7 | * it is a helper called by the `watch` task (`tasks/config/watch.js`). 8 | * 9 | * For more information see: 10 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/sync-assets-js 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | grunt.registerTask('syncAssets', [ 15 | 'jst:dev', 16 | 'less:dev', 17 | 'sync:dev', 18 | 'coffee:dev' 19 | ]); 20 | }; 21 | -------------------------------------------------------------------------------- /backend/api/policies/isAuthorized.js: -------------------------------------------------------------------------------- 1 | module.exports = function isAuthorized (req, res, next) { 2 | if (process.env.NODE_ENV === 'test') return next() 3 | 4 | const token = req.headers['x-token'] 5 | if (!token) return res.forbidden() 6 | 7 | try { 8 | const decryptedSessionStorageToken = TokenService.verify(token) 9 | 10 | User 11 | .findOne({id: decryptedSessionStorageToken.id}) 12 | .exec((error, user) => { 13 | if (error) return res.serverError(error) 14 | if (user) return next() 15 | }) 16 | } catch (error) { 17 | return res.forbidden(error) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/api/services/CryptographyService.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | module.exports = { 4 | encrypt: (decrypted) => { 5 | let cipher = crypto.createCipher('aes-256-cbc', 'd6F3Efeq') 6 | let crypted = cipher.update(decrypted.toString(), 'utf8', 'hex') 7 | crypted += cipher.final('hex') 8 | 9 | return crypted 10 | }, 11 | 12 | decrypt: (encrypted) => { 13 | let decipher = crypto.createDecipher('aes-256-cbc', 'd6F3Efeq') 14 | let decrypted = decipher.update(encrypted.toString(), 'hex', 'utf8') 15 | decrypted += decipher.final('utf8') 16 | 17 | return decrypted 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/assets/sw.js: -------------------------------------------------------------------------------- 1 | importScripts("/precache-manifest.28eda1f90a81d9bfde2bfaf166d3f21f.js", "https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js"); 2 | 3 | /* eslint-disable */ 4 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []) 5 | 6 | workbox.routing.registerRoute( 7 | new RegExp('/'), 8 | workbox.strategies.networkFirst() 9 | ) 10 | 11 | workbox.routing.registerRoute( 12 | new RegExp('.*\.js'), 13 | workbox.strategies.staleWhileRevalidate() 14 | ) 15 | 16 | workbox.routing.registerRoute( 17 | new RegExp('(.*)/api/(.*)$'), 18 | workbox.strategies.networkFirst() 19 | ) 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Product paradise 10 | 11 | 12 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /backend/tasks/register/compileAssets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `compileAssets` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist is not designed to be used directly-- rather 7 | * it is a helper called by the `default`, `prod`, `build`, and 8 | * `buildProd` tasklists. 9 | * 10 | * For more information see: 11 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/compile-assets-js 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | grunt.registerTask('compileAssets', [ 16 | 'clean:dev', 17 | 'jst:dev', 18 | 'less:dev', 19 | 'copy:dev', 20 | 'coffee:dev' 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/src/bootstrap.mixin.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueResource from 'vue-resource' 3 | import VueTranslate from 'vue-translate-plugin' 4 | import store from './../src/state/index' 5 | 6 | Vue.use(VueResource) 7 | Vue.use(VueTranslate) 8 | 9 | const LocaleMixin = { 10 | computed: { 11 | locale: { 12 | get () { 13 | return this.$store.state.locale 14 | }, 15 | 16 | set (locale) { 17 | this.$store.commit('SET_LOCALE', locale) 18 | } 19 | } 20 | }, 21 | 22 | watch: { 23 | locale () { 24 | this.$translate.setLang(this.locale) 25 | } 26 | } 27 | } 28 | 29 | export { 30 | Vue, 31 | store, 32 | LocaleMixin 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') { 2 | window.addEventListener('load', () => { 3 | navigator.serviceWorker.register('/sw.js') 4 | .then(registration => console.log(registration)) 5 | .catch(registrationError => console.log(registrationError)) 6 | }) 7 | } 8 | 9 | const browserWidth = Math.max( 10 | document.body.scrollWidth, 11 | document.documentElement.scrollWidth, 12 | document.body.offsetWidth, 13 | document.documentElement.offsetWidth, 14 | document.documentElement.clientWidth 15 | ) 16 | 17 | if (browserWidth >= 768) import('./bootstrap.desktop') 18 | if (browserWidth < 768) import('./bootstrap.mobile') 19 | -------------------------------------------------------------------------------- /frontend/src/components/user/shop/BasketIndex.mixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | basket: { 4 | get () { 5 | return this.$store.state.Basket.basket 6 | } 7 | }, 8 | 9 | totalPrice: { 10 | get () { 11 | let totalPrice = 0 12 | this.basket.products.forEach(product => { 13 | totalPrice += product.price 14 | }) 15 | 16 | return Math.round(totalPrice * 100) / 100 17 | } 18 | } 19 | }, 20 | 21 | methods: { 22 | async checkout () { 23 | await this.$store.dispatch('checkout', this.basket) 24 | }, 25 | 26 | removeProduct (index) { 27 | this.$store.commit('REMOVE_PRODUCT_FROM_BASKET', index) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/api/controllers/LoginController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | post: async (req, res) => { 3 | const {name, password} = req.allParams() 4 | 5 | const user = await User 6 | .findOne({name}) 7 | .catch(error => res.serverError(error)) 8 | 9 | if (!user) return res.forbidden() 10 | 11 | User 12 | .isValidPassword(password, user, (error, isValid) => { 13 | if (error) return res.serverError(error) 14 | if (!isValid) return res.forbidden() 15 | 16 | sails.log.info('User logged in', user) 17 | 18 | return res.json({ 19 | xToken: TokenService.issue({id: user.id}), 20 | cookie: CryptographyService.encrypt(user.id) 21 | }) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | 15 | module.exports = function (on, config) { 16 | // `on` is used to hook into various events Cypress emits 17 | // `config` is the resolved Cypress config 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/ProductsIndex.desktop.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /backend/tasks/register/linkAssetsBuild.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `linkAssetsBuild` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist is not designed to be used directly-- rather 7 | * it is a helper called by the `build` tasklist. 8 | * 9 | * For more information see: 10 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/link-assets-build-js 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | grunt.registerTask('linkAssetsBuild', [ 15 | 'sails-linker:devJsRelative', 16 | 'sails-linker:devStylesRelative', 17 | 'sails-linker:devTpl', 18 | 'sails-linker:devJsRelativeJade', 19 | 'sails-linker:devStylesRelativeJade', 20 | 'sails-linker:devTplJade' 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/src/components/Register.mixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | user: { 4 | get () { 5 | return this.$store.state.User.user 6 | } 7 | }, 8 | 9 | name: { 10 | get () { 11 | return this.$store.state.User.user.name 12 | }, 13 | 14 | set (name) { 15 | this.$store.commit('SET_USER_NAME', name) 16 | } 17 | }, 18 | 19 | password: { 20 | get () { 21 | return this.$store.state.User.user.password 22 | }, 23 | 24 | set (password) { 25 | this.$store.commit('SET_USER_PASSWORD', password) 26 | } 27 | } 28 | }, 29 | 30 | methods: { 31 | async signUp () { 32 | await this.$store.dispatch('signUp', this.user) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /backend/tasks/register/linkAssetsBuildProd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `linkAssetsBuildProd` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist is not designed to be used directly-- rather 7 | * it is a helper called by the `buildProd` tasklist. 8 | * 9 | * For more information see: 10 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/link-assets-build-prod-js 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | grunt.registerTask('linkAssetsBuildProd', [ 15 | 'sails-linker:prodJsRelative', 16 | 'sails-linker:prodStylesRelative', 17 | 'sails-linker:devTpl', 18 | 'sails-linker:prodJsRelativeJade', 19 | 'sails-linker:prodStylesRelativeJade', 20 | 'sails-linker:devTplJade' 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/src/components/user/shop/BasketIndex.mobile.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | const WorkboxPlugin = require('workbox-webpack-plugin') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | productionSourceMap: false, 6 | 7 | configureWebpack: { 8 | resolve: { 9 | alias: { 10 | 'vue$': 'vue/dist/vue.esm.js' 11 | } 12 | }, 13 | 14 | plugins: [ 15 | new WorkboxPlugin.InjectManifest({ 16 | swSrc: path.join(__dirname, 'src/sw.js') 17 | }) 18 | ] 19 | }, 20 | 21 | lintOnSave: 'error', 22 | 23 | outputDir: '../backend/assets', 24 | 25 | devServer: { 26 | proxy: { 27 | '/api': { 28 | target: 'http://localhost:1337/api', 29 | changeOrigin: true, 30 | pathRewrite: { 31 | '^/api': '' 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/tasks/register/linkAssets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `linkAssets` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist is not designed to be used directly-- rather 7 | * it is a helper called by the `default` tasklist and the `watch` task 8 | * (but only if the `grunt-sails-linker` package is in use). 9 | * 10 | * For more information see: 11 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/link-assets-js 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | grunt.registerTask('linkAssets', [ 16 | 'sails-linker:devJs', 17 | 'sails-linker:devStyles', 18 | 'sails-linker:devTpl', 19 | 'sails-linker:devJsJade', 20 | 'sails-linker:devStylesJade', 21 | 'sails-linker:devTplJade' 22 | ]); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/cypress/integration/Login.spec.js: -------------------------------------------------------------------------------- 1 | describe('Login', () => { 2 | describe('desktop', () => { 3 | beforeEach(() => { 4 | cy.viewport(414, 736) 5 | cy.visit('/login') 6 | }) 7 | 8 | it('loads the app', () => { 9 | cy.get('.mint-header').should('be.visible') 10 | }) 11 | 12 | it('accepts inputs and sends post data', () => { 13 | cy.server() 14 | }) 15 | }) 16 | 17 | describe('mobile', () => { 18 | beforeEach(() => { 19 | cy.viewport(1280, 768) 20 | cy.visit('/login') 21 | }) 22 | 23 | it('loads the app', () => { 24 | cy.get('.container').should('be.visible') 25 | }) 26 | 27 | it('accepts inputs and sends post data', () => { 28 | cy.server() 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /frontend/src/components/Home.mixin.js: -------------------------------------------------------------------------------- 1 | import { mapActions } from 'vuex' 2 | import he from 'he' 3 | 4 | export default { 5 | created () { 6 | this.getProducts(this.currentPage) 7 | }, 8 | 9 | data: () => ({ 10 | currentPage: 1 11 | }), 12 | 13 | watch: { 14 | currentPage () { 15 | this.getProducts(this.currentPage) 16 | } 17 | }, 18 | 19 | computed: { 20 | products: { 21 | get () { 22 | return this.$store.state.Products.products.products 23 | } 24 | }, 25 | 26 | amountOfProducts: { 27 | get () { 28 | return this.$store.state.Products.products.amountOfProducts 29 | } 30 | } 31 | }, 32 | 33 | methods: { 34 | encode: text => he.encode(text), 35 | 36 | ...mapActions(['getProducts']) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/tasks/register/prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `prod` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist will be executed instead of `default` when 7 | * your Sails app is lifted in a production environment (e.g. using 8 | * `NODE_ENV=production node app`). 9 | * 10 | * For more information see: 11 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/prod-js 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | grunt.registerTask('prod', [ 16 | 'compileAssets', 17 | 'concat', 18 | 'uglify', 19 | 'cssmin', 20 | 'sails-linker:prodJs', 21 | 'sails-linker:prodStyles', 22 | 'sails-linker:devTpl', 23 | 'sails-linker:prodJsJade', 24 | 'sails-linker:prodStylesJade', 25 | 'sails-linker:devTplJade' 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /backend/assets/js/0011.d26555d2.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["0011"],{"0011":function(t,n,e){"use strict";e.r(n);var s=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("div",{staticClass:"row"},[e("div",{staticClass:"col"},[e("b-card",{attrs:{"no-body":""}},[e("b-tabs",{ref:"tabs",attrs:{card:""}},[e("b-tab",{attrs:{title:t.t("productindex.mixin.tab.first")}},[e("products-get")],1),e("b-tab",{attrs:{title:t.t("productindex.mixin.tab.second")}},[e("product-post")],1)],1)],1)],1)])},o=[],r={components:{ProductPost:function(){return Promise.all([e.e("chunk-6017"),e.e("chunk-abc6")]).then(e.bind(null,"905d"))},ProductsGet:function(){return e.e("chunk-1a17").then(e.bind(null,"8205"))}}},c=r,a=e("2877"),i=Object(a["a"])(c,s,o,!1,null,null,null);i.options.__file="ProductsIndex.desktop.vue";n["default"]=i.exports}}]); -------------------------------------------------------------------------------- /backend/tasks/config/coffee.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `coffee` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Compile CoffeeScript files located in `assets/js` into Javascript 7 | * and generate new `.js` files in `.tmp/public/js`. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-coffee 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('coffee', { 16 | dev: { 17 | options: { 18 | bare: true, 19 | sourceMap: true, 20 | sourceRoot: './' 21 | }, 22 | files: [{ 23 | expand: true, 24 | cwd: 'assets/js/', 25 | src: ['**/*.coffee'], 26 | dest: '.tmp/public/js/', 27 | ext: '.js' 28 | }] 29 | } 30 | }); 31 | 32 | grunt.loadNpmTasks('grunt-contrib-coffee'); 33 | }; 34 | -------------------------------------------------------------------------------- /backend/tasks/config/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `watch` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Run predefined tasks whenever watched file patterns are added, changed or deleted. 7 | * 8 | * Watch for changes on: 9 | * - files in the `assets` folder 10 | * - the `tasks/pipeline.js` file 11 | * and re-run the appropriate tasks. 12 | * 13 | * For usage docs see: 14 | * https://github.com/gruntjs/grunt-contrib-watch 15 | * 16 | */ 17 | module.exports = function(grunt) { 18 | 19 | grunt.config.set('watch', { 20 | assets: { 21 | 22 | // Assets to watch: 23 | files: ['assets/**/*', 'tasks/pipeline.js', '!**/node_modules/**'], 24 | 25 | // When assets are changed: 26 | tasks: ['syncAssets' , 'linkAssets' ] 27 | } 28 | }); 29 | 30 | grunt.loadNpmTasks('grunt-contrib-watch'); 31 | }; 32 | -------------------------------------------------------------------------------- /backend/tasks/config/less.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `less` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Compile your LESS files into a CSS stylesheet. 7 | * 8 | * By default, only the `assets/styles/importer.less` is compiled. 9 | * This allows you to control the ordering yourself, i.e. import your 10 | * dependencies, mixins, variables, resets, etc. before other stylesheets) 11 | * 12 | * For usage docs see: 13 | * https://github.com/gruntjs/grunt-contrib-less 14 | * 15 | */ 16 | module.exports = function(grunt) { 17 | 18 | grunt.config.set('less', { 19 | dev: { 20 | files: [{ 21 | expand: true, 22 | cwd: 'assets/styles/', 23 | src: ['importer.less'], 24 | dest: '.tmp/public/styles/', 25 | ext: '.css' 26 | }] 27 | } 28 | }); 29 | 30 | grunt.loadNpmTasks('grunt-contrib-less'); 31 | }; 32 | -------------------------------------------------------------------------------- /backend/tasks/config/cssmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compress CSS files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minify the intermediate concatenated CSS stylesheet which was 7 | * prepared by the `concat` task at `.tmp/public/concat/production.css`. 8 | * 9 | * Together with the `concat` task, this is the final step that minifies 10 | * all CSS files from `assets/styles/` (and potentially your LESS importer 11 | * file from `assets/styles/importer.less`) 12 | * 13 | * For usage docs see: 14 | * https://github.com/gruntjs/grunt-contrib-cssmin 15 | * 16 | */ 17 | module.exports = function(grunt) { 18 | 19 | grunt.config.set('cssmin', { 20 | dist: { 21 | src: ['.tmp/public/concat/production.css'], 22 | dest: '.tmp/public/min/production.min.css' 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 27 | }; 28 | -------------------------------------------------------------------------------- /backend/tasks/register/buildProd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `buildProd` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist will be executed instead of `build` if you 7 | * run `sails www` in a production environment, e.g.: 8 | * `NODE_ENV=production sails www` 9 | * 10 | * This generates a folder containing your compiled (and usually minified) 11 | * assets. The most common use case for this is bundling up files to 12 | * deploy to a CDN. 13 | * 14 | * For more information see: 15 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/build-prod-js 16 | * 17 | */ 18 | module.exports = function(grunt) { 19 | grunt.registerTask('buildProd', [ 20 | 'compileAssets', 21 | 'concat', 22 | 'uglify', 23 | 'cssmin', 24 | 'linkAssetsBuildProd', 25 | 'clean:build', 26 | 'copy:build' 27 | ]); 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /backend/tasks/config/sync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `sync` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Synchronize files from the `assets` folder to `.tmp/public`, 7 | * smashing anything that's already there. 8 | * 9 | * This task synchronizes one directory with another (like rsync). 10 | * In the default Sails asset pipeline, it plays very similar role 11 | * to `grunt-contrib-copy`, but copies only those files that have 12 | * actually changed since the last time the task was run. 13 | * 14 | * For usage docs see: 15 | * https://github.com/tomusdrw/grunt-sync 16 | * 17 | */ 18 | module.exports = function(grunt) { 19 | 20 | grunt.config.set('sync', { 21 | dev: { 22 | files: [{ 23 | cwd: './assets', 24 | src: ['**/*.!(coffee|less)'], 25 | dest: '.tmp/public' 26 | }] 27 | } 28 | }); 29 | 30 | grunt.loadNpmTasks('grunt-sync'); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/ProductsIndex.mobile.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | -------------------------------------------------------------------------------- /backend/tasks/register/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `build` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist will be executed if you run `sails www` or 7 | * `grunt build` in a development environment. It generates a 8 | * folder containing your compiled assets, e.g. for troubleshooting 9 | * issues with other Grunt plugins, bundling assets for an Electron 10 | * or PhoneGap app, or deploying your app's flat files to a CDN. 11 | * 12 | * Note that when running `sails www` in a production environment (with the 13 | * `NODE_ENV` environment variable set to 'production') the `buildProd` task 14 | * will be run instead of this one. 15 | * 16 | * For more information see: 17 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/build-js 18 | * 19 | */ 20 | module.exports = function(grunt) { 21 | grunt.registerTask('build', [ 22 | 'compileAssets', 23 | 'linkAssetsBuild', 24 | 'clean:build', 25 | 'copy:build' 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /backend/tasks/config/concat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `concat` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Concatenates the contents of multiple JavaScript and/or CSS files 7 | * into two new files, each located at `concat/production.js` and 8 | * `concat/production.css` respectively in `.tmp/public/concat`. 9 | * 10 | * This is used as an intermediate step to generate monolithic files 11 | * that can then be passed in to `uglify` and/or `cssmin` for minification. 12 | * 13 | * For usage docs see: 14 | * https://github.com/gruntjs/grunt-contrib-concat 15 | * 16 | */ 17 | module.exports = function(grunt) { 18 | 19 | grunt.config.set('concat', { 20 | js: { 21 | src: require('../pipeline').jsFilesToInject, 22 | dest: '.tmp/public/concat/production.js' 23 | }, 24 | css: { 25 | src: require('../pipeline').cssFilesToInject, 26 | dest: '.tmp/public/concat/production.css' 27 | } 28 | }); 29 | 30 | grunt.loadNpmTasks('grunt-contrib-concat'); 31 | }; 32 | -------------------------------------------------------------------------------- /backend/config/env/development.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Development environment settings 3 | * 4 | * This file can include shared settings for a development team, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | /*************************************************************************** 16 | * Set the default database connection for models in the development * 17 | * environment (see config/connections.js and config/models.js ) * 18 | ***************************************************************************/ 19 | 20 | session: { 21 | cookie: { 22 | secure: false 23 | } 24 | } 25 | 26 | // models: { 27 | // connection: 'someMongodbServer' 28 | // } 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /backend/api/controllers/user/products/ProductsController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getProducts: async (req, res) => { 3 | const page = req.param('page') 4 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 5 | 6 | const amountOfProducts = await Product 7 | .count() 8 | .where({user: {'!': userIdentifier}}) 9 | .catch(error => res.serverError(error)) 10 | 11 | const products = await Product 12 | .find() 13 | .where({user: {'!': userIdentifier}}) 14 | .populate('user', { select: ['name'] }) 15 | .paginate({page, limit: 6}) 16 | .catch(error => res.serverError(error)) 17 | 18 | return res.json({ 19 | products, 20 | amountOfProducts 21 | }) 22 | }, 23 | 24 | getProductsByUser: async (req, res) => { 25 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 26 | 27 | const products = await Product 28 | .find({user: userIdentifier}) 29 | .catch(error => res.serverError(error)) 30 | 31 | return res.json(products) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/components/user/shop/ShopIndex.mixin.js: -------------------------------------------------------------------------------- 1 | import { mapMutations } from 'vuex' 2 | import he from 'he' 3 | 4 | export default { 5 | async created () { 6 | await this.$store.dispatch('getUser') 7 | await this.$store.dispatch('getShopProducts', this.currentPage) 8 | }, 9 | 10 | data: () => ({ 11 | currentPage: 1 12 | }), 13 | 14 | watch: { 15 | async currentPage () { 16 | await this.$store.dispatch('getShopProducts', this.currentPage) 17 | } 18 | }, 19 | 20 | computed: { 21 | products: { 22 | get () { 23 | return this.$store.state.Products.products.products 24 | } 25 | }, 26 | 27 | amountOfProducts: { 28 | get () { 29 | return this.$store.state.Products.products.amountOfProducts 30 | } 31 | }, 32 | 33 | user: { 34 | get () { 35 | return this.$store.state.User.user 36 | } 37 | } 38 | }, 39 | 40 | methods: { 41 | encode: text => he.encode(text), 42 | 43 | ...mapMutations({ 44 | pushToBasket: 'PUSH_TO_BASKET' 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/App.mixin.js: -------------------------------------------------------------------------------- 1 | import { mapMutations } from 'vuex' 2 | import locales from './locales' 3 | import isNull from 'lodash/isNull' 4 | 5 | export default { 6 | name: 'app', 7 | 8 | locales, 9 | 10 | mounted () { 11 | this.$translate.setLang('en') 12 | }, 13 | 14 | created () { 15 | const basket = JSON.parse(sessionStorage.getItem('basket')) 16 | if (!isNull(basket)) this.$store.commit('SET_BASKET', basket) 17 | }, 18 | 19 | computed: { 20 | isUserAuthenticated: { 21 | get () { 22 | return this.$store.state.isUserAuthenticated 23 | }, 24 | 25 | set (isUserAuthenticated) { 26 | this.$store.commit('SET_IS_USER_AUTHENTICATED', isUserAuthenticated) 27 | } 28 | }, 29 | 30 | basket: { 31 | get () { 32 | return this.$store.state.Basket.basket 33 | } 34 | } 35 | }, 36 | 37 | methods: { 38 | deleteCookie: cookie => { document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;` }, 39 | 40 | ...mapMutations({ 41 | setLocale: 'SET_LOCALE' 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/test/functional/specs/Register.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const chai = require('chai') 3 | const faker = require('faker') 4 | 5 | describe('Register', function() { 6 | it('should persist data', function(done) { 7 | const name = faker.name.findName() 8 | const password = faker.internet.password() 9 | 10 | request(sails.hooks.http.app) 11 | .post('/api/register/post') 12 | .send({ 13 | name, 14 | password 15 | }) 16 | .expect(200) 17 | .then(() => { 18 | 19 | User 20 | .find({ 21 | name 22 | }) 23 | .then(user => { 24 | chai.assert.isDefined(user) 25 | done() 26 | }) 27 | .catch(error => done(error)) 28 | }) 29 | }) 30 | 31 | it('should accept inputs', function(done) { 32 | request(sails.hooks.http.app) 33 | .post('/api/register/post') 34 | .send({ 35 | name: faker.name.findName(), 36 | password: faker.internet.password() 37 | }) 38 | .expect(200, done) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /frontend/src/components/Register.mobile.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | -------------------------------------------------------------------------------- /backend/api/models/User.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs') 2 | 3 | module.exports = { 4 | attributes: { 5 | name: { 6 | type: 'string', 7 | required: true 8 | }, 9 | password: { 10 | type: 'string', 11 | required: true 12 | }, 13 | products: { 14 | collection: 'product', 15 | via: 'user' 16 | }, 17 | baskets: { 18 | collection: 'basket', 19 | via: 'user' 20 | } 21 | }, 22 | 23 | beforeCreate: (user, next) => { 24 | bcrypt.genSalt(10, (error, salt) => { 25 | if (error) return next(error) 26 | 27 | bcrypt.hash(user.password, salt, (error, hash) => { 28 | if (error) return next(error) 29 | 30 | user.password = hash 31 | next() 32 | }) 33 | }) 34 | }, 35 | 36 | isValidPassword: (password, user, callback) => { 37 | bcrypt.compare(password, user.password, (error, isMatch) => { 38 | if (error) return callback(error) 39 | 40 | if (isMatch) { 41 | callback(null, true) 42 | } else callback(new Error('Passwords doesn\'t match'), false) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/product/ProductPost.mixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | created () { 3 | this.$store.commit('RESET_PRODUCT') 4 | }, 5 | 6 | computed: { 7 | product: { 8 | get () { 9 | return this.$store.state.Product.product 10 | } 11 | }, 12 | 13 | user: { 14 | get () { 15 | return this.$store.state.User.user 16 | } 17 | }, 18 | 19 | title: { 20 | get () { 21 | return this.$store.state.Product.product.title 22 | }, 23 | 24 | set (title) { 25 | this.$store.commit('SET_PRODUCT_TITLE', title) 26 | } 27 | }, 28 | 29 | description: { 30 | get () { 31 | return this.$store.state.Product.product.description 32 | }, 33 | 34 | set (description) { 35 | this.$store.commit('SET_PRODUCT_DESCRIPTION', description) 36 | } 37 | }, 38 | 39 | price: { 40 | get () { 41 | return this.$store.state.Product.product.price 42 | }, 43 | 44 | set (price) { 45 | this.$store.commit('SET_PRODUCT_PRICE', price) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Julian Claus 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 | -------------------------------------------------------------------------------- /backend/tasks/register/default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `default` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This is the default Grunt tasklist that will be executed if you 7 | * run `grunt` in the top level directory of your app. It is also 8 | * called automatically when you start Sails in development mode using 9 | * `sails lift` or `node app`. 10 | * 11 | * Note that when lifting your app with a custom environment setting 12 | * (i.e. `sails.config.environment`), Sails will look for a tasklist file 13 | * with the same name and run that instead of this one. 14 | * 15 | * > Note that as a special case for compatibility/historial reasons, if 16 | * > your environment is "production", and Sails cannot find a tasklist named 17 | * > `production.js`, it will attempt to run the `prod.js` tasklist as well 18 | * > before defaulting to `default.js`. 19 | * 20 | * For more information see: 21 | * http://sailsjs.org/documentation/anatomy/my-app/tasks/register/default-js 22 | * 23 | */ 24 | module.exports = function (grunt) { 25 | grunt.registerTask('default', ['compileAssets', 'linkAssets', 'watch']); 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/state/products/products.actions.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | getProducts (context, page) { 5 | return new Promise((resolve, reject) => { 6 | Vue.http 7 | .get('/api/products/get', {params: {page}}) 8 | .then(({body}) => { 9 | context.commit('SET_PRODUCTS', body) 10 | resolve() 11 | }) 12 | .catch(error => reject(error)) 13 | }) 14 | }, 15 | 16 | getShopProducts (context, page) { 17 | return new Promise((resolve, reject) => { 18 | Vue.http 19 | .get('/api/user/products/get', {params: {page}}) 20 | .then(({body}) => { 21 | context.commit('SET_PRODUCTS', body) 22 | resolve() 23 | }) 24 | .catch(error => reject(error)) 25 | }) 26 | }, 27 | 28 | getProductsByUser (context) { 29 | return new Promise((resolve, reject) => { 30 | Vue.http 31 | .get('/api/user/products/getProductsByUser') 32 | .then(({body}) => { 33 | context.commit('SET_PRODUCTS', body) 34 | resolve() 35 | }) 36 | .catch(error => reject(error)) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-sails-example", 3 | "version": "0.0.0", 4 | "description": "Vue.js with Sails.js example project", 5 | "main": "backend/app.js", 6 | "scripts": { 7 | "dev": "cd backend && npm run dev & cd frontend && npm run dev", 8 | "commitmsg": "commitlint -e $GIT_PARAMS", 9 | "install": "cd frontend && npm i && cd ../backend && npm i && cd ..", 10 | "install:dev": "npm run install && npm run dev", 11 | "push": "git push && git push heroku `git subtree split --prefix backend master`:master --force" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/ndabAP/Vue.js-with-Sails.js-backend-example-project.git" 16 | }, 17 | "author": "Julian Claus ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/ndabAP/Vue.js-with-Sails.js-backend-example-project/issues" 21 | }, 22 | "homepage": "https://github.com/ndabAP/Vue.js-with-Sails.js-backend-example-project#readme", 23 | "devDependencies": { 24 | "@commitlint/cli": "^7.0.0", 25 | "@commitlint/config-conventional": "^7.0.1", 26 | "husky": "^0.14.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/state/user/user.actions.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | getUser (context) { 5 | return new Promise((resolve, reject) => { 6 | Vue.http 7 | .get('/api/user/get') 8 | .then(({body}) => { 9 | context.commit('SET_USER', body) 10 | resolve() 11 | }) 12 | .catch(error => reject(error)) 13 | }) 14 | }, 15 | 16 | signUp (context, {name, password}) { 17 | return new Promise((resolve, reject) => { 18 | Vue.http 19 | .post('/api/register/post', {name, password}) 20 | .then(() => { 21 | context.commit('RESET_USER') 22 | resolve() 23 | }) 24 | .catch(error => reject(error)) 25 | }) 26 | }, 27 | 28 | signIn (context, {name, password}) { 29 | return new Promise((resolve, reject) => { 30 | Vue.http 31 | .post('/api/login/post', { 32 | name, 33 | password 34 | }) 35 | .then(({body}) => { 36 | context.commit('SET_IS_USER_AUTHENTICATED', true) 37 | resolve(body) 38 | }) 39 | .catch(error => reject(error)) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/assets/js/83d0.4eb65a7f.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["83d0"],{"83d0":function(t,n,e){"use strict";e.r(n);var r=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("div",[e("mt-navbar",{model:{value:t.currentTab,callback:function(n){t.currentTab=n},expression:"currentTab"}},[e("mt-tab-item",{attrs:{id:"products-get"}},[t._v(t._s(t.t("productindex.mixin.tab.first")))]),e("mt-tab-item",{attrs:{id:"product-post"}},[t._v(t._s(t.t("productindex.mixin.tab.second")))])],1),e("mt-tab-container",{model:{value:t.currentTab,callback:function(n){t.currentTab=n},expression:"currentTab"}},[e("mt-tab-container-item",{attrs:{id:"products-get"}},[e("products-get")],1),e("mt-tab-container-item",{attrs:{id:"product-post"}},[e("product-post")],1)],1)],1)},c=[],u=(e("cadf"),e("551c"),e("097d"),{components:{ProductPost:function(){return Promise.all([e.e("chunk-6017"),e.e("chunk-db80")]).then(e.bind(null,"0a8e"))},ProductsGet:function(){return e.e("chunk-01c9").then(e.bind(null,"6b42"))}},data:function(){return{currentTab:"products-get"}}}),o=u,a=e("2877"),i=Object(a["a"])(o,r,c,!1,null,null,null);i.options.__file="ProductsIndex.mobile.vue";n["default"]=i.exports}}]); -------------------------------------------------------------------------------- /frontend/cypress/fixtures/mocks.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker' 2 | 3 | export default (entity, limit, parameters) => { 4 | const entities = { 5 | products: limit => { 6 | let products = [] 7 | for (let index = 0; index < limit; index++) { 8 | products.push({ 9 | id: index, 10 | title: faker.commerce.productName(), 11 | description: faker.lorem.sentence(), 12 | price: faker.commerce.price(), 13 | user: { 14 | id: index, 15 | name: faker.name.firstName() 16 | } 17 | }) 18 | } 19 | 20 | return { 21 | products, 22 | amountOfProducts: parameters.amountOfProducts 23 | } 24 | }, 25 | 26 | productsByUser: limit => { 27 | let products = [] 28 | for (let index = 0; index < limit; index++) { 29 | products.push({ 30 | id: index, 31 | title: faker.commerce.productName(), 32 | description: faker.lorem.sentence(), 33 | price: faker.commerce.price(), 34 | user: index 35 | }) 36 | } 37 | 38 | return products 39 | } 40 | } 41 | 42 | return entities[entity](limit) 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/product/ProductPatch.mixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: ['id'], 3 | 4 | computed: { 5 | user: { 6 | get () { 7 | return this.$store.state.User.user 8 | } 9 | }, 10 | 11 | title: { 12 | get () { 13 | return this.$store.state.Product.product.title 14 | }, 15 | 16 | set (title) { 17 | this.$store.commit('SET_PRODUCT_TITLE', title) 18 | } 19 | }, 20 | 21 | description: { 22 | get () { 23 | return this.$store.state.Product.product.description 24 | }, 25 | 26 | set (description) { 27 | this.$store.commit('SET_PRODUCT_DESCRIPTION', description) 28 | } 29 | }, 30 | 31 | price: { 32 | get () { 33 | return this.$store.state.Product.product.price 34 | }, 35 | 36 | set (price) { 37 | this.$store.commit('SET_PRODUCT_PRICE', price) 38 | } 39 | }, 40 | 41 | isVisibleProductPatch: { 42 | get () { 43 | return this.$store.state.isVisibleProductPatch 44 | }, 45 | 46 | set (isVisibleProductPatch) { 47 | this.$store.commit('SET_IS_VISIBLE_PRODUCT_PATCH', isVisibleProductPatch) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/bootstrap.mobile.js: -------------------------------------------------------------------------------- 1 | import { Vue, store, LocaleMixin } from './bootstrap.mixin' 2 | import App from './App.mobile' 3 | import router from './router/router.mobile' 4 | import MintUI from 'mint-ui' 5 | import 'mint-ui/lib/style.css' 6 | 7 | Vue.use(MintUI) 8 | 9 | Vue.http.interceptors.push((request, next) => { 10 | if (request.url !== '/api/login/post') { 11 | const xToken = window.localStorage.getItem('x-token') 12 | request.headers.set('X-Token', xToken) 13 | } 14 | 15 | next(response => { 16 | if ((response.status === 404) || (response.status === 504)) { 17 | router.push({name: 'Home'}) 18 | } 19 | }) 20 | }) 21 | 22 | router.beforeEach((to, from, next) => { 23 | if (to.matched.some(record => record.meta.userOnly)) { 24 | const getCookie = name => { 25 | let cookies = `; ${document.cookie}`.match(`;\\s*${name}=([^;]+)`) 26 | return cookies ? cookies[1] : '' 27 | } 28 | 29 | if (getCookie('user')) { 30 | next() 31 | } else router.push({name: 'Login'}) 32 | } else next() 33 | }) 34 | 35 | /* eslint-disable no-new */ 36 | new Vue({ 37 | el: '#app', 38 | store, 39 | router, 40 | template: '', 41 | mixins: [LocaleMixin], 42 | components: { 43 | App 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /backend/test/functional/specs/Login.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const chai = require('chai') 3 | const faker = require('faker') 4 | 5 | describe('Login', function() { 6 | it('should send a non empty JSON response', function(done) { 7 | let name = 'Joe' 8 | let password = 'toasty' 9 | 10 | request(sails.hooks.http.app) 11 | .post('/api/register/post') 12 | .send({ 13 | name, 14 | password 15 | }) 16 | .expect(200) 17 | .then(() => { 18 | 19 | request(sails.hooks.http.app) 20 | .post('/api/login/post') 21 | .send({ 22 | name, 23 | password 24 | }) 25 | .expect(200) 26 | .end((error, response) => { 27 | if (error) return done(error) 28 | 29 | chai.assert.isNotEmpty(response.body.xToken) 30 | chai.assert.isNotEmpty(response.body.cookie) 31 | done() 32 | }) 33 | }) 34 | }) 35 | 36 | it('should deny access', function(done) { 37 | request(sails.hooks.http.app) 38 | .post('/api/login/post') 39 | .send({ 40 | name: faker.name.findName(), 41 | password: faker.internet.password() 42 | }) 43 | .expect(403, done) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /backend/config/locales/_README.md: -------------------------------------------------------------------------------- 1 | # Internationalization / Localization Settings 2 | 3 | > Also see the official docs on internationalization/localization: 4 | > http://links.sailsjs.org/docs/config/locales 5 | 6 | ## Locales 7 | All locale files live under `config/locales`. Here is where you can add translations 8 | as JSON key-value pairs. The name of the file should match the language that you are supporting, which allows for automatic language detection based on request headers. 9 | 10 | Here is an example locale stringfile for the Spanish language (`config/locales/es.json`): 11 | ```json 12 | { 13 | "Hello!": "Hola!", 14 | "Hello %s, how are you today?": "¿Hola %s, como estas?", 15 | } 16 | ``` 17 | ## Usage 18 | Locales can be accessed in controllers/policies through `res.i18n()`, or in views through the `__(key)` or `i18n(key)` functions. 19 | Remember that the keys are case sensitive and require exact key matches, e.g. 20 | 21 | ```ejs 22 |

<%= __('Welcome to PencilPals!') %>

23 |

<%= i18n('Hello %s, how are you today?', 'Pencil Maven') %>

24 |

<%= i18n('That\'s right-- you can use either i18n() or __()') %>

25 | ``` 26 | 27 | ## Configuration 28 | Localization/internationalization config can be found in `config/i18n.js`, from where you can set your supported locales. 29 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/ProductsGet.mixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data: () => ({ 3 | id: '' 4 | }), 5 | 6 | async created () { 7 | await this.$store.dispatch('getProductsByUser', this.user) 8 | }, 9 | 10 | computed: { 11 | fields: { 12 | get () { 13 | return { 14 | title: { 15 | label: this.t('productsget.mixin.field.first') 16 | }, 17 | 18 | description: { 19 | label: this.t('productsget.mixin.field.second') 20 | }, 21 | 22 | price: { 23 | label: this.t('productsget.mixin.field.third') 24 | }, 25 | 26 | actions: { 27 | label: this.t('productsget.mixin.field.fourth') 28 | } 29 | } 30 | } 31 | }, 32 | 33 | products: { 34 | get () { 35 | return this.$store.state.Products.products 36 | } 37 | }, 38 | 39 | user: { 40 | get () { 41 | return this.$store.state.User.user 42 | } 43 | }, 44 | 45 | isVisibleProductPatch: { 46 | get () { 47 | return this.$store.state.isVisibleProductPatch 48 | }, 49 | 50 | set (isVisibleProductPatch) { 51 | this.$store.commit('SET_IS_VISIBLE_PRODUCT_PATCH', isVisibleProductPatch) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/tasks/config/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `copy` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Copy files and/or folders from your `assets/` directory into 7 | * the web root (`.tmp/public`) so they can be served via HTTP, 8 | * and also for further pre-processing by other Grunt tasks. 9 | * 10 | * #### Normal usage (`sails lift`) 11 | * Copies all directories and files (except CoffeeScript and LESS) 12 | * from the `assets/` folder into the web root -- conventionally a 13 | * hidden directory located `.tmp/public`. 14 | * 15 | * #### Via the `build` tasklist (`sails www`) 16 | * Copies all directories and files from the .tmp/public directory into a www directory. 17 | * 18 | * For usage docs see: 19 | * https://github.com/gruntjs/grunt-contrib-copy 20 | * 21 | */ 22 | module.exports = function(grunt) { 23 | 24 | grunt.config.set('copy', { 25 | dev: { 26 | files: [{ 27 | expand: true, 28 | cwd: './assets', 29 | src: ['**/*.!(coffee|less)'], 30 | dest: '.tmp/public' 31 | }] 32 | }, 33 | build: { 34 | files: [{ 35 | expand: true, 36 | cwd: '.tmp/public', 37 | src: ['**/*'], 38 | dest: 'www' 39 | }] 40 | } 41 | }); 42 | 43 | grunt.loadNpmTasks('grunt-contrib-copy'); 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/src/components/Register.desktop.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/user/shop/ShopIndex.desktop.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/Login.desktop.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 41 | -------------------------------------------------------------------------------- /frontend/src/bootstrap.desktop.js: -------------------------------------------------------------------------------- 1 | import { Vue, store, LocaleMixin } from './bootstrap.mixin' 2 | import App from './App.desktop' 3 | import router from './router/router.desktop' 4 | import BootstrapVue from 'bootstrap-vue' 5 | import 'bootstrap/dist/css/bootstrap.css' 6 | import 'bootstrap-vue/dist/bootstrap-vue.css' 7 | 8 | Vue.use(BootstrapVue) 9 | 10 | Vue.http.interceptors.push((request, next) => { 11 | if (request.url !== '/api/login/post') { 12 | const xToken = window.localStorage.getItem('x-token') 13 | request.headers.set('X-Token', xToken) 14 | } 15 | 16 | next(response => { 17 | if ((response.status === 404) || (response.status === 504)) { 18 | router.push({name: 'Home'}) 19 | } 20 | 21 | if (response.status === 403) router.push({name: 'Login'}) 22 | }) 23 | }) 24 | 25 | router.beforeEach((to, from, next) => { 26 | if (to.matched.some(record => record.meta.userOnly)) { 27 | const getCookie = name => { 28 | const cookies = `; ${document.cookie}`.match(`;\\s*${name}=([^;]+)`) 29 | return cookies ? cookies[1] : '' 30 | } 31 | 32 | if (getCookie('user')) { 33 | next() 34 | } else router.push({name: 'Login'}) 35 | } else next() 36 | }) 37 | 38 | /* eslint-disable no-new */ 39 | new Vue({ 40 | el: '#app', 41 | store, 42 | router, 43 | template: '', 44 | mixins: [LocaleMixin], 45 | components: { 46 | App 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /backend/config/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in Log Configuration 3 | * (sails.config.log) 4 | * 5 | * Configure the log level for your app, as well as the transport 6 | * (Underneath the covers, Sails uses Winston for logging, which 7 | * allows for some pretty neat custom transports/adapters for log messages) 8 | * 9 | * For more information on the Sails logger, check out: 10 | * http://sailsjs.org/#!/documentation/concepts/Logging 11 | */ 12 | 13 | module.exports.log = { 14 | 15 | /*************************************************************************** 16 | * * 17 | * Valid `level` configs: i.e. the minimum log level to capture with * 18 | * sails.log.*() * 19 | * * 20 | * The order of precedence for log levels from lowest to highest is: * 21 | * silly, verbose, info, debug, warn, error * 22 | * * 23 | * You may also set the level to "silent" to suppress all logs. * 24 | * * 25 | ***************************************************************************/ 26 | 27 | // level: 'info' 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/src/state/product/product.actions.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | getProduct ({}, id) { 5 | return new Promise((resolve, reject) => { 6 | Vue.http 7 | .get('/api/user/products/product/get', {params: {id}}) 8 | .then(({body}) => resolve(body)) 9 | .catch(error => reject(error)) 10 | }) 11 | }, 12 | 13 | deleteProduct ({}, id) { 14 | return new Promise((resolve, reject) => { 15 | Vue.http 16 | .delete('/api/user/products/product/delete', {params: {id}}) 17 | .then(() => resolve()) 18 | .catch(error => reject(error)) 19 | }) 20 | }, 21 | 22 | postProduct (context, {product: {title, description, price}}) { 23 | return new Promise((resolve, reject) => { 24 | Vue.http 25 | .post('/api/user/products/product/post', {title, description, price}) 26 | .then(() => { 27 | context.commit('RESET_PRODUCT') 28 | resolve() 29 | }) 30 | .catch(error => reject(error)) 31 | }) 32 | }, 33 | 34 | patchProduct ({}, {id, title, description, price}) { 35 | return new Promise((resolve, reject) => { 36 | Vue.http 37 | .patch('/api/user/products/product/patch', { 38 | id, 39 | title, 40 | price, 41 | description 42 | }) 43 | .then(() => resolve()) 44 | .catch(error => reject(error)) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/components/Login.mixin.js: -------------------------------------------------------------------------------- 1 | import { mapMutations } from 'vuex' 2 | 3 | export default { 4 | computed: { 5 | user: { 6 | get () { 7 | return this.$store.state.User.user 8 | } 9 | }, 10 | 11 | name: { 12 | get () { 13 | return this.$store.state.User.user.name 14 | }, 15 | 16 | set (name) { 17 | this.$store.commit('SET_USER_NAME', name) 18 | } 19 | }, 20 | 21 | password: { 22 | get () { 23 | return this.$store.state.User.user.password 24 | }, 25 | 26 | set (password) { 27 | this.$store.commit('SET_USER_PASSWORD', password) 28 | } 29 | } 30 | }, 31 | 32 | methods: { 33 | async signIn () { 34 | const {xToken, cookie} = await this.$store.dispatch('signIn', this.user) 35 | 36 | window.localStorage.setItem('x-token', xToken) 37 | this.setCookie('user', cookie, 3600 * 24 * 7) 38 | this.setIsUserAuthenticated(true) 39 | this.$emit('userLoggedIn') 40 | 41 | this.$router.push({name: 'Products'}) 42 | }, 43 | 44 | setCookie (name, value, seconds) { 45 | const expires = seconds ? '; expires=' + new Date(new Date().getTime() + seconds * 1000).toGMTString() : '' 46 | document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' 47 | }, 48 | 49 | ...mapMutations({ 50 | setIsUserAuthenticated: 'SET_IS_USER_AUTHENTICATED' 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/ProductsGet.mobile.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project should people give an introduction into single page-applications. Furthermore, it should be a reference 4 | book for all kinds of features from Sails.js and Vue.js. If you think an important feature is missing, I'm happy about a 5 | contribution. Just make sure you fulfill the following points. 6 | 7 | ## General 8 | 9 | - Use [JavaScript Standard Style](https://standardjs.com/) 10 | - Watch for a clean and readable code style. More [here](https://medium.com/javascript-scene/elements-of-javascript-style-caa8821cb99f) 11 | - Avoid JavaScript before [ES6](http://es6-features.org/) wherever possible 12 | - Run tests with `npm run test` against Vue.js and Sails.js before commiting 13 | - Add appropriate tests when adding new features 14 | - Ideally run `npm run build` if you change Vue.js related things 15 | 16 | For Vue.js frontend tasks, consult the [Vue.js Component Style Guide](https://github.com/pablohpsilva/vuejs-component-style-guide). 17 | 18 | ## Commits 19 | 20 | You must follow the given commit schema, which is similar to the Angular one. A linter should warn and prevent you from 21 | committing if you don't follow the rules. 22 | 23 | ``` 24 | type: message 25 | ``` 26 | 27 | Where `type` is one of the following: 28 | 29 | ``` 30 | 'build', 31 | 'ci', 32 | 'chore', 33 | 'docs', 34 | 'feat', 35 | 'fix', 36 | 'perf', 37 | 'refactor', 38 | 'revert', 39 | 'style', 40 | 'test' 41 | ``` 42 | 43 | And `scope` is either `client` or `server`. -------------------------------------------------------------------------------- /frontend/src/components/User.validation.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty' 2 | import validate from 'validate.js' 3 | 4 | export default { 5 | data: () => ({ 6 | errors: { 7 | name: [], 8 | password: [] 9 | } 10 | }), 11 | 12 | computed: { 13 | isValidName: { 14 | get () { 15 | if (isEmpty(this.user.name)) return null 16 | 17 | const validation = validate.single(this.user.name, { 18 | presence: true, 19 | length: { 20 | minimum: 3, 21 | message: 'Name must be at least three characters long' 22 | }, 23 | 24 | format: { 25 | pattern: '^[A-Z](.*)$', 26 | message: 'Name must start capitalized' 27 | } 28 | }) 29 | 30 | if (validation) { 31 | this.errors.name = validation 32 | 33 | return false 34 | } 35 | 36 | return true 37 | } 38 | }, 39 | 40 | isValidPassword: { 41 | get () { 42 | if (isEmpty(this.user.password)) return null 43 | 44 | const validation = validate.single(this.user.password, { 45 | presence: true, 46 | length: { 47 | minimum: 3, 48 | message: 'Password must be at least three characters long' 49 | } 50 | }) 51 | 52 | if (validation) { 53 | this.errors.password = validation 54 | 55 | return false 56 | } 57 | 58 | return true 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/config/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default model configuration 3 | * (sails.config.models) 4 | * 5 | * Unless you override them, the following properties will be included 6 | * in each of your models. 7 | * 8 | * For more info on Sails models, see: 9 | * http://sailsjs.org/#!/documentation/concepts/ORM 10 | */ 11 | 12 | module.exports.models = { 13 | 14 | /*************************************************************************** 15 | * * 16 | * Your app's default connection. i.e. the name of one of your app's * 17 | * connections (see `config/connections.js`) * 18 | * * 19 | ***************************************************************************/ 20 | // connection: 'localDiskDb', 21 | 22 | /*************************************************************************** 23 | * * 24 | * How and whether Sails will attempt to automatically rebuild the * 25 | * tables/collections/etc. in your schema. * 26 | * * 27 | * See http://sailsjs.org/#!/documentation/concepts/ORM/model-settings.html * 28 | * * 29 | ***************************************************************************/ 30 | migrate: 'safe' 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/ProductsGet.desktop.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 50 | -------------------------------------------------------------------------------- /frontend/src/components/user/shop/BasketIndex.desktop.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/user/shop/ShopIndex.mobile.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | -------------------------------------------------------------------------------- /backend/config/env/production.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Production environment settings 3 | * 4 | * This file can include shared settings for a production environment, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | sockets: { 16 | onlyAllowOrigins: ['https://vue-sails-example.herokuapp.com'], 17 | } 18 | 19 | /*************************************************************************** 20 | * Set the default database connection for models in the production * 21 | * environment (see config/connections.js and config/models.js ) * 22 | ***************************************************************************/ 23 | 24 | // models: { 25 | // connection: 'someMysqlServer' 26 | // }, 27 | 28 | /*************************************************************************** 29 | * Set the port in the production environment to 80 * 30 | ***************************************************************************/ 31 | 32 | // port: 80, 33 | 34 | /*************************************************************************** 35 | * Set the log level in production environment to "silent" * 36 | ***************************************************************************/ 37 | 38 | // log: { 39 | // level: "silent" 40 | // } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/src/state/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import user from './user/user.module' 4 | import product from './product/product.module' 5 | import products from './products/products.module' 6 | import basket from './basket/basket.module' 7 | 8 | Vue.use(Vuex) 9 | 10 | const getCookie = name => { 11 | const cookies = `; ${document.cookie}`.match(`;\\s*${name}=([^;]+)`) 12 | return cookies ? cookies[1] : '' 13 | } 14 | 15 | export default new Vuex.Store({ 16 | plugins: [store => { 17 | store.subscribe((mutation, state) => { 18 | switch (mutation.type) { 19 | case 'RESET_BASKET': 20 | case 'PUSH_TO_BASKET': 21 | case 'REMOVE_PRODUCT_FROM_BASKET': 22 | sessionStorage.setItem('basket', JSON.stringify(state.Basket.basket)) 23 | } 24 | }) 25 | }], 26 | 27 | modules: { 28 | User: user, 29 | Product: product, 30 | Products: products, 31 | Basket: basket 32 | }, 33 | 34 | state: { 35 | locale: 'en', 36 | isUserAuthenticated: !!(getCookie('user')), 37 | isVisibleHelp: false, 38 | isVisibleProductPatch: false 39 | }, 40 | 41 | mutations: { 42 | SET_IS_USER_AUTHENTICATED (state, isUserAuthenticated) { 43 | state.isUserAuthenticated = isUserAuthenticated 44 | }, 45 | 46 | SET_LOCALE (state, locale) { 47 | state.locale = locale 48 | }, 49 | 50 | SET_IS_VISIBLE_HELP (state, isVisibleHelp) { 51 | state.isVisibleHelp = isVisibleHelp 52 | }, 53 | 54 | SET_IS_VISIBLE_PRODUCT_PATCH (state, isVisibleProductPatch) { 55 | state.isVisibleProductPatch = isVisibleProductPatch 56 | } 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /backend/test/functional/specs/Product.spec.js: -------------------------------------------------------------------------------- 1 | const sails = require('sails') 2 | const request = require('supertest') 3 | const chai = require('chai') 4 | const sinon = require('sinon') 5 | const faker = require('faker') 6 | 7 | describe('Product', function() { 8 | it('should return a non empty JSON with the product', function(done) { 9 | sails.hooks.policies.middleware.isauthorized = sinon.stub() 10 | sails.services.cryptographyservice.decrypt = sinon.stub().returns(1) 11 | 12 | request(sails.hooks.http.app) 13 | .get('/api/user/products/product/get') 14 | .query({ 15 | id: 1 16 | }) 17 | .expect(200) 18 | .end((error, response) => { 19 | if (error) return done(error) 20 | chai.assert.isNotEmpty(response.body) 21 | done() 22 | }) 23 | }) 24 | 25 | it('should post data', function(done) { 26 | sails.hooks.policies.middleware.isauthorized = sinon.stub() 27 | sails.services.cryptographyservice.decrypt = sinon.stub().returns(1) 28 | 29 | const title = faker.commerce.productName() 30 | const description = faker.commerce.productAdjective() 31 | const price = faker.commerce.price() 32 | 33 | request(sails.hooks.http.app) 34 | .post('/api/user/products/product/post') 35 | .send({ 36 | title, 37 | description, 38 | price 39 | }) 40 | .expect(200) 41 | .then(() => { 42 | 43 | Product 44 | .findOne({ 45 | title 46 | }) 47 | .then(product => { 48 | chai.assert.strictEqual(product.title, title) 49 | done() 50 | }) 51 | .catch(error => done(error)) 52 | }) 53 | }) 54 | 55 | }) 56 | -------------------------------------------------------------------------------- /frontend/src/router/router.mobile.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | mode: 'history', 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'Home', 12 | component: resolve => { 13 | require(['./../components/Home.mobile.vue'], resolve) 14 | } 15 | }, 16 | { 17 | path: '/register', 18 | name: 'Register', 19 | component: resolve => { 20 | require(['./../components/Register.mobile.vue'], resolve) 21 | } 22 | }, 23 | { 24 | path: '/login', 25 | name: 'Login', 26 | component: resolve => { 27 | require(['./../components/Login.mobile.vue'], resolve) 28 | } 29 | }, 30 | { 31 | path: '/user/products/index', 32 | name: 'Products', 33 | component: resolve => { 34 | require(['../components/user/products/ProductsIndex.mobile.vue'], resolve) 35 | }, 36 | meta: { 37 | userOnly: true 38 | } 39 | }, 40 | { 41 | path: '/user/shop/index', 42 | name: 'Shop', 43 | component: resolve => { 44 | require(['../components/user/shop/ShopIndex.mobile.vue'], resolve) 45 | }, 46 | meta: { 47 | userOnly: true 48 | } 49 | }, 50 | { 51 | path: '/user/shop/basket/index', 52 | name: 'Basket', 53 | component: resolve => { 54 | require(['../components/user/shop/BasketIndex.mobile.vue'], resolve) 55 | }, 56 | meta: { 57 | userOnly: true 58 | } 59 | }, 60 | { 61 | path: '*', 62 | component: resolve => { 63 | require(['./../components/Home.mobile.vue'], resolve) 64 | } 65 | } 66 | ] 67 | }) 68 | -------------------------------------------------------------------------------- /frontend/src/router/router.desktop.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | mode: 'history', 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'Home', 12 | component: resolve => { 13 | require(['./../components/Home.desktop.vue'], resolve) 14 | } 15 | }, 16 | { 17 | path: '/register', 18 | name: 'Register', 19 | component: resolve => { 20 | require(['./../components/Register.desktop.vue'], resolve) 21 | } 22 | }, 23 | { 24 | path: '/login', 25 | name: 'Login', 26 | component: resolve => { 27 | require(['./../components/Login.desktop.vue'], resolve) 28 | } 29 | }, 30 | { 31 | path: '/user/products/index', 32 | name: 'Products', 33 | component: resolve => { 34 | require(['../components/user/products/ProductsIndex.desktop.vue'], resolve) 35 | }, 36 | meta: { 37 | userOnly: true 38 | } 39 | }, 40 | { 41 | path: '/user/shop/index', 42 | name: 'Shop', 43 | component: resolve => { 44 | require(['../components/user/shop/ShopIndex.desktop.vue'], resolve) 45 | }, 46 | meta: { 47 | userOnly: true 48 | } 49 | }, 50 | { 51 | path: '/user/shop/basket/index', 52 | name: 'Basket', 53 | component: resolve => { 54 | require(['../components/user/shop/BasketIndex.desktop.vue'], resolve) 55 | }, 56 | meta: { 57 | userOnly: true 58 | } 59 | }, 60 | { 61 | path: '*', 62 | component: resolve => { 63 | require(['./../components/Home.desktop.vue'], resolve) 64 | } 65 | } 66 | ] 67 | }) 68 | -------------------------------------------------------------------------------- /frontend/src/components/Home.mobile.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 55 | 56 | 67 | -------------------------------------------------------------------------------- /backend/api/controllers/user/products/product/ProductController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | postProduct: async (req, res) => { 3 | const {title, price, description} = req.allParams() 4 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 5 | 6 | const product = await Product 7 | .create({title, description, price, user: userIdentifier}) 8 | .catch(error => res.serverError(error)) 9 | 10 | sails.log.info('Product created', product) 11 | 12 | return res.ok() 13 | }, 14 | 15 | getProduct: async (req, res) => { 16 | const id = req.param('id') 17 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 18 | 19 | const product = await Product 20 | .findOne({id, user: userIdentifier}) 21 | .catch(error => res.serverError(error)) 22 | 23 | return res.json(product) 24 | }, 25 | 26 | patchProduct: async (req, res) => { 27 | const {id, title, price, description} = req.allParams() 28 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 29 | 30 | const product = await Product 31 | .update({ 32 | id, 33 | user: userIdentifier 34 | }, { 35 | title, 36 | description, 37 | price 38 | }) 39 | .catch(error => res.serverError(error)) 40 | 41 | sails.log.info('Product patched', product) 42 | 43 | return res.ok() 44 | }, 45 | 46 | deleteProduct: async (req, res) => { 47 | const id = req.param('id') 48 | const userIdentifier = CryptographyService.decrypt(req.cookies.user) 49 | 50 | await Product 51 | .destroy({id, user: userIdentifier}) 52 | .catch(error => res.serverError(error)) 53 | 54 | sails.log.info('Product removed') 55 | 56 | return res.ok() 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/components/Home.desktop.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 50 | -------------------------------------------------------------------------------- /backend/tasks/config/jst.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `jst` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Precompile HTML templates using Underscore/Lodash notation into 7 | * functions, creating a `.jst` file. This can be brought into your HTML 8 | * via a 67 | -------------------------------------------------------------------------------- /backend/api/responses/ok.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 200 (OK) Response 3 | * 4 | * Usage: 5 | * return res.ok(); 6 | * return res.ok(data); 7 | * return res.ok(data, 'auth/login'); 8 | * 9 | * @param {Object} data 10 | * @param {String|Object} options 11 | * - pass string to render specified view 12 | */ 13 | 14 | module.exports = function sendOK (data, options) { 15 | 16 | // Get access to `req`, `res`, & `sails` 17 | var req = this.req; 18 | var res = this.res; 19 | var sails = req._sails; 20 | 21 | sails.log.silly('res.ok() :: Sending 200 ("OK") response'); 22 | 23 | // Set status code 24 | res.status(200); 25 | 26 | // If appropriate, serve data as JSON(P) 27 | // If views are disabled, revert to json 28 | if (req.wantsJSON || sails.config.hooks.views === false) { 29 | return res.json(data); 30 | } 31 | 32 | // If second argument is a string, we take that to mean it refers to a view. 33 | // If it was omitted, use an empty object (`{}`) 34 | options = (typeof options === 'string') ? { view: options } : options || {}; 35 | 36 | // Attempt to prettify data for views, if it's a non-error object 37 | var viewData = data; 38 | if (!(viewData instanceof Error) && 'object' == typeof viewData) { 39 | try { 40 | viewData = require('util').inspect(data, {depth: null}); 41 | } 42 | catch(e) { 43 | viewData = undefined; 44 | } 45 | } 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: viewData, title: 'OK' }); 52 | } 53 | 54 | // If no second argument provided, try to serve the implied view, 55 | // but fall back to sending JSON(P) if no view can be inferred. 56 | else return res.guessView({ data: viewData, title: 'OK' }, function couldNotGuessView () { 57 | return res.json(data); 58 | }); 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "description": "Product paradise", 5 | "author": "Julian Claus ", 6 | "license": "MIT", 7 | "private": false, 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "cypress:open": "npm run dev & cypress open --global", 11 | "cypress:run": "cypress run --headed", 12 | "build": "vue-cli-service build", 13 | "lint": "vue-cli-service lint", 14 | "test": "start-server-and-test dev http://localhost:8080 cypress:run" 15 | }, 16 | "dependencies": { 17 | "@vue/cli-plugin-babel": "^3.0.0-rc.3", 18 | "@vue/cli-plugin-e2e-cypress": "^3.0.0-rc.3", 19 | "@vue/cli-plugin-eslint": "^3.0.0-rc.3", 20 | "@vue/cli-plugin-unit-mocha": "^3.0.0-rc.3", 21 | "@vue/cli-service": "^3.0.0-rc.3", 22 | "@vue/eslint-config-standard": "^3.0.0-rc.3", 23 | "@vue/test-utils": "^1.0.0-beta.20", 24 | "bootstrap": "^4.1.1", 25 | "bootstrap-vue": "^2.0.0-rc.11", 26 | "chalk": "^1.1.3", 27 | "cypress": "^3.0.2", 28 | "faker": "^4.1.0", 29 | "he": "^1.2.0", 30 | "localforage": "^1.6.0", 31 | "lodash": "^4.17.10", 32 | "mint-ui": "^2.2.13", 33 | "node-sass": "^4.9.0", 34 | "sails.io.js": "^1.2.1", 35 | "sass-loader": "^7.0.1", 36 | "socket.io-client": "^2.1.1", 37 | "start-server-and-test": "^1.5.0", 38 | "validate.js": "^0.13.1", 39 | "vue": "^2.5.16", 40 | "vue-resource": "^1.5.1", 41 | "vue-router": "^3.0.1", 42 | "vue-sails.io": "^1.0.2", 43 | "vue-socket.io": "^2.1.1-a", 44 | "vue-template-compiler": "^2.5.16", 45 | "vue-translate-plugin": "^1.2.0", 46 | "vuex": "^3.0.1", 47 | "webpack": "^4.16.4", 48 | "workbox-webpack-plugin": "^3.0.1" 49 | }, 50 | "engines": { 51 | "node": ">=10.4.0", 52 | "npm": ">=6.1.0" 53 | }, 54 | "browserslist": [ 55 | "> 1%", 56 | "last 2 versions", 57 | "not ie <= 8" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /backend/api/responses/created.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 201 (CREATED) Response 3 | * 4 | * Usage: 5 | * return res.created(); 6 | * return res.created(data); 7 | * return res.created(data, 'auth/login'); 8 | * 9 | * @param {Object} data 10 | * @param {String|Object} options 11 | * - pass string to render specified view 12 | */ 13 | 14 | module.exports = function created (data, options) { 15 | 16 | // Get access to `req`, `res`, & `sails` 17 | var req = this.req; 18 | var res = this.res; 19 | var sails = req._sails; 20 | 21 | sails.log.silly('res.created() :: Sending 201 ("CREATED") response'); 22 | 23 | // Set status code 24 | res.status(201); 25 | 26 | // If appropriate, serve data as JSON(P) 27 | // If views are disabled, revert to json 28 | if (req.wantsJSON || sails.config.hooks.views === false) { 29 | return res.json(data); 30 | } 31 | 32 | // If second argument is a string, we take that to mean it refers to a view. 33 | // If it was omitted, use an empty object (`{}`) 34 | options = (typeof options === 'string') ? { view: options } : options || {}; 35 | 36 | // Attempt to prettify data for views, if it's a non-error object 37 | var viewData = data; 38 | if (!(viewData instanceof Error) && 'object' == typeof viewData) { 39 | try { 40 | viewData = require('util').inspect(data, {depth: null}); 41 | } 42 | catch(e) { 43 | viewData = undefined; 44 | } 45 | } 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: viewData, title: 'Created' }); 52 | } 53 | 54 | // If no second argument provided, try to serve the implied view, 55 | // but fall back to sending JSON(P) if no view can be inferred. 56 | else return res.guessView({ data: viewData, title: 'Created' }, function couldNotGuessView () { 57 | return res.json(data); 58 | }); 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/product/ProductValidation.mixin.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty' 2 | import validate from 'validate.js' 3 | 4 | export default { 5 | data: () => ({ 6 | errors: { 7 | title: [], 8 | price: [], 9 | description: [] 10 | } 11 | }), 12 | 13 | computed: { 14 | isValidTitle: { 15 | get () { 16 | if (isEmpty(this.title)) return null 17 | 18 | const validation = validate.single(this.title, { 19 | presence: true, 20 | format: { 21 | pattern: '^[A-Z](.*)$', 22 | message: 'Title must start capitalized' 23 | } 24 | }) 25 | 26 | if (validation) { 27 | this.errors.title = validation 28 | 29 | return false 30 | } 31 | 32 | return true 33 | } 34 | }, 35 | 36 | isValidPrice: { 37 | get () { 38 | if (this.price === null) return null 39 | 40 | const validation = validate.single(this.price, { 41 | presence: true, 42 | numericality: { 43 | notValid: 'Price must be number', 44 | greaterThan: 0, 45 | notGreaterThan: 'Price must be greater than zero' 46 | } 47 | }) 48 | 49 | if (validation) { 50 | this.errors.price = validation 51 | 52 | return false 53 | } 54 | 55 | return true 56 | } 57 | }, 58 | 59 | isValidDescription: { 60 | get () { 61 | if (isEmpty(this.description)) return null 62 | 63 | const validation = validate.single(this.description, { 64 | presence: true, 65 | length: { 66 | minimum: 40, 67 | message: 'Description must be at least forty characters long' 68 | } 69 | }) 70 | 71 | if (validation) { 72 | this.errors.description = validation 73 | 74 | return false 75 | } 76 | 77 | return true 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /backend/assets/index.html: -------------------------------------------------------------------------------- 1 | Product paradise
-------------------------------------------------------------------------------- /frontend/cypress/integration/Register.spec.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker' 2 | 3 | describe('Register', () => { 4 | describe('desktop', () => { 5 | beforeEach(() => { 6 | cy.viewport(414, 736) 7 | cy.visit('/register') 8 | }) 9 | 10 | it('loads the app', () => { 11 | cy.get('.mint-header').should('be.visible') 12 | }) 13 | 14 | it('accepts inputs and sends post data', () => { 15 | cy.server() 16 | cy.route({ 17 | method: 'POST', 18 | url: '/api/register/post', 19 | status: 200, 20 | response: {} 21 | }) 22 | .as('post') 23 | 24 | let name = faker.name.firstName() 25 | let password = faker.internet.password() 26 | 27 | cy.get('input').first().type(name) 28 | cy.get('input').last().type(password) 29 | cy.get('.mint-button.mint-button--primary.mint-button--small.is-plain').click() 30 | 31 | cy.wait('@post') 32 | .its('requestBody') 33 | .should('deep.eq', { name, password }) 34 | }) 35 | }) 36 | 37 | describe('mobile', () => { 38 | beforeEach(() => { 39 | cy.viewport(1280, 768) 40 | cy.visit('/register') 41 | }) 42 | 43 | it('loads the app', () => { 44 | cy.get('.container').should('be.visible') 45 | }) 46 | 47 | it('accepts inputs and sends post data', () => { 48 | cy.server() 49 | cy.route({ 50 | method: 'POST', 51 | url: '/api/register/post', 52 | status: 200, 53 | response: {} 54 | }) 55 | .as('post') 56 | 57 | let name = faker.name.firstName() 58 | let password = faker.internet.password() 59 | 60 | cy.get('input').first().type(name) 61 | cy.get('input').last().type(password) 62 | cy.get('.btn.btn-outline-success.btn-sm').click() 63 | 64 | cy.wait('@post') 65 | .its('requestBody') 66 | .should('deep.eq', { name, password }) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/product/ProductPost.desktop.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 60 | -------------------------------------------------------------------------------- /backend/test/functional/specs/Products.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const chai = require('chai') 3 | const sinon = require('sinon') 4 | const faker = require('faker') 5 | 6 | describe('Products', function() { 7 | 8 | it('should return a non empty JSON with products', function(done) { 9 | sails.hooks.policies.middleware.isauthorized = sinon.stub() 10 | sails.services.cryptographyservice.decrypt = sinon.stub().returns(1) 11 | 12 | request(sails.hooks.http.app) 13 | .get('/api/products/get') 14 | .query({ 15 | page: 1 16 | }) 17 | .expect('Content-Type', /json/) 18 | .expect(200) 19 | .end((error, response) => { 20 | if (error) return done(error) 21 | 22 | chai.assert.isNotEmpty(response.body.products) 23 | done() 24 | }) 25 | }) 26 | 27 | it('should find products by given user', function(done) { 28 | sails.hooks.policies.middleware.isauthorized = sinon.stub() 29 | sails.services.cryptographyservice.decrypt = sinon.stub().returns(1) 30 | 31 | let name = 'Joe' 32 | let password = 'toasty' 33 | let product = { 34 | title: 'Cell phone', 35 | description: 'Now with a cool touch display and many colors.', 36 | price: 199.95, 37 | user: 1 38 | } 39 | 40 | request(sails.hooks.http.app) 41 | .post('/api/user/products/product/post') 42 | .set('Cookie', 'user=92f06e135b1749215c2a2bbc6261ffaa') 43 | .send(product) 44 | .expect(200) 45 | .then(() => { 46 | 47 | request(sails.hooks.http.app) 48 | .post('/api/register/post') 49 | .send({ 50 | name, 51 | password 52 | }) 53 | .expect(200) 54 | .then(() => { 55 | 56 | request(sails.hooks.http.app) 57 | .get('/api/user/products/getProductsByUser') 58 | .set('Cookie', 'user=92f06e135b1749215c2a2bbc6261ffaa') 59 | .expect('Content-Type', /json/) 60 | .expect(200) 61 | .end((error, response) => { 62 | if (error) return done(error) 63 | 64 | chai.assert.isNotEmpty(response.body) 65 | done() 66 | }) 67 | }) 68 | }) 69 | }) 70 | 71 | }) 72 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/product/ProductPatch.mobile.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 64 | 65 | 73 | -------------------------------------------------------------------------------- /backend/assets/js/chunk-596f.e000c2e0.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-596f"],{"0c78":function(t,s,e){"use strict";var a=e("c93e"),i=e("2f62"),r=e("92d7"),c=e.n(r);s["a"]={created:function(){this.getProducts(this.currentPage)},data:function(){return{currentPage:1}},watch:{currentPage:function(){this.getProducts(this.currentPage)}},computed:{products:{get:function(){return this.$store.state.Products.products.products}},amountOfProducts:{get:function(){return this.$store.state.Products.products.amountOfProducts}}},methods:Object(a["a"])({encode:function(t){return c.a.encode(t)}},Object(i["b"])(["getProducts"]))}},b61f:function(t,s,e){"use strict";e.r(s);var a=function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",[e("div",{staticClass:"d-flex justify-content-center"},[e("b-carousel",{staticClass:"mb-4",attrs:{background:"transparent",interval:3e3,"img-width":"1100","img-height":"200"}},[e("b-carousel-slide",{attrs:{"img-blank":""}},[e("h3",{staticClass:"text-info"},[t._v(t._s(t.t("home.mixin.h3.first")))]),e("p",{staticClass:"text-muted"},[t._v(t._s(t.t("home.mixin.p.first")))])]),e("b-carousel-slide",{attrs:{"img-blank":""}},[e("h3",{staticClass:"text-info"},[t._v(t._s(t.t("home.mixin.h3.second")))]),e("p",{staticClass:"text-muted"},[t._v(t._s(t.t("home.mixin.p.second")))])]),e("b-carousel-slide",{attrs:{"img-blank":""}},[e("h3",{staticClass:"text-info"},[t._v(t._s(t.t("home.mixin.h3.third")))]),e("p",{staticClass:"text-muted"},[t._v(t._s(t.t("home.mixin.p.third")))])])],1)],1),e("div",{staticClass:"row"},t._l(t.products,function(s){return e("div",{key:s.id,staticClass:"col-4"},[e("b-card",{key:s.id,staticClass:"mb-4",attrs:{header:t.encode(s.title),"footer-tag":"footer"}},[e("p",{staticClass:"card-text"},[t._v(t._s(s.description))]),e("small",{staticClass:"text-muted",attrs:{slot:"footer"},slot:"footer"},[e("span",{staticClass:"float-left"},[t._v("$"+t._s(s.price))]),e("span",{staticClass:"float-right"},[t._v(t._s(t.t("home.mixin.span.first"))+" "+t._s(s.user.name))])])])],1)})),e("b-pagination",{attrs:{size:"sm","total-rows":t.amountOfProducts,"per-page":6},model:{value:t.currentPage,callback:function(s){t.currentPage=s},expression:"currentPage"}})],1)},i=[],r=(e("cadf"),e("551c"),e("097d"),e("0c78")),c={mixins:[r["a"]]},n=c,o=e("2877"),u=Object(o["a"])(n,a,i,!1,null,null,null);u.options.__file="Home.desktop.vue";s["default"]=u.exports}}]); -------------------------------------------------------------------------------- /backend/assets/js/chunk-58f5.62a37769.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-58f5"],{"0469":function(t,e,n){},"0c78":function(t,e,n){"use strict";var i=n("c93e"),s=n("2f62"),c=n("92d7"),o=n.n(c);e["a"]={created:function(){this.getProducts(this.currentPage)},data:function(){return{currentPage:1}},watch:{currentPage:function(){this.getProducts(this.currentPage)}},computed:{products:{get:function(){return this.$store.state.Products.products.products}},amountOfProducts:{get:function(){return this.$store.state.Products.products.amountOfProducts}}},methods:Object(i["a"])({encode:function(t){return o.a.encode(t)}},Object(s["b"])(["getProducts"]))}},"2dba":function(t,e,n){"use strict";n.r(e);var i=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("mt-swipe",{attrs:{"show-indicators":!1}},[n("mt-swipe-item",{staticClass:"slide1"},[t._v("\n "+t._s(t.t("home.mixin.p.first"))+"\n ")]),n("mt-swipe-item",{staticClass:"slide2"},[t._v("\n "+t._s(t.t("home.mixin.p.second"))+"\n ")]),n("mt-swipe-item",{staticClass:"slide3"},[t._v("\n "+t._s(t.t("home.mixin.p.third"))+"\n ")])],1),t._l(t.products,function(e){return n("mt-cell",{key:e.id,attrs:{title:e.title}},[n("mt-button",{attrs:{size:"small"},on:{click:function(n){t.makeProductVisible(e)}}},[t._v(t._s(t.t("home.mixin.button.first")))])],1)}),n("mt-cell",{attrs:{title:""}},[n("mt-button",{attrs:{disabled:t.isDisabledNextButton,size:"small",type:"primary"},on:{click:function(e){t.currentPage++}}},[t._v("\n "+t._s(t.t("home.mixin.button.second"))+"\n ")])],1)],2)},s=[],c=(n("cadf"),n("551c"),n("097d"),n("0c78")),o=n("76a0"),r={mixins:[c["a"]],computed:{isDisabledNextButton:function(){return this.currentPage===Math.ceil(this.amountOfProducts/6)}},methods:{makeProductVisible:function(t){Object(o["MessageBox"])({title:t.title,message:this.encode(t.description),confirmButtonText:"Okay"})}}},u=r,a=(n("d239"),n("2877")),d=Object(a["a"])(u,i,s,!1,null,null,null);d.options.__file="Home.mobile.vue";e["default"]=d.exports},"62e4":function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},d239:function(t,e,n){"use strict";var i=n("0469"),s=n.n(i);s.a}}]); -------------------------------------------------------------------------------- /backend/api/responses/badRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 400 (Bad Request) Handler 3 | * 4 | * Usage: 5 | * return res.badRequest(); 6 | * return res.badRequest(data); 7 | * return res.badRequest(data, 'some/specific/badRequest/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.badRequest( 12 | * 'Please choose a valid `password` (6-12 characters)', 13 | * 'trial/signup' 14 | * ); 15 | * ``` 16 | */ 17 | 18 | module.exports = function badRequest(data, options) { 19 | 20 | // Get access to `req`, `res`, & `sails` 21 | var req = this.req; 22 | var res = this.res; 23 | var sails = req._sails; 24 | 25 | // Set status code 26 | res.status(400); 27 | 28 | // Log error to console 29 | if (data !== undefined) { 30 | sails.log.verbose('Sending 400 ("Bad Request") response: \n',data); 31 | } 32 | else sails.log.verbose('Sending 400 ("Bad Request") response'); 33 | 34 | // Only include errors in response if application environment 35 | // is not set to 'production'. In production, we shouldn't 36 | // send back any identifying information about errors. 37 | if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { 38 | data = undefined; 39 | } 40 | 41 | // If the user-agent wants JSON, always respond with JSON 42 | // If views are disabled, revert to json 43 | if (req.wantsJSON || sails.config.hooks.views === false) { 44 | return res.json(data); 45 | } 46 | 47 | // If second argument is a string, we take that to mean it refers to a view. 48 | // If it was omitted, use an empty object (`{}`) 49 | options = (typeof options === 'string') ? { view: options } : options || {}; 50 | 51 | // Attempt to prettify data for views, if it's a non-error object 52 | var viewData = data; 53 | if (!(viewData instanceof Error) && 'object' == typeof viewData) { 54 | try { 55 | viewData = require('util').inspect(data, {depth: null}); 56 | } 57 | catch(e) { 58 | viewData = undefined; 59 | } 60 | } 61 | 62 | // If a view was provided in options, serve it. 63 | // Otherwise try to guess an appropriate view, or if that doesn't 64 | // work, just send JSON. 65 | if (options.view) { 66 | return res.view(options.view, { data: viewData, title: 'Bad Request' }); 67 | } 68 | 69 | // If no second argument provided, try to serve the implied view, 70 | // but fall back to sending JSON(P) if no view can be inferred. 71 | else return res.guessView({ data: viewData, title: 'Bad Request' }, function couldNotGuessView () { 72 | return res.json(data); 73 | }); 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * Use `app.js` to run your app without `sails lift`. 5 | * To start the server, run: `node app.js`. 6 | * 7 | * This is handy in situations where the sails CLI is not relevant or useful. 8 | * 9 | * For example: 10 | * => `node app.js` 11 | * => `forever start app.js` 12 | * => `node debug app.js` 13 | * => `modulus deploy` 14 | * => `heroku scale` 15 | * 16 | * 17 | * The same command-line arguments are supported, e.g.: 18 | * `node app.js --silent --port=80 --prod` 19 | */ 20 | 21 | var SailsApp = require('sails').Sails 22 | 23 | // Ensure we're in the project directory, so cwd-relative paths work as expected 24 | // no matter where we actually lift from. 25 | // > Note: This is not required in order to lift, but it is a convenient default. 26 | process.chdir(__dirname) 27 | 28 | // Attempt to import `sails`. 29 | var sails = new SailsApp() 30 | try { 31 | sails = require('sails') 32 | } catch (e) { 33 | console.error( 34 | 'To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.') 35 | console.error('To do that, run `npm install sails`') 36 | console.error('') 37 | console.error( 38 | 'Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.') 39 | console.error( 40 | 'When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,') 41 | console.error( 42 | 'but if it doesn\'t, the app will run with the global sails instead!') 43 | return 44 | } 45 | 46 | // --• 47 | // Try to get `rc` dependency (for loading `.sailsrc` files). 48 | var rc 49 | try { 50 | rc = require('rc') 51 | } catch (e0) { 52 | try { 53 | rc = require('sails/node_modules/rc') 54 | } catch (e1) { 55 | console.error('Could not find dependency: `rc`.') 56 | console.error('Your `.sailsrc` file(s) will be ignored.') 57 | console.error('To resolve this, run:') 58 | console.error('npm install rc --save') 59 | rc = function () { return {} } 60 | } 61 | } 62 | 63 | // Start server 64 | sails.lift(rc('sails')) 65 | 66 | setInterval(function () { 67 | sails.lower(function (err) { 68 | const fs = require('fs') 69 | fs.unlinkSync(__dirname + '/.tmp/localDiskDb.db') 70 | 71 | if (err) return console.error('Error occurred lowering Sails app: ', err) 72 | console.info('Sails app lowered successfully!') 73 | sails = new SailsApp() 74 | sails.lift(rc('sails')) 75 | } 76 | ) 77 | }, 86400000) 78 | -------------------------------------------------------------------------------- /backend/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile 3 | * 4 | * This Node script is executed when you run `grunt` or `sails lift`. 5 | * It's purpose is to load the Grunt tasks in your project's `tasks` 6 | * folder, and allow you to add and remove tasks as you see fit. 7 | * For more information on how this works, check out the `README.md` 8 | * file that was generated in your `tasks` folder. 9 | * 10 | * WARNING: 11 | * Unless you know what you're doing, you shouldn't change this file. 12 | * Check out the `tasks` directory instead. 13 | */ 14 | 15 | module.exports = function(grunt) { 16 | 17 | 18 | // Load the include-all library in order to require all of our grunt 19 | // configurations and task registrations dynamically. 20 | var includeAll; 21 | try { 22 | includeAll = require('include-all'); 23 | } catch (e0) { 24 | try { 25 | includeAll = require('sails/node_modules/include-all'); 26 | } catch (e1) { 27 | console.error('Could not find `include-all` module.'); 28 | console.error('Skipping grunt tasks...'); 29 | console.error('To fix this, please run:'); 30 | console.error('npm install include-all --save`'); 31 | console.error(); 32 | 33 | grunt.registerTask('default', []); 34 | return; 35 | } 36 | } 37 | 38 | 39 | /** 40 | * Loads Grunt configuration modules from the specified 41 | * relative path. These modules should export a function 42 | * that, when run, should either load/configure or register 43 | * a Grunt task. 44 | */ 45 | function loadTasks(relPath) { 46 | return includeAll({ 47 | dirname: require('path').resolve(__dirname, relPath), 48 | filter: /(.+)\.js$/, 49 | excludeDirs: /^\.(git|svn)$/ 50 | }) || {}; 51 | } 52 | 53 | /** 54 | * Invokes the function from a Grunt configuration module with 55 | * a single argument - the `grunt` object. 56 | */ 57 | function invokeConfigFn(tasks) { 58 | for (var taskName in tasks) { 59 | if (tasks.hasOwnProperty(taskName)) { 60 | tasks[taskName](grunt); 61 | } 62 | } 63 | } 64 | 65 | 66 | 67 | // Load task functions 68 | var taskConfigurations = loadTasks('./tasks/config'), 69 | registerDefinitions = loadTasks('./tasks/register'); 70 | 71 | // (ensure that a default task exists) 72 | if (!registerDefinitions.default) { 73 | registerDefinitions.default = function(grunt) { 74 | grunt.registerTask('default', []); 75 | }; 76 | } 77 | 78 | // Run task functions to configure Grunt. 79 | invokeConfigFn(taskConfigurations); 80 | invokeConfigFn(registerDefinitions); 81 | 82 | }; 83 | -------------------------------------------------------------------------------- /frontend/cypress/integration/ProductsGet.js: -------------------------------------------------------------------------------- 1 | import mocks from './../fixtures/mocks' 2 | 3 | describe('ProductsGet', () => { 4 | describe('mobile', () => { 5 | beforeEach(() => { 6 | cy.server() 7 | cy.viewport(414, 736) 8 | cy.setCookie('user', '1') 9 | 10 | cy.route({ 11 | method: 'GET', 12 | url: '/api/user/products/getProductsByUser', 13 | status: 200, 14 | response: mocks('productsByUser', 12) 15 | }) 16 | .as('/api/user/products/getProductsByUser') 17 | 18 | cy.visit('/user/products/index', { 19 | onBeforeLoad: window => { 20 | window.localStorage.setItem('token', 1) 21 | } 22 | }) 23 | }) 24 | 25 | it('loads the app', () => { 26 | cy.get('.mint-header').should('be.visible') 27 | }) 28 | 29 | it('shows products', () => { 30 | cy.get(':nth-child(1) > :nth-child(1) > :nth-child(1) > .mint-cell-wrapper > .mint-cell-title > .mint-cell-text').should('not.be.empty') 31 | }) 32 | }) 33 | 34 | describe('desktop', () => { 35 | beforeEach(() => { 36 | cy.server() 37 | cy.viewport(1280, 768) 38 | cy.setCookie('user', '1') 39 | 40 | cy.route({ 41 | method: 'GET', 42 | url: '/api/user/products/getProductsByUser', 43 | status: 200, 44 | response: mocks('productsByUser', 12) 45 | }) 46 | .as('/api/user/products/getProductsByUser') 47 | 48 | cy.route({ 49 | method: 'DELETE', 50 | url: '/api/user/products/product/delete?id=0', 51 | status: 200, 52 | response: {} 53 | }) 54 | .as('/api/user/products/product/delete?id=0') 55 | 56 | cy.visit('/user/products/index', { 57 | onBeforeLoad: window => { 58 | window.localStorage.setItem('token', 1) 59 | } 60 | }) 61 | }) 62 | 63 | it('loads the app', () => { 64 | cy.get('.container').should('be.visible') 65 | }) 66 | 67 | it('shows products', () => { 68 | cy.get('tbody > :nth-child(1) > [aria-colindex="1"]').should('not.be.empty') 69 | }) 70 | 71 | it('removes products', () => { 72 | cy.route({ 73 | method: 'GET', 74 | url: '/api/user/products/getProductsByUser', 75 | status: 200, 76 | response: mocks('productsByUser', 11) 77 | }) 78 | .as('/api/user/products/product/delete?id=0') 79 | cy.get(':nth-child(1) > [aria-colindex="4"] > .btn-group > .btn-outline-warning').click() 80 | cy.wait('@/api/user/products/product/delete?id=0') 81 | cy.wait('@/api/user/products/getProductsByUser') 82 | cy.get('table tbody').find('tr').should($tbody => expect($tbody).to.have.length(11)) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /backend/assets/js/chunk-5e44.8377145e.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-5e44"],{"91a7":function(s,e,t){"use strict";var a=t("e478"),i=t.n(a);i.a},e478:function(s,e,t){},e636:function(s,e,t){"use strict";t.r(e);var a=function(){var s=this,e=s.$createElement,t=s._self._c||e;return t("b-modal",{attrs:{"no-fade":"","no-close-on-backdrop":!0,"no-close-on-esc":!0,"hide-header-close":!0,"ok-only":!0,id:"help",title:s.t("help.desktop.modal.title"),size:"lg"}},[t("div",{staticClass:"card mb-2"},[t("div",{staticClass:"card-body"},s._l(s.messages,function(e,a){return t("div",{key:a},[e.message.assistant?t("p",{staticClass:"mb-0"},[t("b",{staticClass:"mr-2"},[s._v(s._s(e.message.assistant.name))]),s._v("\n "+s._s(e.message.assistant.message))]):s._e(),e.message.assistant?t("p",{staticClass:"text-muted"},[t("small",[s._v(s._s(e.time))])]):s._e(),e.message.user?t("p",{staticClass:"text-right mb-0"},[s._v(s._s(e.message.user.message)+" "),t("b",{staticClass:"ml-2"},[s._v("You")])]):s._e(),e.message.user?t("p",{staticClass:"text-right text-muted"},[t("small",[s._v(s._s(e.time))])]):s._e()])}))]),t("b-form-input",{attrs:{type:"text"},nativeOn:{keyup:function(e){return"button"in e||!s._k(e.keyCode,"enter",13,e.key,"Enter")?s.postMessage(e):null}},model:{value:s.message,callback:function(e){s.message=e},expression:"message"}}),t("template",{slot:"modal-footer"},[t("b-button",{attrs:{size:"sm",variant:"secondary"},on:{click:function(e){s.setIsVisibleHelp(!1)}}},[s._v("\n "+s._s(s.t("help.desktop.button.first"))+"\n ")])],1)],2)},i=[],n=t("c93e"),o=(t("6b54"),t("2f62")),l={props:["io"],data:function(){return{assistant:"",message:"",messages:[{message:{assistant:{name:"System",message:"Hey, how can we help you?"}},time:(new Date).toString()}]}},mounted:function(){this.$root.$emit("bv::show::modal","help"),this.$emit("helpMounted")},created:function(){this.setRandomAssistantName()},computed:{isHelpVisible:{get:function(){return this.$store.state.isHelpVisible},set:function(s){this.store.commit("SET_IS_HELP_VISIBLE",s)}}},updated:function(){var s=this.$el.querySelector(".card");s.scrollTop=s.scrollHeight},methods:Object(n["a"])({postMessage:function(){var s=this;this.messages.push({message:{user:{message:this.message}},time:(new Date).toString()}),this.io.socket.post("/api/help",{message:this.message},function(e){s.messages.push({message:{assistant:{name:s.assistant,message:e.answer}},time:(new Date).toString()})}),this.$set(this,"message","")},setRandomAssistantName:function(){var s=["Irvin Case","Juliette Cooper","Sheldon James"],e=s[Math.floor(Math.random()*s.length)];this.$set(this,"assistant",e)}},Object(o["c"])({setIsVisibleHelp:"SET_IS_VISIBLE_HELP"}))},m=l,r=(t("91a7"),t("2877")),c=Object(r["a"])(m,a,i,!1,null,"13b6df7c",null);c.options.__file="Help.desktop.vue";e["default"]=c.exports}}]); -------------------------------------------------------------------------------- /backend/config/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Variable Configuration 3 | * (sails.config.globals) 4 | * 5 | * Configure which global variables which will be exposed 6 | * automatically by Sails. 7 | * 8 | * For more information on configuration, check out: 9 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.globals.html 10 | */ 11 | module.exports.globals = { 12 | 13 | /**************************************************************************** 14 | * * 15 | * Expose the lodash installed in Sails core as a global variable. If this * 16 | * is disabled, like any other node module you can always run npm install * 17 | * lodash --save, then var _ = require('lodash') at the top of any file. * 18 | * * 19 | ****************************************************************************/ 20 | 21 | _: false, 22 | 23 | /**************************************************************************** 24 | * * 25 | * Expose the async installed in Sails core as a global variable. If this is * 26 | * disabled, like any other node module you can always run npm install async * 27 | * --save, then var async = require('async') at the top of any file. * 28 | * * 29 | ****************************************************************************/ 30 | 31 | async: false, 32 | 33 | /**************************************************************************** 34 | * * 35 | * Expose the sails instance representing your app. If this is disabled, you * 36 | * can still get access via req._sails. * 37 | * * 38 | ****************************************************************************/ 39 | 40 | sails: true, 41 | 42 | /**************************************************************************** 43 | * * 44 | * Expose each of your app's models as global variables (using their * 45 | * "globalId"). E.g. a model defined in api/models/User.js would have a * 46 | * globalId of User by default. If this is disabled, you can still access * 47 | * your models via sails.models.*. * 48 | * * 49 | ****************************************************************************/ 50 | 51 | models: true 52 | }; 53 | -------------------------------------------------------------------------------- /frontend/cypress/integration/ShopIndex.spec.js: -------------------------------------------------------------------------------- 1 | import mocks from './../fixtures/mocks' 2 | 3 | describe('ShopIndex', () => { 4 | describe('mobile', () => { 5 | beforeEach(() => { 6 | cy.server() 7 | cy.viewport(414, 736) 8 | cy.setCookie('user', '1') 9 | cy.route({ 10 | method: 'GET', 11 | url: '/api/user/get', 12 | status: 200, 13 | response: { 14 | 'name': 'Joe', 15 | 'createdAt': '2018-01-24T09:50:30.083Z', 16 | 'updatedAt': '2018-01-24T09:50:30.083Z', 17 | 'id': 1 18 | } 19 | }) 20 | .as('/api/user/get') 21 | 22 | cy.route({ 23 | method: 'GET', 24 | url: '/api/user/products/get?page=1', 25 | status: 200, 26 | response: mocks('products', 12, {amountOfProducts: 12}) 27 | }) 28 | .as('/api/user/products/get?page=1') 29 | 30 | cy.visit('/user/shop/index', { 31 | onBeforeLoad: window => { 32 | window.localStorage.setItem('token', 1) 33 | } 34 | }) 35 | }) 36 | 37 | it('loads the app', () => { 38 | cy.get('.mint-header').should('be.visible') 39 | }) 40 | 41 | it('adds a product to basket', () => { 42 | cy.wait('@/api/user/get') 43 | cy.wait('@/api/user/products/get?page=1') 44 | cy.get(':nth-child(1) > .mint-cell-wrapper > .mint-cell-value > .mint-button').click() 45 | cy.get('.mint-msgbox-btn.mint-msgbox-confirm').click() 46 | }) 47 | }) 48 | 49 | describe('desktop', () => { 50 | beforeEach(() => { 51 | cy.server() 52 | cy.viewport(1280, 768) 53 | cy.setCookie('user', '1') 54 | cy.route({ 55 | method: 'GET', 56 | url: '/api/user/get', 57 | status: 200, 58 | response: { 59 | 'name': 'Joe', 60 | 'createdAt': '2018-01-24T09:50:30.083Z', 61 | 'updatedAt': '2018-01-24T09:50:30.083Z', 62 | 'id': 1 63 | } 64 | }) 65 | .as('/api/user/get') 66 | 67 | cy.route({ 68 | method: 'GET', 69 | url: '/api/user/products/get?page=1', 70 | status: 200, 71 | response: mocks('products', 12, {amountOfProducts: 12}) 72 | }) 73 | .as('/api/user/products/get?page=1') 74 | 75 | cy.visit('/user/shop/index', { 76 | onBeforeLoad: window => { 77 | window.localStorage.setItem('token', 1) 78 | } 79 | }) 80 | }) 81 | 82 | it('loads the app', () => { 83 | cy.get('.container').should('be.visible') 84 | }) 85 | 86 | it('adds a product to basket', () => { 87 | cy.wait('@/api/user/get') 88 | cy.wait('@/api/user/products/get?page=1') 89 | cy.get(':nth-child(1) > .card > .card-body > .btn').click() 90 | cy.get('.ml-auto > :nth-child(2) > .nav-link').contains('Basket (1)') 91 | }) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /backend/config/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * (sails.config.i18n) 4 | * 5 | * If your app will touch people from all over the world, i18n (or internationalization) 6 | * may be an important part of your international strategy. 7 | * 8 | * 9 | * For more informationom i18n in Sails, check out: 10 | * http://sailsjs.org/#!/documentation/concepts/Internationalization 11 | * 12 | * For a complete list of i18n options, see: 13 | * https://github.com/mashpie/i18n-node#list-of-configuration-options 14 | * 15 | * 16 | */ 17 | 18 | module.exports.i18n = { 19 | 20 | /*************************************************************************** 21 | * * 22 | * Which locales are supported? * 23 | * * 24 | ***************************************************************************/ 25 | 26 | // locales: ['en', 'es', 'fr', 'de'], 27 | 28 | /**************************************************************************** 29 | * * 30 | * What is the default locale for the site? Note that this setting will be * 31 | * overridden for any request that sends an "Accept-Language" header (i.e. * 32 | * most browsers), but it's still useful if you need to localize the * 33 | * response for requests made by non-browser clients (e.g. cURL). * 34 | * * 35 | ****************************************************************************/ 36 | 37 | // defaultLocale: 'en', 38 | 39 | /**************************************************************************** 40 | * * 41 | * Automatically add new keys to locale (translation) files when they are * 42 | * encountered during a request? * 43 | * * 44 | ****************************************************************************/ 45 | 46 | // updateFiles: false, 47 | 48 | /**************************************************************************** 49 | * * 50 | * Path (relative to app root) of directory to store locale (translation) * 51 | * files in. * 52 | * * 53 | ****************************************************************************/ 54 | 55 | // localesDirectory: '/config/locales' 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /backend/config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy Mappings 3 | * (sails.config.policies) 4 | * 5 | * Policies are simple functions which run **before** your controllers. 6 | * You can apply one or more policies to a given controller, or protect 7 | * its actions individually. 8 | * 9 | * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed 10 | * below by its filename, minus the extension, (e.g. "authenticated") 11 | * 12 | * For more information on how policies work, see: 13 | * http://sailsjs.org/#!/documentation/concepts/Policies 14 | * 15 | * For more information on configuring policies, check out: 16 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.policies.html 17 | */ 18 | 19 | 20 | module.exports.policies = { 21 | 22 | /*************************************************************************** 23 | * * 24 | * Default policy for all controllers and actions (`true` allows public * 25 | * access) * 26 | * * 27 | ***************************************************************************/ 28 | 29 | // '*': true, 30 | 31 | /*************************************************************************** 32 | * * 33 | * Here's an example of mapping some policies to run before a controller * 34 | * and its actions * 35 | * * 36 | ***************************************************************************/ 37 | // RabbitController: { 38 | 39 | // Apply the `false` policy as the default for all of RabbitController's actions 40 | // (`false` prevents all access, which ensures that nothing bad happens to our rabbits) 41 | // '*': false, 42 | 43 | // For the action `nurture`, apply the 'isRabbitMother' policy 44 | // (this overrides `false` above) 45 | // nurture : 'isRabbitMother', 46 | 47 | // Apply the `isNiceToAnimals` AND `hasRabbitFood` policies 48 | // before letting any users feed our rabbits 49 | // feed : ['isNiceToAnimals', 'hasRabbitFood'] 50 | // } 51 | 52 | 'RegisterController': { 53 | '*': true 54 | }, 55 | 56 | 'LoginController': { 57 | '*': true 58 | }, 59 | 60 | 'ProductController': { 61 | '*': 'isAuthorized' 62 | }, 63 | 64 | 'user/ProductsController': { 65 | '*': 'isAuthorized' 66 | }, 67 | 68 | 'user/ProductController': { 69 | '*': 'isAuthorized' 70 | }, 71 | 72 | 'ProductsController': { 73 | '*': true 74 | }, 75 | 76 | 'user/shop/BasketController': { 77 | '*': 'isAuthorized' 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /backend/api/responses/forbidden.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 403 (Forbidden) Handler 3 | * 4 | * Usage: 5 | * return res.forbidden(); 6 | * return res.forbidden(err); 7 | * return res.forbidden(err, 'some/specific/forbidden/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.forbidden('Access denied.'); 12 | * ``` 13 | */ 14 | 15 | module.exports = function forbidden (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(403); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.verbose('Sending 403 ("Forbidden") response: \n',data); 28 | } 29 | else sails.log.verbose('Sending 403 ("Forbidden") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | // If views are disabled, revert to json 40 | if (req.wantsJSON || sails.config.hooks.views === false) { 41 | return res.json(data); 42 | } 43 | 44 | // If second argument is a string, we take that to mean it refers to a view. 45 | // If it was omitted, use an empty object (`{}`) 46 | options = (typeof options === 'string') ? { view: options } : options || {}; 47 | 48 | // Attempt to prettify data for views, if it's a non-error object 49 | var viewData = data; 50 | if (!(viewData instanceof Error) && 'object' == typeof viewData) { 51 | try { 52 | viewData = require('util').inspect(data, {depth: null}); 53 | } 54 | catch(e) { 55 | viewData = undefined; 56 | } 57 | } 58 | 59 | // If a view was provided in options, serve it. 60 | // Otherwise try to guess an appropriate view, or if that doesn't 61 | // work, just send JSON. 62 | if (options.view) { 63 | return res.view(options.view, { data: viewData, title: 'Forbidden' }); 64 | } 65 | 66 | // If no second argument provided, try to serve the default view, 67 | // but fall back to sending JSON(P) if any errors occur. 68 | else return res.view('403', { data: viewData, title: 'Forbidden' }, function (err, html) { 69 | 70 | // If a view error occured, fall back to JSON(P). 71 | if (err) { 72 | // 73 | // Additionally: 74 | // • If the view was missing, ignore the error but provide a verbose log. 75 | if (err.code === 'E_VIEW_FAILED') { 76 | sails.log.verbose('res.forbidden() :: Could not locate view for error page (sending JSON instead). Details: ',err); 77 | } 78 | // Otherwise, if this was a more serious error, log to the console with the details. 79 | else { 80 | sails.log.warn('res.forbidden() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 81 | } 82 | return res.json(data); 83 | } 84 | 85 | return res.send(html); 86 | }); 87 | 88 | }; 89 | -------------------------------------------------------------------------------- /backend/config/datastores.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Datastores 3 | * (sails.config.datastores) 4 | * 5 | * A set of datastore configurations which tell Sails where to fetch or save 6 | * data when you execute built-in model methods like `.find()` and `.create()`. 7 | * 8 | * > This file is mainly useful for configuring your development database, 9 | * > as well as any additional one-off databases used by individual models. 10 | * > Ready to go live? Head towards `config/env/production.js`. 11 | * 12 | * For more information on configuring datastores, check out: 13 | * https://sailsjs.com/config/datastores 14 | */ 15 | 16 | module.exports.datastores = { 17 | 18 | 19 | /*************************************************************************** 20 | * * 21 | * Your app's default datastore. * 22 | * * 23 | * Sails apps read and write to local disk by default, using a built-in * 24 | * database adapter called `sails-disk`. This feature is purely for * 25 | * convenience during development; since `sails-disk` is not designed for * 26 | * use in a production environment. * 27 | * * 28 | * To use a different db _in development_, follow the directions below. * 29 | * Otherwise, just leave the default datastore as-is, with no `adapter`. * 30 | * * 31 | * (For production configuration, see `config/env/production.js`.) * 32 | * * 33 | ***************************************************************************/ 34 | 35 | default: { 36 | 37 | /*************************************************************************** 38 | * * 39 | * Want to use a different database during development? * 40 | * * 41 | * 1. Choose an adapter: * 42 | * https://sailsjs.com/plugins/databases * 43 | * * 44 | * 2. Install it as a dependency of your Sails app. * 45 | * (For example: npm install sails-mysql --save) * 46 | * * 47 | * 3. Then pass it in, along with a connection URL. * 48 | * (See https://sailsjs.com/config/datastores for help.) * 49 | * * 50 | ***************************************************************************/ 51 | // adapter: 'sails-mysql', 52 | // url: 'mysql://user:password@host:port/database', 53 | 54 | }, 55 | 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /backend/config/csrf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cross-Site Request Forgery Protection Settings 3 | * (sails.config.csrf) 4 | * 5 | * CSRF tokens are like a tracking chip. While a session tells the server that a user 6 | * "is who they say they are", a csrf token tells the server "you are where you say you are". 7 | * 8 | * When enabled, all non-GET requests to the Sails server must be accompanied by 9 | * a special token, identified as the '_csrf' parameter. 10 | * 11 | * This option protects your Sails app against cross-site request forgery (or CSRF) attacks. 12 | * A would-be attacker needs not only a user's session cookie, but also this timestamped, 13 | * secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. 14 | * 15 | * This allows us to have certainty that our users' requests haven't been hijacked, 16 | * and that the requests they're making are intentional and legitimate. 17 | * 18 | * This token has a short-lived expiration timeline, and must be acquired by either: 19 | * 20 | * (a) For traditional view-driven web apps: 21 | * Fetching it from one of your views, where it may be accessed as 22 | * a local variable, e.g.: 23 | *
24 | * 25 | *
26 | * 27 | * or (b) For AJAX/Socket-heavy and/or single-page apps: 28 | * Sending a GET request to the `/csrfToken` route, where it will be returned 29 | * as JSON, e.g.: 30 | * { _csrf: 'ajg4JD(JGdajhLJALHDa' } 31 | * 32 | * 33 | * Enabling this option requires managing the token in your front-end app. 34 | * For traditional web apps, it's as easy as passing the data from a view into a form action. 35 | * In AJAX/Socket-heavy apps, just send a GET request to the /csrfToken route to get a valid token. 36 | * 37 | * For more information on CSRF, check out: 38 | * http://en.wikipedia.org/wiki/Cross-site_request_forgery 39 | * 40 | * For more information on this configuration file, including info on CSRF + CORS, see: 41 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.csrf.html 42 | * 43 | */ 44 | 45 | /**************************************************************************** 46 | * * 47 | * Enabled CSRF protection for your site? * 48 | * * 49 | ****************************************************************************/ 50 | 51 | // module.exports.csrf = false; 52 | 53 | /**************************************************************************** 54 | * * 55 | * You may also specify more fine-grained settings for CSRF, including the * 56 | * domains which are allowed to request the CSRF token via AJAX. These * 57 | * settings override the general CORS settings in your config/cors.js file. * 58 | * * 59 | ****************************************************************************/ 60 | 61 | // module.exports.csrf = { 62 | // grantTokenViaAjax: true, 63 | // origin: '' 64 | // } 65 | -------------------------------------------------------------------------------- /backend/api/responses/serverError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 500 (Server Error) Response 3 | * 4 | * Usage: 5 | * return res.serverError(); 6 | * return res.serverError(err); 7 | * return res.serverError(err, 'some/specific/error/view'); 8 | * 9 | * NOTE: 10 | * If something throws in a policy or controller, or an internal 11 | * error is encountered, Sails will call `res.serverError()` 12 | * automatically. 13 | */ 14 | 15 | module.exports = function serverError (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(500); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.error('Sending 500 ("Server Error") response: \n',data); 28 | } 29 | else sails.log.error('Sending empty 500 ("Server Error") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | // If views are disabled, revert to json 40 | if (req.wantsJSON || sails.config.hooks.views === false) { 41 | return res.json(data); 42 | } 43 | 44 | // If second argument is a string, we take that to mean it refers to a view. 45 | // If it was omitted, use an empty object (`{}`) 46 | options = (typeof options === 'string') ? { view: options } : options || {}; 47 | 48 | // Attempt to prettify data for views, if it's a non-error object 49 | var viewData = data; 50 | if (!(viewData instanceof Error) && 'object' == typeof viewData) { 51 | try { 52 | viewData = require('util').inspect(data, {depth: null}); 53 | } 54 | catch(e) { 55 | viewData = undefined; 56 | } 57 | } 58 | 59 | // If a view was provided in options, serve it. 60 | // Otherwise try to guess an appropriate view, or if that doesn't 61 | // work, just send JSON. 62 | if (options.view) { 63 | return res.view(options.view, { data: viewData, title: 'Server Error' }); 64 | } 65 | 66 | // If no second argument provided, try to serve the default view, 67 | // but fall back to sending JSON(P) if any errors occur. 68 | else return res.view('500', { data: viewData, title: 'Server Error' }, function (err, html) { 69 | 70 | // If a view error occured, fall back to JSON(P). 71 | if (err) { 72 | // 73 | // Additionally: 74 | // • If the view was missing, ignore the error but provide a verbose log. 75 | if (err.code === 'E_VIEW_FAILED') { 76 | sails.log.verbose('res.serverError() :: Could not locate view for error page (sending JSON instead). Details: ',err); 77 | } 78 | // Otherwise, if this was a more serious error, log to the console with the details. 79 | else { 80 | sails.log.warn('res.serverError() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 81 | } 82 | return res.json(data); 83 | } 84 | 85 | return res.send(html); 86 | }); 87 | 88 | }; 89 | -------------------------------------------------------------------------------- /backend/tasks/pipeline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt/pipeline.js 3 | * 4 | * The order in which your css, javascript, and template files should be 5 | * compiled and linked from your views and static HTML files. 6 | * 7 | * (Note that you can take advantage of Grunt-style wildcard/glob/splat expressions 8 | * for matching multiple files, and ! in front of an expression to ignore files.) 9 | * 10 | * For more information see: 11 | * https://github.com/balderdashy/sails-docs/blob/master/anatomy/myApp/tasks/pipeline.js.md 12 | */ 13 | 14 | 15 | // CSS files to inject in order 16 | // 17 | // (if you're using LESS with the built-in default config, you'll want 18 | // to change `assets/styles/importer.less` instead.) 19 | var cssFilesToInject = [ 20 | 'styles/**/*.css' 21 | ]; 22 | 23 | 24 | // Client-side javascript files to inject in order 25 | // (uses Grunt-style wildcard/glob/splat expressions) 26 | var jsFilesToInject = [ 27 | 28 | // Load sails.io before everything else 29 | 'js/dependencies/sails.io.js', 30 | 31 | // Dependencies like jQuery, or Angular are brought in here 32 | 'js/dependencies/**/*.js', 33 | 34 | // All of the rest of your client-side js files 35 | // will be injected here in no particular order. 36 | 'js/**/*.js' 37 | ]; 38 | 39 | 40 | // Client-side HTML templates are injected using the sources below 41 | // The ordering of these templates shouldn't matter. 42 | // (uses Grunt-style wildcard/glob/splat expressions) 43 | // 44 | // By default, Sails uses JST templates and precompiles them into 45 | // functions for you. If you want to use jade, handlebars, dust, etc., 46 | // with the linker, no problem-- you'll just want to make sure the precompiled 47 | // templates get spit out to the same file. Be sure and check out `tasks/README.md` 48 | // for information on customizing and installing new tasks. 49 | var templateFilesToInject = [ 50 | 'templates/**/*.html' 51 | ]; 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | // Default path for public folder (see documentation for more information) 60 | var tmpPath = '.tmp/public/'; 61 | 62 | // Prefix relative paths to source files so they point to the proper locations 63 | // (i.e. where the other Grunt tasks spit them out, or in some cases, where 64 | // they reside in the first place) 65 | module.exports.cssFilesToInject = cssFilesToInject.map(function(cssPath) { 66 | // If we're ignoring the file, make sure the ! is at the beginning of the path 67 | if (cssPath[0] === '!') { 68 | return require('path').join('!.tmp/public/', cssPath.substr(1)); 69 | } 70 | return require('path').join('.tmp/public/', cssPath); 71 | }); 72 | module.exports.jsFilesToInject = jsFilesToInject.map(function(jsPath) { 73 | // If we're ignoring the file, make sure the ! is at the beginning of the path 74 | if (jsPath[0] === '!') { 75 | return require('path').join('!.tmp/public/', jsPath.substr(1)); 76 | } 77 | return require('path').join('.tmp/public/', jsPath); 78 | }); 79 | module.exports.templateFilesToInject = templateFilesToInject.map(function(tplPath) { 80 | // If we're ignoring the file, make sure the ! is at the beginning of the path 81 | if (tplPath[0] === '!') { 82 | return require('path').join('!assets/', tplPath.substr(1)); 83 | } 84 | return require('path').join('assets/',tplPath); 85 | }); 86 | 87 | 88 | -------------------------------------------------------------------------------- /backend/tasks/README.md: -------------------------------------------------------------------------------- 1 | # About the `tasks` folder 2 | 3 | The `tasks` directory is a suite of Grunt tasks and their configurations, bundled for your convenience. The Grunt integration is mainly useful for bundling front-end assets, (like stylesheets, scripts, & markup templates) but it can also be used to run all kinds of development tasks, from browserify compilation to database migrations. 4 | 5 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, read on! 6 | 7 | 8 | ### How does this work? 9 | 10 | The asset pipeline bundled in Sails is a set of Grunt tasks configured with conventional defaults designed to make your project more consistent and productive. 11 | 12 | The entire front-end asset workflow in Sails is completely customizable-- while it provides some suggestions out of the box, Sails makes no pretense that it can anticipate all of the needs you'll encounter building the browser-based/front-end portion of your application. Who's to say you're even building an app for a browser? 13 | 14 | 15 | 16 | ### What tasks does Sails run automatically? 17 | 18 | Sails runs some of these tasks (the ones in the `tasks/register` folder) automatically when you run certain commands. 19 | 20 | ###### `sails lift` 21 | 22 | Runs the `default` task (`tasks/register/default.js`). 23 | 24 | ###### `sails lift --prod` 25 | 26 | Runs the `prod` task (`tasks/register/prod.js`). 27 | 28 | ###### `sails www` 29 | 30 | Runs the `build` task (`tasks/register/build.js`). 31 | 32 | ###### `sails www --prod` (production) 33 | 34 | Runs the `buildProd` task (`tasks/register/buildProd.js`). 35 | 36 | 37 | ### Can I customize this for SASS, Angular, client-side Jade templates, etc? 38 | 39 | You can modify, omit, or replace any of these Grunt tasks to fit your requirements. You can also add your own Grunt tasks- just add a `someTask.js` file in the `grunt/config` directory to configure the new task, then register it with the appropriate parent task(s) (see files in `grunt/register/*.js`). 40 | 41 | 42 | ### Do I have to use Grunt? 43 | 44 | Nope! To disable Grunt integration in Sails, just delete your Gruntfile or disable the Grunt hook. 45 | 46 | 47 | ### What if I'm not building a web frontend? 48 | 49 | That's ok! A core tenant of Sails is client-agnosticism-- it's especially designed for building APIs used by all sorts of clients; native Android/iOS/Cordova, serverside SDKs, etc. 50 | 51 | You can completely disable Grunt by following the instructions above. 52 | 53 | If you still want to use Grunt for other purposes, but don't want any of the default web front-end stuff, just delete your project's `assets` folder and remove the front-end oriented tasks from the `grunt/register` and `grunt/config` folders. You can also run `sails new myCoolApi --no-frontend` to omit the `assets` folder and front-end-oriented Grunt tasks for future projects. You can also replace your `sails-generate-frontend` module with alternative community generators, or create your own. This allows `sails new` to create the boilerplate for native iOS apps, Android apps, Cordova apps, SteroidsJS apps, etc. 54 | 55 | -------------------------------------------------------------------------------- /backend/api/responses/notFound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 404 (Not Found) Handler 3 | * 4 | * Usage: 5 | * return res.notFound(); 6 | * return res.notFound(err); 7 | * return res.notFound(err, 'some/specific/notfound/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.notFound(); 12 | * ``` 13 | * 14 | * NOTE: 15 | * If a request doesn't match any explicit routes (i.e. `config/routes.js`) 16 | * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` 17 | * automatically. 18 | */ 19 | 20 | module.exports = function notFound (data, options) { 21 | 22 | // Get access to `req`, `res`, & `sails` 23 | var req = this.req; 24 | var res = this.res; 25 | var sails = req._sails; 26 | 27 | // Set status code 28 | res.status(404); 29 | 30 | // Log error to console 31 | if (data !== undefined) { 32 | sails.log.verbose('Sending 404 ("Not Found") response: \n',data); 33 | } 34 | else sails.log.verbose('Sending 404 ("Not Found") response'); 35 | 36 | // Only include errors in response if application environment 37 | // is not set to 'production'. In production, we shouldn't 38 | // send back any identifying information about errors. 39 | if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { 40 | data = undefined; 41 | } 42 | 43 | // If the user-agent wants JSON, always respond with JSON 44 | // If views are disabled, revert to json 45 | if (req.wantsJSON || sails.config.hooks.views === false) { 46 | return res.json(data); 47 | } 48 | 49 | // If second argument is a string, we take that to mean it refers to a view. 50 | // If it was omitted, use an empty object (`{}`) 51 | options = (typeof options === 'string') ? { view: options } : options || {}; 52 | 53 | // Attempt to prettify data for views, if it's a non-error object 54 | var viewData = data; 55 | if (!(viewData instanceof Error) && 'object' == typeof viewData) { 56 | try { 57 | viewData = require('util').inspect(data, {depth: null}); 58 | } 59 | catch(e) { 60 | viewData = undefined; 61 | } 62 | } 63 | 64 | // If a view was provided in options, serve it. 65 | // Otherwise try to guess an appropriate view, or if that doesn't 66 | // work, just send JSON. 67 | if (options.view) { 68 | return res.view(options.view, { data: viewData, title: 'Not Found' }); 69 | } 70 | 71 | // If no second argument provided, try to serve the default view, 72 | // but fall back to sending JSON(P) if any errors occur. 73 | else return res.view('404', { data: viewData, title: 'Not Found' }, function (err, html) { 74 | 75 | // If a view error occured, fall back to JSON(P). 76 | if (err) { 77 | // 78 | // Additionally: 79 | // • If the view was missing, ignore the error but provide a verbose log. 80 | if (err.code === 'E_VIEW_FAILED') { 81 | sails.log.verbose('res.notFound() :: Could not locate view for error page (sending JSON instead). Details: ',err); 82 | } 83 | // Otherwise, if this was a more serious error, log to the console with the details. 84 | else { 85 | sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 86 | } 87 | return res.json(data); 88 | } 89 | 90 | return res.send(html); 91 | }); 92 | 93 | }; 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kontakt@julian-claus.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ############### .gitignore ################## 3 | ################################################ 4 | # 5 | # This file is only relevant if you are using git. 6 | # 7 | # Files which match the splat patterns below will 8 | # be ignored by git. This keeps random crap and 9 | # sensitive credentials from being uploaded to 10 | # your repository. It allows you to configure your 11 | # app for your machine without accidentally 12 | # committing settings which will smash the local 13 | # settings of other developers on your team. 14 | # 15 | # Some reasonable defaults are included below, 16 | # but, of course, you should modify/extend/prune 17 | # to fit your needs! 18 | ################################################ 19 | 20 | 21 | 22 | 23 | ################################################ 24 | # Local Configuration 25 | # 26 | # Explicitly ignore files which contain: 27 | # 28 | # 1. Sensitive information you'd rather not push to 29 | # your git repository. 30 | # e.g., your personal API keys or passwords. 31 | # 32 | # 2. Environment-specific configuration 33 | # Basically, anything that would be annoying 34 | # to have to change every time you do a 35 | # `git pull` 36 | # e.g., your local development database, or 37 | # the S3 bucket you're using for file uploads 38 | # development. 39 | # 40 | ################################################ 41 | 42 | config/local.js 43 | 44 | 45 | 46 | 47 | 48 | ################################################ 49 | # Dependencies 50 | # 51 | # When releasing a production app, you may 52 | # consider including your node_modules and 53 | # bower_components directory in your git repo, 54 | # but during development, its best to exclude it, 55 | # since different developers may be working on 56 | # different kernels, where dependencies would 57 | # need to be recompiled anyway. 58 | # 59 | # More on that here about node_modules dir: 60 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 61 | # (credit Mikeal Rogers, @mikeal) 62 | # 63 | # About bower_components dir, you can see this: 64 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 65 | # (credit Addy Osmani, @addyosmani) 66 | # 67 | ################################################ 68 | 69 | node_modules 70 | bower_components 71 | 72 | 73 | 74 | 75 | ################################################ 76 | # Sails.js / Waterline / Grunt 77 | # 78 | # Files generated by Sails and Grunt, or related 79 | # tasks and adapters. 80 | ################################################ 81 | .tmp 82 | dump.rdb 83 | 84 | 85 | 86 | 87 | 88 | ################################################ 89 | # Node.js / NPM 90 | # 91 | # Common files generated by Node, NPM, and the 92 | # related ecosystem. 93 | ################################################ 94 | lib-cov 95 | *.seed 96 | *.log 97 | *.out 98 | *.pid 99 | npm-debug.log 100 | 101 | 102 | 103 | 104 | 105 | ################################################ 106 | # Miscellaneous 107 | # 108 | # Common files generated by text editors, 109 | # operating systems, file systems, etc. 110 | ################################################ 111 | 112 | *~ 113 | *# 114 | .DS_STORE 115 | .netbeans 116 | nbproject 117 | .idea 118 | .node_history 119 | package-lock.json 120 | -------------------------------------------------------------------------------- /frontend/src/components/user/products/product/ProductPatch.desktop.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 91 | -------------------------------------------------------------------------------- /backend/config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap 3 | * (sails.config.bootstrap) 4 | * 5 | * An asynchronous bootstrap function that runs before your Sails app gets lifted. 6 | * This gives you an opportunity to set up your data model, run jobs, or perform some special logic. 7 | * 8 | * For more information on bootstrapping your app, check out: 9 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.bootstrap.html 10 | */ 11 | 12 | const users = [{ 13 | name: 'Joe', 14 | password: 'toasty' 15 | }, { 16 | name: 'Anna', 17 | password: 'sunflower' 18 | }, { 19 | name: 'Tom', 20 | password: 'jerry' 21 | }] 22 | 23 | const products = [{ 24 | title: 'Cell phone', 25 | description: 'Now with a cool touch display and many colors.', 26 | price: 199.95, 27 | user: 1 28 | }, { 29 | title: 'Television', 30 | description: 'This is a brand new television with remote control.', 31 | price: 499.99, 32 | user: 2 33 | }, { 34 | title: 'Keyboard', 35 | description: 'The coolest keyboard in the world. Just buy it.', 36 | price: 10.00, 37 | user: 2 38 | }, { 39 | title: 'Book', 40 | description: 'A entertaining, exciting book with many images.', 41 | price: 14.95, 42 | user: 3 43 | }, { 44 | title: 'Sword', 45 | description: 'Take your enemies apart with this lovely sword.', 46 | price: 300, 47 | user: 2 48 | }, { 49 | title: 'Mouse', 50 | description: 'This is the new generation mouse with multiple inputs.', 51 | price: 12.99, 52 | user: 3 53 | }, { 54 | title: 'Watch', 55 | description: 'There is no better watch in this world. Just buy it.', 56 | price: 199.95, 57 | user: 1 58 | }, { 59 | title: 'Laptop', 60 | description: 'This laptop lacks of nothing. You can do everything with it.', 61 | price: 700, 62 | user: 2 63 | }, { 64 | title: 'Printer', 65 | description: 'You print so many things with this printer. Give it a try.', 66 | price: 395, 67 | user: 1 68 | }, { 69 | title: 'Headphones', 70 | description: 'Feel the music with these headphones. Exclusive and special.', 71 | price: 100.95, 72 | user: 2 73 | }, { 74 | title: 'House', 75 | description: 'Many rooms, many windows, one door. What more to say? It\'s a house.', 76 | price: 100000, 77 | user: 2 78 | }, { 79 | title: 'Water', 80 | description: 'If you feel dry and not liquid you should buy this water instantly.', 81 | price: 0.95, 82 | user: 1 83 | }] 84 | 85 | module.exports.bootstrap = function(cb) { 86 | users.forEach(user => { 87 | 88 | User 89 | .findOne({ 90 | name: user.name 91 | }) 92 | .exec((error, potentialUser) => { 93 | if (error) sails.log.error(error) 94 | 95 | if (!potentialUser) { 96 | User 97 | .create({ 98 | name: user.name, 99 | password: user.password 100 | }) 101 | .exec((error, user) => { 102 | sails.log.info('Created fixture user', user) 103 | }) 104 | } 105 | }) 106 | }) 107 | 108 | products.forEach(product => { 109 | Product 110 | .findOrCreate({ 111 | title: product.title, 112 | description: product.description, 113 | price: product.price, 114 | user: product.user 115 | }) 116 | .exec((error, product) => { 117 | if (error) sails.log.error(error) 118 | 119 | sails.log.info('Created fixture product', product) 120 | }) 121 | }) 122 | 123 | cb() 124 | } 125 | -------------------------------------------------------------------------------- /backend/assets/precache-manifest.28eda1f90a81d9bfde2bfaf166d3f21f.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = [ 2 | { 3 | "revision": "7cb72ca9be1e52890315", 4 | "url": "/js/0011.d26555d2.js" 5 | }, 6 | { 7 | "revision": "1e22d0bbae81de3dc9ce", 8 | "url": "/js/83d0.4eb65a7f.js" 9 | }, 10 | { 11 | "revision": "43bf89bcde05089277e9", 12 | "url": "/js/92d7.1f72dc58.js" 13 | }, 14 | { 15 | "revision": "560d20ca9ac5658d16bf", 16 | "url": "/js/app.1bcceadd.js" 17 | }, 18 | { 19 | "revision": "22b7e24a35f5270dd684", 20 | "url": "/css/chunk-01c9.4ddb07be.css" 21 | }, 22 | { 23 | "revision": "22b7e24a35f5270dd684", 24 | "url": "/js/chunk-01c9.9f7230ba.js" 25 | }, 26 | { 27 | "revision": "f98a57d24f3005b06f21", 28 | "url": "/css/chunk-04f2.d46b21cd.css" 29 | }, 30 | { 31 | "revision": "f98a57d24f3005b06f21", 32 | "url": "/js/chunk-04f2.1b246ded.js" 33 | }, 34 | { 35 | "revision": "9aa7061667031e9c2dba", 36 | "url": "/js/chunk-08b7.8512807a.js" 37 | }, 38 | { 39 | "revision": "509b072ffa4180493b53", 40 | "url": "/js/chunk-1563.7396ec83.js" 41 | }, 42 | { 43 | "revision": "378bbf3854c988d7d08b", 44 | "url": "/js/chunk-1a17.30f2d575.js" 45 | }, 46 | { 47 | "revision": "7f31c63beba70bf7b8d6", 48 | "url": "/js/chunk-3887.aa15bb72.js" 49 | }, 50 | { 51 | "revision": "4aedc9b2b7e58d8e743e", 52 | "url": "/css/chunk-501f.1eb1bb2f.css" 53 | }, 54 | { 55 | "revision": "4aedc9b2b7e58d8e743e", 56 | "url": "/js/chunk-501f.e145111d.js" 57 | }, 58 | { 59 | "revision": "7c1e76f802589d41cfd5", 60 | "url": "/css/chunk-58f5.b1ea0394.css" 61 | }, 62 | { 63 | "revision": "7c1e76f802589d41cfd5", 64 | "url": "/js/chunk-58f5.62a37769.js" 65 | }, 66 | { 67 | "revision": "e9f6f11007417812089e", 68 | "url": "/js/chunk-596f.e000c2e0.js" 69 | }, 70 | { 71 | "revision": "afe1091aefbf8c1df9d9", 72 | "url": "/css/chunk-5e44.062decc4.css" 73 | }, 74 | { 75 | "revision": "afe1091aefbf8c1df9d9", 76 | "url": "/js/chunk-5e44.8377145e.js" 77 | }, 78 | { 79 | "revision": "a875373a8d4bbf4501e2", 80 | "url": "/js/chunk-5e5d.1f401c26.js" 81 | }, 82 | { 83 | "revision": "0b06c675490ffd7df576", 84 | "url": "/js/chunk-6017.a01ecfd3.js" 85 | }, 86 | { 87 | "revision": "a1aa58fb0899e13511b1", 88 | "url": "/js/chunk-6eaa.c2ea0542.js" 89 | }, 90 | { 91 | "revision": "e1d165636ee0de191bc3", 92 | "url": "/js/chunk-727a.61973d24.js" 93 | }, 94 | { 95 | "revision": "99b7bb7ae474925e9456", 96 | "url": "/js/chunk-7644.e3f32036.js" 97 | }, 98 | { 99 | "revision": "cebb351cda7861785dc2", 100 | "url": "/js/chunk-7bd0.f386b588.js" 101 | }, 102 | { 103 | "revision": "435faf69d77a7e1e1478", 104 | "url": "/js/chunk-84ba.0c9cad26.js" 105 | }, 106 | { 107 | "revision": "6d5ecc03006c032fb0f1", 108 | "url": "/js/chunk-a85b.82aa834f.js" 109 | }, 110 | { 111 | "revision": "a1510bf2918075a75851", 112 | "url": "/js/chunk-abc6.2eb6f8f4.js" 113 | }, 114 | { 115 | "revision": "6d05238dd6f7081c6d1c", 116 | "url": "/js/chunk-db80.3deb64de.js" 117 | }, 118 | { 119 | "revision": "fb929085840cd998dd28", 120 | "url": "/js/chunk-vendors.3e9c575e.js" 121 | }, 122 | { 123 | "revision": "430f0a872990c82c3ca55b9a44cf46e8", 124 | "url": "/index.html" 125 | }, 126 | { 127 | "revision": "d2bd7d76fb1ea6f201f8bb1ea1345ba1", 128 | "url": "/img/baseline-info-24px.svg" 129 | } 130 | ]; -------------------------------------------------------------------------------- /backend/config/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Mappings 3 | * (sails.config.routes) 4 | * 5 | * Your routes map URLs to views and controllers. 6 | * 7 | * If Sails receives a URL that doesn't match any of the routes below, 8 | * it will check for matching files (images, scripts, stylesheets, etc.) 9 | * in your assets directory. e.g. `http://localhost:1337/images/foo.jpg` 10 | * might match an image file: `/assets/images/foo.jpg` 11 | * 12 | * Finally, if those don't match either, the default 404 handler is triggered. 13 | * See `api/responses/notFound.js` to adjust your app's 404 logic. 14 | * 15 | * Note: Sails doesn't ACTUALLY serve stuff from `assets`-- the default Gruntfile in Sails copies 16 | * flat files from `assets` to `.tmp/public`. This allows you to do things like compile LESS or 17 | * CoffeeScript for the front-end. 18 | * 19 | * For more information on configuring custom routes, check out: 20 | * http://sailsjs.org/#!/documentation/concepts/Routes/RouteTargetSyntax.html 21 | */ 22 | 23 | module.exports.routes = { 24 | 25 | /*************************************************************************** 26 | * * 27 | * Make the view located at `views/homepage.ejs` (or `views/homepage.jade`, * 28 | * etc. depending on your default view engine) your home page. * 29 | * * 30 | * (Alternatively, remove this and add an `index.html` file in your * 31 | * `assets` directory) * 32 | * * 33 | ***************************************************************************/ 34 | 35 | '/*': { 36 | controller: 'App', 37 | action: 'serve', 38 | skipAssets: true, 39 | skipRegex: /^\/api\/.*$/ 40 | }, 41 | 42 | 'post /api/help': 'help/Help.postHelp', 43 | 44 | 'post /api/register/post': 'Register.post', 45 | 46 | 'post /api/login/post': 'LoginController.post', 47 | 48 | 'get /api/user/get': 'user/UserController.get', 49 | 50 | 'get /api/products/get': 'ProductsController.getProducts', 51 | 52 | 'post /api/user/products/product/post': 'user/products/product/ProductController.postProduct', 53 | 54 | 'get /api/user/products/product/get': 'user/products/product/ProductController.getProduct', 55 | 56 | 'patch /api/user/products/product/patch': 'user/products/product/ProductController.patchProduct', 57 | 58 | 'delete /api/user/products/product/delete': 'user/products/product/ProductController.deleteProduct', 59 | 60 | 'get /api/user/products/getProductsByUser': 'user/products/ProductsController.getProductsByUser', 61 | 62 | 'get /api/user/products/get': 'user/products/ProductsController.getProducts', 63 | 64 | 'post /api/user/shop/basket/post': 'user/shop/BasketController.postBasket' 65 | 66 | /*************************************************************************** 67 | * * 68 | * Custom routes here... * 69 | * * 70 | * If a request to a URL doesn't match any of the custom routes above, it * 71 | * is matched against Sails route blueprints. See `config/blueprints.js` * 72 | * for configuration options and examples. * 73 | * * 74 | ***************************************************************************/ 75 | 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/components/help/Help.desktop.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 129 | 130 | 137 | -------------------------------------------------------------------------------- /frontend/src/App.mobile.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 129 | 130 | 156 | -------------------------------------------------------------------------------- /frontend/src/App.desktop.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 123 | 124 | 129 | --------------------------------------------------------------------------------