├── .gitignore ├── README.md ├── package.json ├── saofile.js ├── template ├── .babelrc ├── .eslintrc ├── .podhook ├── README.md ├── data │ └── README.md ├── f3.config.js ├── gitignore ├── package.json ├── src │ ├── README.md │ ├── client │ │ ├── README.md │ │ ├── assets │ │ │ ├── README.md │ │ │ ├── app.styl │ │ │ └── theme.less │ │ ├── components │ │ │ ├── DataCard.vue │ │ │ ├── DataTable.vue │ │ │ ├── README.md │ │ │ ├── SelectCard.vue │ │ │ ├── layout │ │ │ │ ├── Footer.vue │ │ │ │ ├── SideNav.vue │ │ │ │ └── TopNav.vue │ │ │ └── select │ │ │ │ └── Permission.vue │ │ ├── layouts │ │ │ ├── README.md │ │ │ ├── dashboard.vue │ │ │ └── default.vue │ │ ├── middleware │ │ │ ├── README.md │ │ │ ├── anonymous.js │ │ │ ├── authenticated.js │ │ │ └── crash.js │ │ ├── modules │ │ │ ├── less.js │ │ │ ├── livescript.js │ │ │ └── pug.js │ │ ├── pages │ │ │ ├── README.md │ │ │ ├── index.vue │ │ │ └── reports.vue │ │ ├── plugins │ │ │ ├── README.md │ │ │ ├── async-computed.js │ │ │ ├── casl.js │ │ │ ├── crash.js │ │ │ ├── data.js │ │ │ ├── feathers.js │ │ │ ├── fuzzysort.js │ │ │ ├── iview.js │ │ │ ├── media-query.js │ │ │ ├── routersync.js │ │ │ ├── scrollto.js │ │ │ ├── storyboard.js │ │ │ └── vuebar.js │ │ ├── static │ │ │ ├── README.md │ │ │ ├── console.html │ │ │ ├── css │ │ │ │ ├── assets │ │ │ │ │ ├── error.svg │ │ │ │ │ ├── info.svg │ │ │ │ │ ├── out.svg │ │ │ │ │ ├── prompt.svg │ │ │ │ │ └── share.svg │ │ │ │ ├── console.css │ │ │ │ └── fab.css │ │ │ ├── docs.html │ │ │ ├── icon.png │ │ │ ├── js │ │ │ │ ├── EventSource.js │ │ │ │ ├── console.js │ │ │ │ ├── console.ls │ │ │ │ ├── copy.js │ │ │ │ └── prettify.packed.js │ │ │ └── logo.png │ │ ├── store │ │ │ ├── README.md │ │ │ ├── auth.js │ │ │ ├── crash.js │ │ │ ├── index.js │ │ │ ├── lookup.js │ │ │ └── network.js │ │ └── utils │ │ │ ├── README.md │ │ │ ├── hooks.js │ │ │ ├── index.js │ │ │ ├── initAuth.js │ │ │ ├── initClient.js │ │ │ ├── store │ │ │ ├── modules │ │ │ │ ├── auth │ │ │ │ │ ├── actions.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── mutations.js │ │ │ │ │ └── state.js │ │ │ │ ├── crash │ │ │ │ │ ├── actions.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── mutations.js │ │ │ │ │ └── state.js │ │ │ │ ├── lookup │ │ │ │ │ ├── actions.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── mutations.js │ │ │ │ │ └── state.js │ │ │ │ └── network │ │ │ │ │ ├── actions.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── mutations.js │ │ │ │ │ └── state.js │ │ │ └── plugins │ │ │ │ ├── casl.js │ │ │ │ └── crash.js │ │ │ └── vue-media-query-mixin │ │ │ ├── index.js │ │ │ └── server.js │ └── server │ │ ├── api.ls │ │ ├── channels.ls │ │ ├── config │ │ ├── default.yml │ │ ├── production-0.yml │ │ └── production.yml │ │ ├── db │ │ ├── migrations │ │ │ ├── 00-userTypes.js │ │ │ ├── 01-roles.js │ │ │ ├── 02-accountStatus.js │ │ │ ├── 03-accountGroups.js │ │ │ ├── 04-users.js │ │ │ ├── 05-userAccounts.js │ │ │ ├── 06-userAccountGroups.js │ │ │ └── 07-userRoles.js │ │ ├── mongoose-orm.ls │ │ ├── seed.ls │ │ └── sequelize-orm.ls │ │ ├── hooks │ │ ├── abilities.ls │ │ ├── associate-current-partner.ls │ │ ├── authenticate.ls │ │ ├── authorize.ls │ │ ├── ensure-enabled.ls │ │ ├── global.ls │ │ ├── has-permission-boolean.ls │ │ ├── has-permission.ls │ │ ├── index.ls │ │ ├── logger.ls │ │ ├── prevent-disabled-admin.ls │ │ ├── send-verification-email.ls │ │ ├── set-default-role.ls │ │ └── set-first-user-to-role.ls │ │ ├── index.ls │ │ ├── jobs │ │ ├── index.ls │ │ ├── queue.ls │ │ ├── scheduler.ls │ │ ├── tasks.ls │ │ └── workers.ls │ │ ├── middleware │ │ ├── index.ls │ │ └── nuxt.ls │ │ ├── notifications │ │ ├── dispatcher.ls │ │ └── templates │ │ │ └── verify-signup.pug │ │ ├── services │ │ ├── auth │ │ │ └── auth.service.ls │ │ ├── index.ls │ │ ├── notifications │ │ │ ├── notifications.hooks.ls │ │ │ └── notifications.service.ls │ │ └── users │ │ │ ├── users.hooks.ls │ │ │ ├── users.model.ls │ │ │ ├── users.schema.ls │ │ │ └── users.service.ls │ │ └── utils │ │ ├── patterns.ls │ │ ├── repl.ls │ │ ├── storyboard.ls │ │ ├── to.ls │ │ ├── validate-pattern.ls │ │ └── winston.ls ├── tests │ ├── README.md │ └── features │ │ ├── hooks.ls │ │ ├── is-it-friday-yet.feature │ │ ├── step_definitions │ │ └── mink-gherkin.ls │ │ ├── support │ │ ├── mink.ls │ │ └── scope.ls │ │ └── world.ls └── uploads │ └── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [sao](https://saojs.org/) template enabling you to scaffold a feathers+nuxt app in seconds. See documentation [site]( https://feathers-nuxt.netlify.com/) for available features. 2 | 3 | **THIS IS ALPHA, BE CAREFUL** 4 | 5 | I'm also VERY interested in feedback / PRs, so send them my way or email me! 6 | 7 | ## Quick Start 8 | Use npx, if you do not have sao installed. npx comes bundled with npm version 5.2+. 9 | 10 | ```bash 11 | npx sao npm:@feathers-nuxt/template-app --update awesome-app 12 | ``` 13 | 14 | You will be prompted to answer a couple of questions to determine how the template should be customized to your needs. 15 | Sao will then clone the template in this repository and put the customized template inside `awesome-app` directory. 16 | 17 | If you already have sao installed globally, just invoke it with this template. 18 | 19 | ```bash 20 | sao npm:@feathers-nuxt/template-app --update awesome-app # downloads template from npm 21 | # sao feathers-nuxt/template-app --update awesome-app # downloads template from github 22 | ``` 23 | 24 | ## Installation 25 | You may also use `f3` cli instead of `sao` if you install it globally. 26 | ```bash 27 | yarn global add @feathers-nuxt/cli 28 | # npm i -g @feathers-nuxt/cli 29 | f3 init awesome-app 30 | ``` 31 | > `yarn` is preferred to `npm`, although you may use the later if you so wish. 32 | 33 | ## Usage 34 | Once your app is initialized, `cd awesome-app` to access your new project. 35 | 36 | To start the application in development mode - watch files for changes and reload - run 37 | ```bash 38 | yarn dev 39 | ``` 40 | If you are using an sql database ensure you run `yarn migrate up` to create necessary tables, then `yarn seed` to add test data to the database. 41 | > There are several other **npm scripts** defined in **package.json**. To list them all, invoke `yarn run` 42 | 43 | ## Features 44 | - SSR ready PWA with offline support. 45 | - User Authentication and Authorization taken care of. 46 | - Logging mixin for Feathers app backed by winston with file and console transports. 47 | - End-to-end, hierarchical, real-time, colorful logs and stories with console and websockets transports. 48 | - Bring your own database or RESTful backend for data storage. 49 | - Database migration and seeding npm scripts included. 50 | - Project build and deployment scripts provided. 51 | - Use any compile to JS langauge supported by webpack: livescipt, coffescript, typescript, 52 | - Use any compile to CSS language supported by webpack: stylus, sass, less, 53 | - Use any compile to HTML language supported by webpack: pug, slim, haml, 54 | - Use HEML markup language for building responsive emails with any compile to HTML language. 55 | - Notifications service for sending all kinds of transactional alerts via emails, SMS, pushes, webpushes or slack 56 | - File uploads service with configurable backing storage: Any store that implements the blob store interface. 57 | - All feathers services automatically available in `vuex` store. 58 | - Namespaced routing to prevent conflict of API and UI routes. 59 | - Optional caching for API routes using Redis on the backend. 60 | - Automatic caching for API routes using feathers-vuex on the frontend. 61 | - Automatic caching for UI routes using workbox runtimeCaching. 62 | - Optional distributed, delayed background job system backed by Redis. 63 | - iView: A high quality UI Toolkit built on Vue.js 64 | - DataTable and DataCard UI components compatible with feathers service endpoints. 65 | 66 | ### Guide 67 | An application initialized using `f3` will have the following directory stucture. 68 | 69 | ```text 70 | ├── f3.config.js # nuxt & backpack configuration 71 | ├── .babelrc # babel configuration to use with backpack 72 | ├── .podhook # shell commands to run on remote server during deploy 73 | ├── .gitignore # list of file to ignore while deploying to remote server 74 | └── src 75 | ├── client # transpiled using nuxt 76 | ├── assets # files to transpile with webpack: less, stylus 77 | ├── static # files to serve as static resources 78 | ├── pages # Vue SFC accessible via a URL 79 | ├── components # Vue SFC to use within other SFC 80 | ├── layouts # Vue SFC to use for page layout 81 | ├── middleware # nuxt renderer middleware 82 | ├── modules # nuxt modules 83 | ├── plugins # Vue.js plugins 84 | ├── store # Vuex store modules 85 | └── utils 86 | ├── initClient.js # creates feathers client for server and client bundle 87 | ├── initAuth.js # autheticate and populate store with user object 88 | └── store 89 | ├── modules # modules for vuex 90 | └── plugins # plugins for vuex 91 | └── server # transpiled using backpack 92 | ├── config 93 | ├── default.yml # settings for development env 94 | ├── production.yml # settings for production env 95 | └── production-0.yml # settings for PM2 app instance 0 96 | ├── index.ls # entry to initialize both app and api servers 97 | ├── app.ls # express server with nuxt middleware and feathers as sub app 98 | ├── api.ls # feathers server with socket.io and rest transports 99 | ├── api.hooks.ls # configures global api hooks 100 | ├── hooks # triggers run during resource access 101 | └──global.ls # configures global api hooks 102 | ├── notifications # templates and dispatcher for email, sms, webpush, 103 | ├── services # service, schema, model and hooks for resources in db, fs, 104 | └── auth.ls # configures feathers for authentication 105 | └── db 106 | ├── orm.ls # configures feathers to use mongoose or sequelize ORM 107 | ├── seed.ls # populates database with dummy data 108 | └── migrations # procedures to creates and drop tables 109 | └── middleware 110 | └── nuxt.ls # nuxt middleware for SSR 111 | ``` 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feathers-nuxt/template-app", 3 | "version": "1.0.11", 4 | "description": "sao template for scaffolding feathers+nuxt apps ", 5 | "main": "sao.js", 6 | "scripts": { 7 | "release": "release-it", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/feathers-nuxt/template-app.git" 13 | }, 14 | "keywords": [ 15 | "feathers", 16 | "nuxt", 17 | "sao", 18 | "template", 19 | "fullstack", 20 | "f3", 21 | "webpack", 22 | "backpack", 23 | "scripts", 24 | "iView" 25 | ], 26 | "author": "kelvin kharhys ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/feathers-nuxt/template-app/issues" 30 | }, 31 | "homepage": "https://github.com/feathers-nuxt/template-app#readme", 32 | "devDependencies": { 33 | "release-it": "^7.6.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /saofile.js: -------------------------------------------------------------------------------- 1 | // uncomment below 2 | 3 | // (function() { 4 | // var childProcess = require("child_process"); 5 | // var oldSpawn = childProcess.spawn; 6 | // function mySpawn() { 7 | // console.log('spawn called'); 8 | // console.log(arguments); 9 | // var result = oldSpawn.apply(this, arguments); 10 | // return result; 11 | // } 12 | // childProcess.spawn = mySpawn; 13 | // })(); 14 | 15 | module.exports = { 16 | description: 'Scaffold a feathers+nuxt project', 17 | prompts() { 18 | return [ 19 | /* About project */ 20 | { 21 | name: 'name', 22 | message: 'What is the name of the new project?', 23 | default: this.outFolder 24 | }, 25 | { 26 | name: 'description', 27 | message: 'How would you describe the new project?', 28 | default: `feathers nuxt fullstack` 29 | }, 30 | 31 | /* Database type */ 32 | { 33 | name: 'database', 34 | message: 'What type of database will you be using', 35 | type: 'list', 36 | choices: [ 37 | { name: 'Memory Storage', value: 'memory', short: 'memory' }, 38 | { name: 'File Storage', value: 'file', short: 'file' }, 39 | { name: 'SQL (Relational) Database', value: 'sql', short: 'sql' }, 40 | { name: 'NoSQL (Document) Database', value: 'nosql', short: 'nosql' } 41 | ], 42 | default: 'sql' 43 | }, 44 | 45 | /* Document DB */ 46 | { 47 | name: 'nosql_dialect', 48 | message: 'What dialect of Document database will you be using', 49 | when: function({database}) { return database == 'nosql' }, 50 | type: 'list', 51 | choices: [ 52 | { name: 'MongoDB', value: 'mongodb', short: 'mongodb' }, 53 | // { name: 'CouchDB', value: 'couchdb', short: 'couchdb' }, 54 | // { name: 'Cassandra', value: 'cassandra', short: 'cassandra' } 55 | ], 56 | default: 'mongodb' 57 | }, 58 | { 59 | name: 'nosql_host', 60 | message: 'Document Database host', 61 | when: function({database}) { return database == 'nosql' }, 62 | default: '127.0.0.1' 63 | }, 64 | { 65 | name: 'nosql_port', 66 | message: 'Document Database port', 67 | when: function({database}) { return database == 'nosql' }, 68 | default: '27017' 69 | }, 70 | { 71 | name: 'nosql_database', 72 | message: 'Document Database name', 73 | when: function({database}) { return database == 'nosql' } 74 | }, 75 | 76 | 77 | /* Relational DB */ 78 | { 79 | name: 'sequelize_dialect', 80 | message: 'What dialect of SQL database will you be using', 81 | when: function({database}) { return database == 'sql' }, 82 | type: 'list', 83 | choices: [ 84 | { name: 'SQLite', value: 'sqlite', short: 'sqlite' }, 85 | { name: 'MySQL', value: 'mysql', short: 'mysql' }, 86 | { name: 'MSSQL', value: 'mssql', short: 'mssql' }, 87 | { name: 'PostgreSQL', value: 'postgresql', short: 'postgresql' } 88 | ], 89 | default: 'mysql' 90 | }, 91 | { 92 | name: 'sequelize_host', 93 | message: 'SQL Database host', 94 | when: function({database}) { return database == 'sql' }, 95 | default: '127.0.0.1' 96 | }, 97 | { 98 | name: 'sequelize_port', 99 | message: 'SQL Database port', 100 | when: function({database}) { return database == 'sql' }, 101 | default: '3306' 102 | }, 103 | { 104 | name: 'sequelize_database', 105 | message: 'SQL Database name', 106 | when: function({database}) { return database == 'sql' } 107 | }, 108 | { 109 | name: 'sequelize_username', 110 | message: 'SQL Database username', 111 | when: function({database}) { return database == 'sql' } 112 | }, 113 | { 114 | name: 'sequelize_password', 115 | message: 'SQL Database password', 116 | when: function({database}) { return database == 'sql' } 117 | }, 118 | 119 | /* Cache DB */ 120 | { 121 | name: 'cache', 122 | type: 'confirm', 123 | message: 'Cache API responses with redis?', 124 | default: false 125 | }, 126 | { 127 | name: 'resque', 128 | type: 'confirm', 129 | message: 'Queue background jobs with redis?', 130 | default: false 131 | }, 132 | { 133 | name: 'redis_host', 134 | message: 'Redis server host address', 135 | when: function({cache, resque}) { return !!(cache || resque) }, 136 | default: '127.0.0.1' 137 | }, 138 | { 139 | name: 'redis_port', 140 | message: 'Redis server host port', 141 | when: function({cache, resque}) { return !!(cache || resque) }, 142 | default: '6379' 143 | }, 144 | { 145 | name: 'redis_database', 146 | message: 'Redis database to use', 147 | when: function({cache, resque}) { return !!(cache || resque) }, 148 | default: '0' 149 | }, 150 | { 151 | name: 'redis_password', 152 | message: 'Password for Redis database', 153 | when: function({cache, resque}) { return !!(cache || resque) }, 154 | default: '' 155 | }, 156 | 157 | /* SMTP Mailer */ 158 | { 159 | name: 'smtp', 160 | type: 'confirm', 161 | message: 'Set up SMPT credentials for sending emails?', 162 | default: false 163 | }, 164 | { 165 | name: 'smtp_host', 166 | message: 'SMTP server host address', 167 | when: function({smtp}) { return !!(smtp) }, 168 | default: '127.0.0.1' 169 | }, 170 | { 171 | name: 'smtp_port', 172 | message: 'SMTP server host port', 173 | when: function({smtp}) { return !!(smtp) }, 174 | default: '6379' 175 | }, 176 | { 177 | name: 'smtp_username', 178 | message: 'Email address of SMTP user', 179 | when: function({smtp}) { return !!(smtp) }, 180 | default: 'no-reply@example.com' 181 | }, 182 | { 183 | name: 'smtp_password', 184 | message: 'Password for SMTP User', 185 | when: function({smtp}) { return !!(smtp) }, 186 | default: '5trong3r' 187 | }, 188 | 189 | /* API Documentation */ 190 | { 191 | name: 'documentation', 192 | type: 'confirm', 193 | message: 'Include swagger for API endpoint documentation?', 194 | default: true 195 | }, 196 | 197 | /* Version Control */ 198 | { 199 | name: 'username', 200 | message: 'What is your GitHub username?', 201 | default: this.gitUser.name, 202 | store: true 203 | }, 204 | { 205 | name: 'email', 206 | message: 'What is your GitHub email?', 207 | default: this.gitUser.email, 208 | store: true 209 | }, 210 | { 211 | name: 'website', 212 | message: 'The URL of your website?', 213 | default: `github.com/${this.gitUser.name}`, 214 | store: true 215 | } 216 | ] 217 | }, 218 | 219 | actions() { 220 | return [ 221 | { 222 | type: 'add', 223 | files: '**', 224 | filters: { 225 | 'src/server/jobs/*': 'resque', 226 | 'src/server/db/sequelize-orm.ls': "database == 'sql'", 227 | 'src/server/db/mongoose-orm.ls': "database == 'nosql'" 228 | } 229 | }, 230 | { 231 | type: 'move', 232 | patterns: { 233 | 'gitignore': '.gitignore', 234 | 'src/server/db/*-orm.ls': 'src/server/db/orm.ls' 235 | } 236 | } 237 | ] 238 | }, 239 | 240 | async completed() { 241 | this.gitInit() 242 | await this.npmInstall() 243 | this.showProjectTips() 244 | } 245 | 246 | } -------------------------------------------------------------------------------- /template/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /template/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:vue/recommended" 5 | ], 6 | "rules": { 7 | "vue/html-self-closing": "off", 8 | "import/no-unresolved": 0, 9 | "import/no-unassigned-import": 0, 10 | "semi": ["error", "never"], 11 | "no-console": "off", 12 | "space-before-function-paren": [ 13 | "error", 14 | { 15 | "anonymous": "always", 16 | "named": "always", 17 | "asyncArrow": "always" 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /template/.podhook: -------------------------------------------------------------------------------- 1 | rm -rf node_modules 2 | npm remove --global f3 3 | yarn install 4 | yarn add github:feathers-nuxt/cli 5 | yarn add github:feathers-nuxt/feathers-rest-proxy 6 | chmod -R a+wrx node_modules/f3 7 | yarn run build:server -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # See (doc site)[feathers-nuxt.netlify.com] for comprehensive guide 2 | 3 | 4 | 5 | 9 | 10 | Changes: 11 | - Use @nuxtjs/axios module for xhr with feathers client 12 | - Use rest transport instead of websockets with feathers client 13 | - Use feathers server instance intead of client instance when server side rendering 14 | - Include End to End tests -------------------------------------------------------------------------------- /template/data/README.md: -------------------------------------------------------------------------------- 1 | This folder is for file-based databases such as ne-db, sqlite -------------------------------------------------------------------------------- /template/f3.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | backpack: (config, options, webpack) => { 5 | config.mode = 'development' // or 'production' 6 | // server main file 7 | config.entry.main = path.resolve(__dirname, 'src', 'server', 'index.ls') 8 | config.output.path = path.resolve(__dirname, 'dist', 'server') 9 | config.resolve.alias = { 10 | '~middleware': path.resolve(__dirname, 'src', 'server', 'middleware'), 11 | '~services': path.resolve(__dirname, 'src', 'services', 'util'), 12 | '~util': path.resolve(__dirname, 'src', 'server', 'util'), 13 | '~': path.resolve(__dirname, 'src', 'server') 14 | } 15 | // console.log(config.resolve.extensions) 16 | return config 17 | }, 18 | nuxt: { 19 | srcDir: path.resolve(__dirname, 'src', 'client'), 20 | modules: [ 21 | ['~/modules/livescript'], // add support for livescript language 22 | ['~/modules/less'], // add support for less language 23 | 24 | ['@nuxtjs/axios'], // for use by feathers-rest client 25 | 26 | ['nuxt-robots-module'], // SEO 27 | '@nuxtjs/pwa', 28 | 29 | ], 30 | plugins: [ 31 | { src: '~/plugins/iview' }, // ui components 32 | { src: '~/plugins/data' }, // datatables backed by feathers 33 | { src: '~/plugins/media-query' }, // responsive rendering 34 | { src: '~/plugins/feathers' }, 35 | { src: '~/plugins/casl' }, 36 | { src: '~/plugins/routersync', ssr: false }, 37 | ], 38 | buildDir: 'dist/client', 39 | build: { 40 | extractCSS: { 41 | allChunks: true 42 | }, 43 | watch: ['utils', 'components/partials/*'], 44 | extend(config, ctx) { 45 | const aliases = Object.assign(config.resolve.alias, { 46 | // ensure we can require files in utils directory with path alias 47 | '~utils': path.resolve(__dirname, 'src/client/utils') 48 | }) 49 | config.resolve.alias = aliases 50 | } 51 | }, 52 | loading: { 53 | color: '#ff0099' 54 | }, 55 | head: { 56 | title: '<%= name %>', 57 | htmlAttrs: { 58 | lang: 'en-US', 59 | }, 60 | meta: [ 61 | { charset: 'utf-8' }, 62 | { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }, 63 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 64 | { hid: 'description', name: 'description', content: 'Vue JS Radar' }, 65 | { hid: 'keywords', name: 'keywords', content: 'cellulant, bulksms, ui, app' } 66 | ], 67 | link: [ 68 | { rel: 'icon', type: 'image/x-icon', href: 'favicon.ico' }, 69 | ] 70 | }, 71 | css: [ 72 | // Then, woff was invented which has a mode that stops people pirating the font. 73 | // This is the preferred format and WOFF2, a more highly compressed WOFF 74 | {src: 'iview/dist/styles/fonts/ionicons.woff'}, 75 | // {src: 'iview/dist/styles/iview.css'}, 76 | 77 | {src: '~assets/theme.less', lang: 'less'}, 78 | {src: '~assets/app.styl', lang: 'stylus'}, 79 | ] 80 | }, 81 | 82 | project: { // config options obtained from prompts by sao 83 | name: "<%= name %>", 84 | description: "<%= description %>", 85 | database: "<%= database %>", 86 | sequelize_dialect: "<%= sequelize_dialect %>", 87 | sequelize_host: "<%= sequelize_host %>", 88 | sequelize_port: "<%= sequelize_port %>", 89 | sequelize_database: "<%= sequelize_database %>", 90 | sequelize_username: "<%= sequelize_username %>", 91 | sequelize_password: "<%= sequelize_password %>", 92 | cache: "<%= cache %>", 93 | resque: "<%= resque %>", 94 | redis_host: "<%= redis_host %>", 95 | redis_port: "<%= redis_port %>", 96 | redis_database: "<%= redis_database %>", 97 | redis_password: "<%= redis_password %>", 98 | username: "<%= username %>", 99 | email: "<%= email %>", 100 | website: "<%= website %>" 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /template/gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "description": "<%= description %>", 4 | "repository": { 5 | "url": "<%= username %>/<%= name %>", 6 | "type": "git" 7 | }, 8 | "author": "<%= username %> <<%= email %>>", 9 | "private": true, 10 | "version": "0.0.0", 11 | "main": "./dist/server/main.js", 12 | "scripts": { 13 | "migrate": "npx f3 migrate", 14 | "seed": "npx f3 seed", 15 | "dev": "npx f3 dev --experimental-repl-await", 16 | "build": "npx f3 build", 17 | "build:server": "npx f3 build-server", 18 | "build:client": "npx f3 build-client", 19 | "analyze": "npx yarn build:analyze && npx yarn start:analyzer", 20 | "build:analyze": "npx f3 build-client -analyze", 21 | "start:analyzer": "npx webpack-bundle-analyzer .nuxt/stats/client.json", 22 | "test": "npx cucumber-js --require-module livescript --require 'tests/features/**/*.ls' tests/features", 23 | "start": "cross-env NODE_ENV=production node ./dist/server/main.js" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "ISC", 28 | "dependencies": { 29 | "@casl/ability": "^2.5.1", 30 | "@casl/vue": "^0.5.0", 31 | "@feathers-nuxt/cli": "^1.1.8", 32 | "@feathers-nuxt/feathers-notifme": "^1.0.1-beta.1", 33 | "@feathersjs/authentication": "^2.1.13", 34 | "@feathersjs/authentication-client": "^1.0.8", 35 | "@feathersjs/authentication-jwt": "^2.0.7", 36 | "@feathersjs/authentication-local": "^1.2.7", 37 | "@feathersjs/configuration": "^2.0.4", 38 | "@feathersjs/errors": "^3.3.4", 39 | "@feathersjs/express": "^1.2.7", 40 | "@feathersjs/feathers": "^3.2.3", 41 | "@feathersjs/rest-client": "^1.4.5", 42 | "@nuxtjs/axios": "^5.3.6", 43 | "@nuxtjs/pwa": "^2.6.0", 44 | "axios": "^0.18.0", 45 | "compression": "^1.7.3", 46 | "conditional-middleware": "^0.2.0", 47 | "cookie-parser": "^1.4.3", 48 | "cors": "^2.8.5", 49 | "dauria": "^2.0.0", 50 | "feathers-authentication-hooks": "^0.3.1", 51 | "feathers-authentication-management": "^2.0.1", 52 | "feathers-hooks-common": "^4.17.14", <% if(cache) { %> 53 | "feathers-hooks-rediscache": "^1.1.3", <% } %> 54 | "feathers-hooks-validator": "github:kharhys/feathers-hooks-validator", 55 | "feathers-logger": "^0.3.2", <% if(database == 'memory') { %> 56 | "feathers-memory": "^2.1.3", <% } %> <% if(nosql_dialect == 'mongodb') { %> 57 | "feathers-mongoose": "^6.3.0", <% } %> <% if(database == 'embedded') { %> 58 | "feathers-nedb": "^3.1.0", <% } %> <% if(database == 'sql') { %> 59 | "feathers-sequelize": "^3.1.0", <% } %> 60 | "form-data": "^2.3.3", 61 | "helmet": "^3.15.0", 62 | "heml": "^1.1.3", 63 | "into-stream": "^4.0.0", 64 | "iview": "^3.1.4", <% if(nosql_dialect == 'mongodb') { %> 65 | "mongoose": "^5.3.15", <% } %> <% if(sequelize_dialect == 'mysql') { %> 66 | "mysql": "^2.15.0", <% } %> <% if(database == 'file') { %> 67 | "nedb": "^1.8.0", <% } %> <% if(resque) { %> 68 | "node-resque": "^5.3.0", <% } %> 69 | "nuxt": "^2.2.0", 70 | "nuxt-robots-module": "^1.3.0", <% if(sequelize_dialect == 'postgresql') { %> 71 | "pg": "^7.4.2", <% } %> <% if(sequelize_dialect == 'postgresql') { %> 72 | "pg-hstore": "^2.3.2", <% } %> <% if(database == 'sql') { %> 73 | "sequelize": "^4.37.6", <% } %> 74 | "passport-custom": "^1.0.5", 75 | "pretty-error": "^2.1.1", 76 | "serve-favicon": "^2.5.0", <% if(sequelize_dialect == 'sqlite') { %> 77 | "sqlite3": "^4.0.0", <% } %> <% if(sequelize_dialect == 'mssql') { %> 78 | "tedious": "^2.3.1", <% } %> 79 | "vue-media-query-mixin": "^0.1.0", 80 | "vuex-router-sync": "^5.0.0", 81 | "winston": "^3.1.0" 82 | }, 83 | "devDependencies": { 84 | "cross-env": "^5.2.0", 85 | "cucumber": "^5.0.3", 86 | "cucumber-mink": "^2.1.0", 87 | "signale": "^1.3.0", 88 | "webpack-bundle-analyzer": "^3.0.3" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /template/src/README.md: -------------------------------------------------------------------------------- 1 | This folder contains your source code. -------------------------------------------------------------------------------- /template/src/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | > pwa 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | $ npm install # Or yarn install 10 | 11 | # serve with hot reload at localhost:3000 12 | # service worker is disabled in dev 13 | $ npm run dev 14 | 15 | # build for production and launch server 16 | $ npm run build 17 | $ npm start 18 | 19 | # generate static project 20 | $ npm run generate 21 | ``` 22 | 23 | For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). 24 | -------------------------------------------------------------------------------- /template/src/client/assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/assets#webpacked 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /template/src/client/assets/app.styl: -------------------------------------------------------------------------------- 1 | body, __nuxt, __layout 2 | height: 100vh 3 | 4 | 5 | .flex-c 6 | display flex 7 | align-items center 8 | justify-content center 9 | flex-direction row 10 | 11 | .ivu-btn-circle, 12 | .ivu-btn-circle-outline 13 | border-radius .5rem 14 | 15 | .ivu-notice 16 | width 500px 17 | 18 | 19 | .flex-c { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | 27 | .ivu-btn-circle, 28 | .ivu-btn-circle-outline { 29 | border-radius: .5rem; 30 | } 31 | 32 | 33 | .ivu-notice { 34 | width: 500px !important; 35 | } 36 | 37 | .ivu-badge-count { 38 | background: #fff; 39 | color: #a0a0a0; 40 | } 41 | 42 | .ivu-radio-inner::after { 43 | border-radius: 6px !important; 44 | } 45 | 46 | 47 | .ivu-menu-horizontal { 48 | height: 64px !important; 49 | } 50 | 51 | 52 | 53 | html { 54 | // height: 100vh; 55 | } 56 | 57 | 58 | body { 59 | background: #ECE9E6; /* fallback for old browsers */ 60 | background: -webkit-linear-gradient(to top, #FFFFFF, #ECE9E6); /* Chrome 10-25, Safari 5.1-6 */ 61 | background: linear-gradient(to top, #FFFFFF, #ECE9E6); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 62 | 63 | } 64 | 65 | .container { 66 | transition: all .5s cubic-bezier(.55,0,.1,1); 67 | } 68 | 69 | .page-enter-active, .page-leave-active { 70 | transition: opacity .5s 71 | } 72 | .page-enter, .page-leave-active { 73 | opacity: 0 74 | } 75 | 76 | .bounce-enter-active { 77 | animation: bounce-in .8s; 78 | } 79 | .bounce-leave-active { 80 | animation: bounce-out .5s; 81 | } 82 | @keyframes bounce-in { 83 | 0% { transform: scale(0) } 84 | 50% { transform: scale(1.5) } 85 | 100% { transform: scale(1) } 86 | } 87 | @keyframes bounce-out { 88 | 0% { transform: scale(1) } 89 | 50% { transform: scale(1.5) } 90 | 100% { transform: scale(0) } 91 | } 92 | 93 | .slide-left-enter, 94 | .slide-right-leave-active { 95 | opacity: 0; 96 | transform: translate(30px, 0); 97 | } 98 | .slide-left-leave-active, 99 | .slide-right-enter { 100 | opacity: 0; 101 | transform: translate(-30px, 0); 102 | } 103 | 104 | 105 | 106 | /*internet explorer scrollbalken*/ 107 | body{ 108 | scrollbar-base-color: #C0C0C0; 109 | scrollbar-base-color: #C0C0C0; 110 | scrollbar-3dlight-color: #C0C0C0; 111 | scrollbar-highlight-color: #C0C0C0; 112 | scrollbar-track-color: #EBEBEB; 113 | scrollbar-arrow-color: black; 114 | scrollbar-shadow-color: #C0C0C0; 115 | scrollbar-dark-shadow-color: #C0C0C0; 116 | overflow: auto; 117 | } 118 | /*mozilla scrolbalken*/ 119 | @-moz-document url-prefix(http:\/\/),url-prefix(https:\/\/) { 120 | scrollbar { 121 | -moz-appearance: none !important; 122 | background: rgb(0,255,0) !important; 123 | } 124 | thumb,scrollbarbutton { 125 | -moz-appearance: none !important; 126 | background-color: rgb(0,0,255) !important; 127 | } 128 | 129 | thumb:hover,scrollbarbutton:hover { 130 | -moz-appearance: none !important; 131 | background-color: rgb(255,0,0) !important; 132 | } 133 | 134 | scrollbarbutton { 135 | display: none !important; 136 | } 137 | 138 | scrollbar[orient="vertical"] { 139 | min-width: 15px !important; 140 | } 141 | } 142 | /**/ 143 | .scrollbar-container { 144 | overflow: auto; 145 | } 146 | ::-webkit-scrollbar { 147 | background: transparent; 148 | } 149 | ::-webkit-scrollbar-thumb { 150 | background-color: rgba(0, 0, 0, 0.2); 151 | border: solid whiteSmoke 4px; 152 | } 153 | ::-webkit-scrollbar-thumb:hover { 154 | background-color: rgba(0, 0, 0, 0.3); 155 | } 156 | 157 | .ivu-btn a { 158 | color: #f3f3f3; 159 | } 160 | 161 | .ivu-btn a:hover { 162 | color: #ffffff; 163 | } 164 | 165 | .ivu-table-tip table td { 166 | text-align: center; 167 | display: flex; 168 | justify-content: center; 169 | } 170 | 171 | 172 | table td .ivu-table-cell { 173 | width: 100%; 174 | height: 1.11rem; 175 | } 176 | 177 | table .ivu-table-cell .ivu-icon { 178 | font-size: 1rem 179 | font-weight: bolder 180 | } 181 | 182 | 183 | @media screen and (max-width: 666px) { 184 | 185 | /* Force table to not be like tables anymore */ 186 | table, thead, tbody, th, td, tr { 187 | display: block; 188 | } 189 | 190 | table { 191 | border: 0; 192 | } 193 | 194 | table caption { 195 | font-size: 1.3em; 196 | } 197 | 198 | table thead { 199 | border: none; 200 | clip: rect(0 0 0 0); 201 | height: 1px; 202 | margin: -1px; 203 | overflow: hidden; 204 | padding: 0; 205 | position: absolute; 206 | width: 1px; 207 | } 208 | 209 | table tr { 210 | border-bottom: 4px solid #efefef; 211 | // margin-bottom: .625em; 212 | } 213 | 214 | table td { 215 | display: flex; 216 | align-items: center; 217 | } 218 | 219 | table td .ivu-table-cell > div { 220 | display: flex; 221 | font-size: .8em; 222 | text-align: right; 223 | align-items: flex-start; 224 | justify-content: flex-start; 225 | } 226 | 227 | table td:last-of-type .ivu-table-cell { 228 | padding: 0 229 | margin 0 230 | } 231 | 232 | table td:last-of-type .ivu-table-cell > div { 233 | display flex 234 | align-items flex-end 235 | justify-content flex-end 236 | padding: 0 237 | margin 0 238 | } 239 | 240 | table td:last-of-type .ivu-table-cell > div button { 241 | width: 3.33rem; 242 | } 243 | 244 | 245 | table td:last-of-type .ivu-table-cell > div i { 246 | font-size: 3rem 247 | height 1.66rem 248 | width 1.66rem 249 | display: flex; 250 | align-items: center; 251 | justify-content: center; 252 | border-radius: 50% 253 | box-shadow 0 1px 2px 0 rgba(60,64,67,0.302), 0 1px 3px 1px rgba(60,64,67,0.149) 254 | } 255 | 256 | table td .ivu-table-cell > div::before { 257 | /* 258 | * aria-label has no advantage, it won't be read inside a table 259 | content: attr(aria-label); 260 | */ 261 | content: attr(data-label); 262 | font-weight: bold; 263 | text-transform: uppercase; 264 | display: inline-block; 265 | min-width: 5rem; 266 | text-align: left; 267 | margin-right: 1rem; 268 | } 269 | 270 | table td .ivu-table-cell > div span { 271 | margin-left: auto; 272 | } 273 | 274 | table td:last-child { 275 | border-bottom: 0; 276 | } 277 | 278 | .ivu-table th, .ivu-table td { 279 | height: auto; 280 | padding: .5rem 0; 281 | } 282 | 283 | .ivu-page-total { 284 | display: block; 285 | text-align: center; 286 | margin-bottom: .5rem; 287 | } 288 | 289 | } -------------------------------------------------------------------------------- /template/src/client/assets/theme.less: -------------------------------------------------------------------------------- 1 | @import '~iview/src/styles/index.less'; 2 | 3 | // Prefix 4 | @css-prefix : ivu-; 5 | @css-prefix-iconfont : ivu-icon; 6 | 7 | // Color 8 | @primary-color : #3399ff; 9 | @info-color : #2db7f5; 10 | @success-color : #39b54a; 11 | @warning-color : #ff9900; 12 | @error-color : #ff3300; 13 | @link-color : #3399ff; 14 | @link-hover-color : tint(@link-color, 20%); 15 | @link-active-color : shade(@link-color, 5%); 16 | @selected-color : fade(@primary-color, 90%); 17 | @tooltip-color : #fff; 18 | @subsidiary-color : #9ea7b4; 19 | @rate-star-color : #f5a623; 20 | 21 | // Base 22 | @body-background : #fff; 23 | // @font-family : "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; 24 | @font-family : "Roboto",Arial,sans-serif; 25 | @code-family : Consolas,Menlo,Courier,monospace; 26 | @title-color : #464c5b; 27 | @text-color : #657180; 28 | @font-size-base : 14px; 29 | @font-size-small : 12px; 30 | @line-height-base : 1.5; 31 | @line-height-computed : floor((@font-size-base * @line-height-base)); 32 | // @border-radius-base : 6px; 33 | // @border-radius-small : 4px; 34 | @border-radius-base : 0; 35 | @border-radius-small : 0; 36 | @cursor-disabled : not-allowed; 37 | 38 | // Border color 39 | @border-color-base : #d7dde4; // outside 40 | @border-color-split : #e3e8ee; // inside 41 | 42 | // Background color 43 | @background-color-base : #f7f7f7; // base 44 | @background-color-select-hover: @input-disabled-bg; 45 | @tooltip-bg : rgba(70, 76, 91, .9); 46 | @head-bg : #f9fafc; 47 | @table-thead-bg : #f5f7f9; 48 | @table-td-stripe-bg : #f5f7f9; 49 | @table-td-hover-bg : #ebf7ff; 50 | @table-td-highlight-bg : #ebf7ff; 51 | @menu-dark-active-bg : #313540; 52 | @date-picker-cell-hover-bg : #e1f0fe; 53 | 54 | // Shadow 55 | @shadow-color : rgba(0, 0, 0, .2); 56 | @shadow-base : @shadow-down; 57 | @shadow-card : 0 1px 1px 0 rgba(0,0,0,.1); 58 | @shadow-up : 0 -1px 6px @shadow-color; 59 | @shadow-down : 0 1px 6px @shadow-color; 60 | @shadow-left : -1px 0 6px @shadow-color; 61 | @shadow-right : 1px 0 6px @shadow-color; 62 | 63 | // Button 64 | @btn-font-weight : normal; 65 | @btn-padding-base : 6px 15px; 66 | @btn-padding-large : 6px 15px 7px 15px; 67 | @btn-padding-small : 2px 7px; 68 | @btn-font-size : 12px; 69 | @btn-font-size-large : 14px; 70 | // @btn-border-radius : 4px; 71 | // @btn-border-radius-small: 3px; 72 | @btn-border-radius : 0; 73 | @btn-border-radius-small: 0; 74 | @btn-group-border : shade(@primary-color, 5%); 75 | 76 | @btn-disable-color : #c3cbd6; 77 | @btn-disable-bg : @background-color-base; 78 | @btn-disable-border : @border-color-base; 79 | 80 | @btn-default-color : @text-color; 81 | @btn-default-bg : @background-color-base; 82 | @btn-default-border : @border-color-base; 83 | 84 | @btn-primary-color : #fff; 85 | @btn-primary-bg : @primary-color; 86 | 87 | @btn-ghost-color : @text-color; 88 | @btn-ghost-bg : transparent; 89 | @btn-ghost-border : @border-color-base; 90 | 91 | @btn-circle-size : 32px; 92 | @btn-circle-size-large : 36px; 93 | @btn-circle-size-small : 24px; 94 | 95 | // Layout and Grid 96 | @grid-columns : 24; 97 | @grid-gutter-width : 0; 98 | 99 | // Legend 100 | @legend-color : #999; 101 | 102 | // Input 103 | @input-height-base : 32px; 104 | @input-height-large : 36px; 105 | @input-height-small : 24px; 106 | 107 | @input-padding-horizontal : 7px; 108 | @input-padding-vertical-base : 4px; 109 | @input-padding-vertical-small: 1px; 110 | @input-padding-vertical-large: 6px; 111 | 112 | @input-placeholder-color : @btn-disable-color; 113 | @input-color : @text-color; 114 | @input-border-color : @border-color-base; 115 | @input-bg : #fff; 116 | 117 | @input-hover-border-color : @primary-color; 118 | @input-focus-border-color : @primary-color; 119 | @input-disabled-bg : #f3f3f3; 120 | 121 | // Tag 122 | @tag-font-size : 12px; 123 | 124 | // Media queries breakpoints 125 | // Extra small screen / phone 126 | @screen-xs : 480px; 127 | @screen-xs-min : @screen-xs; 128 | @screen-xs-max : (@screen-xs-min - 1); 129 | 130 | // Small screen / tablet 131 | @screen-sm : 768px; 132 | @screen-sm-min : @screen-sm; 133 | @screen-sm-max : (@screen-sm-min - 1); 134 | 135 | // Medium screen / desktop 136 | @screen-md : 992px; 137 | @screen-md-min : @screen-md; 138 | @screen-md-max : (@screen-md-min - 1); 139 | 140 | // Large screen / wide desktop 141 | @screen-lg : 1200px; 142 | @screen-lg-min : @screen-lg; 143 | @screen-lg-max : (@screen-lg-min - 1); 144 | 145 | // Z-index 146 | @zindex-spin : 8; 147 | @zindex-affix : 10; 148 | @zindex-back-top : 10; 149 | @zindex-select : 900; 150 | @zindex-modal : 1000; 151 | @zindex-message : 1010; 152 | @zindex-notification : 1010; 153 | @zindex-tooltip : 1060; 154 | @zindex-loading-bar : 2000; 155 | 156 | // Animation 157 | @animation-time : .3s; 158 | @transition-time : .2s; 159 | @ease-in-out : ease-in-out; 160 | 161 | // Slider 162 | @slider-color : tint(@primary-color, 20%); 163 | @slider-height : 4px; 164 | @slider-margin : 16px 0; 165 | @slider-button-wrap-size : 18px; 166 | @slider-button-wrap-offset : -4px; 167 | @slider-disabled-color : #ccc; 168 | -------------------------------------------------------------------------------- /template/src/client/components/DataCard.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 62 | 63 | 64 | 174 | -------------------------------------------------------------------------------- /template/src/client/components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | The components directory contains your Vue.js Components. 4 | Nuxt.js doesn't supercharge these components. 5 | 6 | **This directory is not required, you can delete it if you don't want to use it.** 7 | -------------------------------------------------------------------------------- /template/src/client/components/SelectCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | 32 | 33 | -------------------------------------------------------------------------------- /template/src/client/components/layout/Footer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /template/src/client/components/layout/SideNav.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 47 | 48 | 69 | -------------------------------------------------------------------------------- /template/src/client/components/layout/TopNav.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 52 | 53 | -------------------------------------------------------------------------------- /template/src/client/components/select/Permission.vue: -------------------------------------------------------------------------------- 1 | 6 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /template/src/client/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | This directory contains your Application Layouts. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/views#layouts 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /template/src/client/layouts/dashboard.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 81 | 82 | 83 | 130 | -------------------------------------------------------------------------------- /template/src/client/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | -------------------------------------------------------------------------------- /template/src/client/middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | This directory contains your Application Middleware. 4 | The middleware lets you define custom function to be ran before rendering a page or a group of pages (layouts). 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing#middleware 8 | 9 | **This directory is not required, you can delete it if you don't want to use it.** 10 | -------------------------------------------------------------------------------- /template/src/client/middleware/anonymous.js: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect, error }) { 2 | // ensure user is authenticated 3 | if (store.state.auth.user) { 4 | // // throw 5 | // error({ 6 | // message: 'Access denied', 7 | // statusCode: 403 8 | // }) 9 | // // or redirect 10 | redirect('/messages/compose') 11 | } 12 | } -------------------------------------------------------------------------------- /template/src/client/middleware/authenticated.js: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect, error }) { 2 | if (!store.state.auth.user) { 3 | // // throw 4 | // error({ 5 | // message: 'Access denied', 6 | // statusCode: 403 7 | // }) 8 | // // or redirect 9 | redirect('/') 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /template/src/client/middleware/crash.js: -------------------------------------------------------------------------------- 1 | 2 | export default function ({ store, error, redirect }) { 3 | if (store.state.crash.error) { 4 | // integrate your error reporter 5 | // or throw error 6 | // or redirect 7 | console.log('@@@@@crashed...', store.state.crash.error) 8 | // error(store.state.crash.error) 9 | // redirect('/') 10 | } 11 | } -------------------------------------------------------------------------------- /template/src/client/modules/less.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | const loader = { 3 | loader: 'less-loader', 4 | options: { 5 | sourceMap: true, 6 | javascriptEnabled: true // <- enable this option 7 | } 8 | } 9 | this.extendBuild((config) => { 10 | const lessLoaders = config.module.rules.filter(({ test = '' }) => { 11 | return ['/\\.less$/'].indexOf(test.toString()) !== -1 12 | }) 13 | for (const lessLoader of lessLoaders) { 14 | for (const rule of lessLoader.oneOf) { 15 | rule.use.push(loader) 16 | } 17 | } 18 | }) 19 | } -------------------------------------------------------------------------------- /template/src/client/modules/livescript.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | // Add .livescript extension for store, middleware and more 3 | this.nuxt.options.extensions.push('livescript') 4 | // Extend build 5 | const livescriptLoader = { 6 | test: /\.livescript$/, 7 | loader: 'livescript-loader' 8 | } 9 | this.extendBuild((config) => { 10 | // Add livescriptScruot loader 11 | config.module.rules.push(livescriptLoader) 12 | // Add .livescript extension in webpack resolve 13 | if (config.resolve.extensions.indexOf('.livescript') === -1) { 14 | config.resolve.extensions.push('.livescript') 15 | } 16 | }) 17 | } -------------------------------------------------------------------------------- /template/src/client/modules/pug.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | // Add .pug extension for store, middleware and more 3 | this.nuxt.options.extensions.push('pug') 4 | // Extend build 5 | const pugLoader = { 6 | test: /\.pug$/, 7 | loader: 'pug-plain-loader', 8 | options: { 9 | data: {} 10 | } 11 | } 12 | this.extendBuild((config) => { 13 | // Add pug loader 14 | config.module.rules.push(pugLoader) 15 | // Add .pug extension in webpack resolve 16 | if (config.resolve.extensions.indexOf('.pug') === -1) { 17 | config.resolve.extensions.push('.pug') 18 | } 19 | }) 20 | 21 | } -------------------------------------------------------------------------------- /template/src/client/pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the .vue files inside this directory and create the router of your application. 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing 8 | -------------------------------------------------------------------------------- /template/src/client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | 130 | -------------------------------------------------------------------------------- /template/src/client/pages/reports.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /template/src/client/plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | This directory contains your Javascript plugins that you want to run before instantiating the root vue.js application. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/plugins 7 | 8 | **Included plugins.** 9 | - `iview` for integrating iview ui components. Set default ui locale on this file. 10 | - `scrollto` for integrating `vue-scrollto`, a handy directive client side only. 11 | - `feathers` for integrating feathersjs client into nuxt, client side only. 12 | 13 | -------------------------------------------------------------------------------- /template/src/client/plugins/async-computed.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import AsyncComputed from 'vue-async-computed' 3 | 4 | Vue.use(AsyncComputed) -------------------------------------------------------------------------------- /template/src/client/plugins/casl.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { abilitiesPlugin } from '@casl/vue' 3 | 4 | import { abilityPlugin, abilityInstance } from '~utils/store/plugins/casl' 5 | 6 | 7 | export default function (ctx) { 8 | 9 | const { app, store, isHMR, isDev } = ctx 10 | 11 | // register vuex store plugin for casl 12 | abilityPlugin(store) 13 | 14 | // register vue ui plugin for casl 15 | Vue.use(abilitiesPlugin, abilityInstance) 16 | 17 | // console.log('casl:vue', '@plugins/casl initialized plugin ') 18 | 19 | } -------------------------------------------------------------------------------- /template/src/client/plugins/crash.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import { crashPlugin } from '~utils/store/plugins/crash' 4 | 5 | 6 | export default function (ctx) { 7 | 8 | const { app, store, isHMR, isDev } = ctx 9 | 10 | // register vuex store plugin for crash 11 | crashPlugin(store) 12 | 13 | // console.log(' @plugins/crash initialized plugin ') 14 | 15 | } -------------------------------------------------------------------------------- /template/src/client/plugins/data.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import DataCard from '~/components/DataCard.vue' 4 | import DataTable from '~/components/DataTable.vue' 5 | 6 | Vue.component('DataCard', DataCard) 7 | Vue.component('DataTable', DataTable) -------------------------------------------------------------------------------- /template/src/client/plugins/feathers.js: -------------------------------------------------------------------------------- 1 | import { initClient } from '~utils' 2 | 3 | export default async (ctx, inject) => { 4 | let api // feathers server or client instance 5 | if(process.server) { 6 | // use feathers server instance for SSR 7 | api = ctx.req.api 8 | } else { 9 | // use feathers rest client instance on the browser 10 | api = await initClient(ctx, ctx.nuxtState.config) 11 | 12 | //attach app to window for easy debugging from browser console 13 | if(ctx.isDev) window.app = ctx.app 14 | 15 | // wait until nuxt is initialized 16 | window.onNuxtReady(() => { 17 | 18 | //nuxtReady event fires for HMR as well 19 | //dont reinitialize feathersClient during HRM 20 | if (ctx.isHMR) return 21 | ctx.nuxtState.services.forEach( path => api.service(path) ) 22 | }) 23 | } 24 | 25 | // inject feathers app to vue context 26 | inject('api', api) 27 | 28 | if(process.server) { 29 | ctx.req.api.info(`DONE @plugins/feathers initialized`) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /template/src/client/plugins/fuzzysort.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import fuzzysort from 'fuzzysort' 3 | 4 | Vue.use({ 5 | install (Vue, options) { 6 | Vue.mixin({ 7 | created: function () { 8 | this.$fuzzysort = fuzzysort 9 | } 10 | }) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /template/src/client/plugins/iview.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import iView from 'iview' 3 | import locale from 'iview/dist/locale/en-US' 4 | 5 | Vue.use(iView, { locale }) 6 | 7 | Vue.use({ 8 | install (Vue, options) { 9 | Vue.mixin({ 10 | created: function () { 11 | this.$Notice = iView.Notice 12 | this.$Message = iView.Message 13 | } 14 | }) 15 | } 16 | }) 17 | 18 | //import 'iview/dist/styles/iview.css' -------------------------------------------------------------------------------- /template/src/client/plugins/media-query.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import {client, server} from '~/utils/vue-media-query-mixin'; 3 | 4 | 5 | if(process.client) { 6 | // use vue-media-query-mixin that listens to window resize events 7 | Vue.use(client, {framework:'vuetify'}); 8 | } else { 9 | // use vue-media-query-mixin that doesn't depend on window object 10 | Vue.use(server, {framework:'vuetify'}); 11 | } -------------------------------------------------------------------------------- /template/src/client/plugins/routersync.js: -------------------------------------------------------------------------------- 1 | import { sync } from 'vuex-router-sync' 2 | 3 | export default ({app: {store, router}}) => { 4 | sync(store, router) 5 | // console.error('~plugins/vue-router-sync', store, router) 6 | } -------------------------------------------------------------------------------- /template/src/client/plugins/scrollto.js: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue' 3 | import VueScrollTo from 'vue-scrollto' 4 | 5 | Vue.use(VueScrollTo) 6 | 7 | // You can also pass in the default options 8 | // Vue.use(VueScrollTo, { 9 | // container: "body", 10 | // duration: 500, 11 | // easing: "ease", 12 | // offset: 0, 13 | // cancelable: true, 14 | // onDone: false, 15 | // onCancel: false, 16 | // x: false, 17 | // y: true 18 | // }) -------------------------------------------------------------------------------- /template/src/client/plugins/storyboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import * as storyboard from 'storyboard' 4 | 5 | export default async function (ctx) { 6 | 7 | Vue.use({ 8 | install(Vue, options) { 9 | Vue.mixin({ 10 | created: function () { 11 | // access feathersClient on any component 12 | this.$storyboard = storyboard 13 | } 14 | }) 15 | } 16 | }) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /template/src/client/plugins/vuebar.js: -------------------------------------------------------------------------------- 1 | import Vuebar from 'vuebar' 2 | import Vue from 'vue' 3 | 4 | Vue.use(Vuebar) -------------------------------------------------------------------------------- /template/src/client/static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | This directory contains your static files. 4 | Each file inside this directory is mapped to /. 5 | 6 | Example: /static/robots.txt is mapped as /robots.txt. 7 | 8 | More information about the usage of this directory in the documentation: 9 | https://nuxtjs.org/guide/assets#static 10 | 11 | **This directory is not required, you can delete it if you don't want to use it.** 12 | -------------------------------------------------------------------------------- /template/src/client/static/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Console 8 | 9 | 10 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 |
66 |
67 |
+
68 |
69 | 75 | 81 | 87 |
88 |
89 |
90 | 91 | 92 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /template/src/client/static/css/assets/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /template/src/client/static/css/assets/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /template/src/client/static/css/assets/out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /template/src/client/static/css/assets/prompt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | path3691 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /template/src/client/static/css/assets/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /template/src/client/static/css/fab.css: -------------------------------------------------------------------------------- 1 | .fab .buttons { 2 | position: absolute; 3 | bottom: 16px; 4 | right: 16px; 5 | text-align: center; 6 | } 7 | .fab .trigger, .fab .action { 8 | border-radius: 50%; 9 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25); 10 | cursor: pointer; 11 | } 12 | .fab .trigger:active, .fab .action:active { 13 | opacity: .7; 14 | } 15 | .fab .trigger { 16 | position: relative; 17 | background: #2c4; 18 | color: #fff; 19 | height: 56px; 20 | width: 56px; 21 | line-height: 56px; 22 | vertical-align: middle; 23 | font-size: 1.5em; 24 | z-index: 1; 25 | } 26 | .fab .action { 27 | position: absolute; 28 | top: 0; 29 | margin: 0 8px; 30 | background: #fff; 31 | color: #333; 32 | height: 40px; 33 | width: 40px; 34 | line-height: 40px; 35 | transition: -webkit-transform 0.4s ease; 36 | transition: transform 0.4s ease; 37 | transition: transform 0.4s ease, -webkit-transform 0.4s ease; 38 | } 39 | .fab .actions, .fab .overlay { 40 | opacity: 0; 41 | transition: opacity 0.4s ease; 42 | } 43 | .fab.open .overlay { 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | right: 0; 48 | bottom: 0; 49 | background: #000; 50 | opacity: .7; 51 | } 52 | .fab.open .actions { 53 | opacity: 1; 54 | } 55 | .fab.open .actions .action:nth-child(1) { 56 | -webkit-transform: translateY(-48px); 57 | transform: translateY(-48px); 58 | } 59 | .fab.open .actions .action:nth-child(2) { 60 | -webkit-transform: translateY(-96px); 61 | transform: translateY(-96px); 62 | } 63 | .fab.open .actions .action:nth-child(3) { 64 | -webkit-transform: translateY(-144px); 65 | transform: translateY(-144px); 66 | } 67 | .fab.open .actions .action:nth-child(4) { 68 | -webkit-transform: translateY(-192px); 69 | transform: translateY(-192px); 70 | } 71 | .fab.open .tooltip { 72 | opacity: 1; 73 | -webkit-transform: translateY(-50%) scale(1); 74 | transform: translateY(-50%) scale(1); 75 | right: 64px; 76 | transition-delay: 0.2s; 77 | } 78 | .fab .tooltip { 79 | position: absolute; 80 | top: 50%; 81 | right: 0; 82 | width: 50vw; 83 | -webkit-transform: translateY(-50%) scale(0); 84 | transform: translateY(-50%) scale(0); 85 | -webkit-transform-origin: right center 0; 86 | transform-origin: right center 0; 87 | text-align: right; 88 | transition: all 0.4s ease; 89 | opacity: 0; 90 | color: #fff; 91 | } 92 | -------------------------------------------------------------------------------- /template/src/client/static/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 | 78 | 79 | 80 | 81 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
177 |
178 |
179 |
+
180 |
181 | 187 | 193 | 199 |
200 |
201 |
202 | 203 | 204 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /template/src/client/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathers-nuxt/template-app/11a5ba02d64f23fe056be874a9246eb2138b5166/template/src/client/static/icon.png -------------------------------------------------------------------------------- /template/src/client/static/js/EventSource.js: -------------------------------------------------------------------------------- 1 | ;(function (global) { 2 | 3 | if ("EventSource" in window) return; 4 | 5 | var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; 6 | 7 | var EventSource = function (url) { 8 | var eventsource = this, 9 | interval = 500, // polling interval 10 | lastEventId = null, 11 | cache = ''; 12 | 13 | if (!url || typeof url != 'string') { 14 | throw new SyntaxError('Not enough arguments'); 15 | } 16 | 17 | this.URL = url; 18 | this.readyState = this.CONNECTING; 19 | this._pollTimer = null; 20 | this._xhr = null; 21 | 22 | function pollAgain() { 23 | eventsource._pollTimer = setTimeout(function () { 24 | poll.call(eventsource); 25 | }, interval); 26 | } 27 | 28 | function poll() { 29 | try { // force hiding of the error message... insane? 30 | if (eventsource.readyState == eventsource.CLOSED) return; 31 | 32 | var xhr = new XMLHttpRequest(); 33 | xhr.open('GET', eventsource.URL, true); 34 | xhr.setRequestHeader('Accept', 'text/event-stream'); 35 | xhr.setRequestHeader('Cache-Control', 'no-cache'); 36 | 37 | // we must make use of this on the server side if we're working with Android - because they don't trigger 38 | // readychange until the server connection is closed 39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 40 | 41 | if (lastEventId != null) xhr.setRequestHeader('Last-Event-ID', lastEventId); 42 | cache = ''; 43 | 44 | xhr.timeout = 50000; 45 | xhr.onreadystatechange = function () { 46 | if ((this.readyState == 3 || this.readyState == 4) && this.status == 200) { 47 | // on success 48 | if (eventsource.readyState == eventsource.CONNECTING) { 49 | eventsource.readyState = eventsource.OPEN; 50 | eventsource.dispatchEvent('open', { type: 'open' }); 51 | } 52 | 53 | // process this.responseText 54 | var parts = this.responseText.substr(cache.length).split("\n"), 55 | data = [], 56 | i = 0, 57 | line = ''; 58 | 59 | cache = this.responseText; 60 | 61 | // TODO handle 'event' (for buffer name), retry 62 | for (; i < parts.length; i++) { 63 | line = parts[i].replace(reTrim, ''); 64 | if (line.indexOf('data') == 0) { 65 | data.push(line.replace(/data:?\s*/, '')); 66 | } else if (line.indexOf('id:') == 0) { 67 | lastEventId = line.replace(/id:?\s*/, ''); 68 | } else if (line.indexOf('id') == 0) { // this resets the id 69 | lastEventId = null; 70 | } else if (line == '') { 71 | if (data.length) { 72 | var event = new MessageEvent(data.join('\n'), eventsource.url, lastEventId); 73 | eventsource.dispatchEvent('message', event); 74 | data = []; 75 | } 76 | } 77 | } 78 | 79 | if (this.readyState == 4) pollAgain(); 80 | // don't need to poll again, because we're long-loading 81 | } else if (eventsource.readyState !== eventsource.CLOSED) { 82 | if (this.readyState == 4) { // and some other status 83 | // dispatch error 84 | eventsource.readyState = eventsource.CONNECTING; 85 | eventsource.dispatchEvent('error', { type: 'error' }); 86 | pollAgain(); 87 | } else if (this.readyState == 0) { // likely aborted 88 | pollAgain(); 89 | } 90 | } 91 | }; 92 | 93 | xhr.send(); 94 | 95 | setTimeout(function () { 96 | if (true || xhr.readyState == 3) xhr.abort(); 97 | }, xhr.timeout); 98 | 99 | eventsource._xhr = xhr; 100 | 101 | } catch (e) { // in an attempt to silence the errors 102 | eventsource.dispatchEvent('error', { type: 'error', data: e.message }); // ??? 103 | } 104 | }; 105 | 106 | poll(); // init now 107 | }; 108 | 109 | EventSource.prototype = { 110 | close: function () { 111 | // closes the connection - disabling the polling 112 | this.readyState = this.CLOSED; 113 | clearInterval(this._pollTimer); 114 | this._xhr.abort(); 115 | }, 116 | CONNECTING: 0, 117 | OPEN: 1, 118 | CLOSED: 2, 119 | dispatchEvent: function (type, event) { 120 | var handlers = this['_' + type + 'Handlers']; 121 | if (handlers) { 122 | for (var i = 0; i < handlers.length; i++) { 123 | handlers.call(this, event); 124 | } 125 | } 126 | 127 | if (this['on' + type]) { 128 | this['on' + type].call(this, event); 129 | } 130 | }, 131 | addEventListener: function (type, handler) { 132 | if (!this['_' + type + 'Handlers']) { 133 | this['_' + type + 'Handlers'] = []; 134 | } 135 | 136 | this['_' + type + 'Handlers'].push(handler); 137 | }, 138 | removeEventListener: function () { 139 | // TODO 140 | }, 141 | onerror: null, 142 | onmessage: null, 143 | onopen: null, 144 | readyState: 0, 145 | URL: '' 146 | }; 147 | 148 | var MessageEvent = function (data, origin, lastEventId) { 149 | this.data = data; 150 | this.origin = origin; 151 | this.lastEventId = lastEventId || ''; 152 | }; 153 | 154 | MessageEvent.prototype = { 155 | data: null, 156 | type: 'message', 157 | lastEventId: '', 158 | origin: '' 159 | }; 160 | 161 | if ('module' in global) module.exports = EventSource; 162 | global.EventSource = EventSource; 163 | 164 | })(this); -------------------------------------------------------------------------------- /template/src/client/static/js/console.ls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathers-nuxt/template-app/11a5ba02d64f23fe056be874a9246eb2138b5166/template/src/client/static/js/console.ls -------------------------------------------------------------------------------- /template/src/client/static/js/copy.js: -------------------------------------------------------------------------------- 1 | var copy = (function () { 2 | function select(element) { 3 | var selectedText; 4 | 5 | if (element.nodeName === 'SELECT') { 6 | element.focus(); 7 | 8 | selectedText = element.value; 9 | } else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { 10 | element.focus(); 11 | element.setSelectionRange(0, element.value.length); 12 | 13 | selectedText = element.value; 14 | } else { 15 | if (element.hasAttribute('contenteditable')) { 16 | element.focus(); 17 | } 18 | 19 | var selection = window.getSelection(); 20 | var range = document.createRange(); 21 | 22 | range.selectNodeContents(element); 23 | selection.removeAllRanges(); 24 | selection.addRange(range); 25 | 26 | selectedText = selection.toString(); 27 | } 28 | 29 | return selectedText; 30 | } 31 | 32 | var isRTL = false; 33 | 34 | function copy(text) { 35 | var fakeElem = document.createElement('textarea'); 36 | // Prevent zooming on iOS 37 | fakeElem.style.fontSize = '12pt'; 38 | // Reset box model 39 | fakeElem.style.border = '0'; 40 | fakeElem.style.padding = '0'; 41 | fakeElem.style.margin = '0'; 42 | // Move element out of screen horizontally 43 | fakeElem.style.position = 'absolute'; 44 | fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; 45 | // Move element to the same position vertically 46 | var yPosition = window.pageYOffset || document.documentElement.scrollTop; 47 | fakeElem.addEventListener('focus', window.scrollTo(0, yPosition)); 48 | fakeElem.style.top = yPosition + 'px'; 49 | 50 | fakeElem.setAttribute('readonly', ''); 51 | fakeElem.value = text; 52 | 53 | document.body.appendChild(fakeElem); 54 | 55 | selectedText = select(fakeElem); 56 | try { 57 | return document.execCommand('copy'); 58 | } catch (err) {} 59 | 60 | return false; 61 | } 62 | 63 | return copy; 64 | })(); -------------------------------------------------------------------------------- /template/src/client/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathers-nuxt/template-app/11a5ba02d64f23fe056be874a9246eb2138b5166/template/src/client/static/logo.png -------------------------------------------------------------------------------- /template/src/client/store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | This directory contains your Vuex Store files. 4 | Vuex Store option is implemented in the Nuxt.js framework. 5 | Creating a index.js file in this directory activate the option in the framework automatically. 6 | 7 | Since there are multiple files in this directory, vuex store is activated by nuxt. 8 | As `index.js` does not instanciate vuex, nuxt sets up vuex store in *modules mode*. 9 | 10 | *auth module* is activated since `auth.js` file is included in this directory. 11 | Peek inside to see how you can activate your store modules declare in `api/store/` directories. 12 | 13 | More information about the usage of this directory in the documentation: 14 | https://nuxtjs.org/guide/vuex-store 15 | 16 | **This directory is not required, you can delete it if you don't want to use it.** 17 | -------------------------------------------------------------------------------- /template/src/client/store/auth.js: -------------------------------------------------------------------------------- 1 | import auth from '~utils/store/modules/auth' 2 | 3 | // store modules defined in ~utils/store/modules, declared in ~store 4 | 5 | export default auth -------------------------------------------------------------------------------- /template/src/client/store/crash.js: -------------------------------------------------------------------------------- 1 | import crash from '~utils/store/modules/crash' 2 | 3 | // store modules defined in ~utils/store/modules, declared in ~store 4 | 5 | export default crash -------------------------------------------------------------------------------- /template/src/client/store/index.js: -------------------------------------------------------------------------------- 1 | import { initAuth } from '~utils' 2 | import { sync } from 'vuex-router-sync' 3 | 4 | export const actions = { 5 | 6 | // this function runs on server side only 7 | async nuxtServerInit (store, ctx) { 8 | 9 | // const { commit, dispatch } = store 10 | const { req, beforeNuxtRender } = ctx 11 | 12 | req.api.log({ level: 'silly', message: `${req.method} HTTP ${req.httpVersion}` }) 13 | req.api.info(`INIT @nuxtServerInit ${req.url}` ) 14 | 15 | // synchronise vue router state with vuex store state 16 | sync(ctx.app.store, ctx.app.router) 17 | 18 | ctx.app.store.commit("config/setLogo", "/logo.png") 19 | 20 | req.api.info(`FORK @nuxtServerInit CALL initAuth` ) 21 | 22 | // retrive user from cookie token into vuex state 23 | await initAuth({ req, commit: ctx.app.store.commit }) 24 | 25 | const config = { 26 | protocol: req.api.get('protocol'), 27 | host: req.api.get('domain'), 28 | port: req.api.get('port') 29 | } 30 | 31 | // pass in params from server to client 32 | beforeNuxtRender(async ({ nuxtState }) => { 33 | nuxtState.config = config 34 | nuxtState.services = Object.keys(req.api.services) 35 | }) 36 | 37 | req.api.info(`DONE @nuxtServerInit ` ) 38 | } 39 | 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /template/src/client/store/lookup.js: -------------------------------------------------------------------------------- 1 | import lookup from '~utils/store/modules/lookup' 2 | 3 | // store modules defined in ~utils/store/modules, declared in ~store 4 | 5 | export default lookup -------------------------------------------------------------------------------- /template/src/client/store/network.js: -------------------------------------------------------------------------------- 1 | import network from '~utils/store/modules/network' 2 | 3 | // store modules defined in ~utils/store/modules, declared in ~store 4 | 5 | export default network -------------------------------------------------------------------------------- /template/src/client/utils/README.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | This directory contains a utilities necessary for integration of feathersjs with nuxt. 4 | 5 | The script creates an instance of feathers client targeted at either the browser or the server depending on whether rendering is serverside or not. 6 | 7 | The browser optimized client uses `socket.io` for *websockets tranport* and `cookie-storage` for persistence. 8 | The server optimized version on the other hand uses `axios` for *rest transport* and `localstorage-memory` for persistance. 9 | 10 | Accessible as `context.api` anywhere within client files. 11 | 12 | Each sub-directory inside `store` directory contains definitions for store modules. See included Auth module for inspiration on how to write custom store modules as state, actions and mutations. Actions define interface between the UI and feathersClient. 13 | 14 | More information about the usage of this directory in the documentation: 15 | https://nuxtjs.org/guide/assets#webpacked 16 | 17 | -------------------------------------------------------------------------------- /template/src/client/utils/hooks.js: -------------------------------------------------------------------------------- 1 | const openProfilerStory = (context) => { 2 | const {app, path, method, params} = context 3 | if (!params.query) { params.query = {} } 4 | app.storyboard.log = app.storyboard.mainStory.child({ 5 | src: 'client', title: `${method} /${path}`, level: 'DEBUG' 6 | }) 7 | params.query.storyId = app.storyboard.log.storyId 8 | context 9 | } 10 | 11 | const closeProfilerStory = (context) => { 12 | const {app} = context 13 | app.storyboard.log.close() 14 | context 15 | } 16 | 17 | const dispatchToVuex = (context) => { 18 | const {app} = context 19 | console.log(' catch api request error ', context.error) 20 | // app.app.store.commit('crash/setError', context.error) 21 | context 22 | } 23 | 24 | const discardUser = (context) => { 25 | // don't send user in request params while client side 26 | if(context.params) delete context.params.user 27 | // context.result = 'discardUser' 28 | context 29 | } 30 | 31 | const getFromCache = (context) => { 32 | // respond with content from local cache 33 | // context.result = 'discardUser' 34 | context 35 | } 36 | 37 | export default { 38 | before: { 39 | all: [ 40 | // openProfilerStory 41 | discardUser 42 | ], 43 | find: [], 44 | get: [ 45 | getFromCache 46 | ], 47 | create: [], 48 | update: [], 49 | patch: [], 50 | remove: [] 51 | }, 52 | after: { 53 | all: [], 54 | find: [], 55 | get: [], 56 | create: [], 57 | update: [], 58 | patch: [], 59 | remove: [] 60 | }, 61 | error: { 62 | all: [ 63 | dispatchToVuex 64 | ], 65 | find: [], 66 | get: [], 67 | create: [], 68 | update: [], 69 | patch: [], 70 | remove: [] 71 | }, 72 | finally: { 73 | all: [ 74 | // closeProfilerStory 75 | ], 76 | find: [], 77 | get: [], 78 | create: [], 79 | update: [], 80 | patch: [], 81 | remove: [] 82 | } 83 | }; -------------------------------------------------------------------------------- /template/src/client/utils/index.js: -------------------------------------------------------------------------------- 1 | export const hooks = require('./hooks').default 2 | export const initAuth = require('./initAuth').default 3 | export const initClient = require('./initClient').default -------------------------------------------------------------------------------- /template/src/client/utils/initAuth.js: -------------------------------------------------------------------------------- 1 | import decode from 'jwt-decode' 2 | const { AbilityBuilder } = require('@casl/ability') 3 | 4 | export default function(options) { 5 | const authDefaults = { 6 | commit: undefined, 7 | req: undefined, 8 | moduleName: 'auth', 9 | cookieName: 'feathers-jwt' 10 | } 11 | const { commit, req, moduleName, cookieName } = Object.assign({}, authDefaults, options) 12 | 13 | if (typeof commit !== 'function') { 14 | throw new Error('You must pass the `commit` function in the `initAuth` function options.') 15 | } 16 | if (!req) { 17 | throw new Error('You must pass the `req` object in the `initAuth` function options.') 18 | } 19 | 20 | const accessToken = readCookie(req.headers.cookie, cookieName) 21 | if(accessToken) { 22 | return req.api.authenticate('jwt', {})(req) 23 | .then(async (result = {}) => { 24 | if (result && result.success == true) { 25 | const {user, payload} = result.data 26 | user.profile = user.user 27 | const {token} = user 28 | delete user.user 29 | authorize(user) 30 | 31 | // // Since we are rendering on the server we have to pass the authenticated user 32 | // // from `req.user` as `params.user` to our services 33 | // const params = { 34 | // user, query: {} 35 | // }; 36 | // // Find the list of users 37 | // const users = await req.api.service('proxyshortcodes').find(params); 38 | 39 | commit(`${moduleName}/setAuthenticatePending`) 40 | commit(`${moduleName}/setAccessToken`, token) 41 | commit(`${moduleName}/setPayload`, payload) 42 | commit(`${moduleName}/setUser`, user) 43 | commit(`${moduleName}/unsetAuthenticatePending`) 44 | 45 | req.api.info(`DONE @initAuth:authenticate `) 46 | } else { 47 | // authentication failed 48 | req.api.warn(`FAIL @initAuth:authenticate`) 49 | if(result.challenge.code == 404) { 50 | commit(`${moduleName}/setAuthenticateError`, result) 51 | 52 | } 53 | } 54 | }).catch((error) => { 55 | // authentication request failed 56 | req.api.error(`FAIL @initAuth:authenticate request`) 57 | }) 58 | } else { 59 | req.api.warn(`BAIL @initAuth:authenticate cookie missing` ) 60 | } 61 | 62 | 63 | } 64 | 65 | // Reads and returns the contents of a cookie with the provided name. 66 | function readCookie(cookies, name) { 67 | if (!cookies) { 68 | return undefined 69 | } 70 | var nameEQ = name + '=' 71 | var ca = cookies.split(';') 72 | for (var i = 0; i < ca.length; i++) { 73 | var c = ca[i] 74 | while (c.charAt(0) === ' ') { 75 | c = c.substring(1, c.length) 76 | } 77 | if (c.indexOf(nameEQ) === 0) { 78 | return c.substring(nameEQ.length, c.length) 79 | } 80 | } 81 | return null 82 | } 83 | 84 | function getValidPayloadFromToken(token) { 85 | if (token) { 86 | try { 87 | var payload = decode(token) 88 | return payloadIsValid(payload) ? payload : undefined 89 | } catch (error) { 90 | return undefined 91 | } 92 | } 93 | return undefined 94 | } 95 | 96 | // Pass a decoded payload and it will return a boolean based on if it hasn't expired. 97 | function payloadIsValid(payload) { 98 | return payload && payload.exp * 1000 > new Date().getTime() 99 | } 100 | 101 | 102 | function defineRulesFor(user){ 103 | var ref$, rules, can, i$, len$, module, actions, j$, len1$, action; 104 | ref$ = AbilityBuilder.extract(), rules = ref$.rules, can = ref$.can; 105 | if (user.permissions && typeof user.permissions === 'object') { 106 | for (i$ = 0, len$ = (ref$ = Object.keys(user.permissions)).length; i$ < len$; ++i$) { 107 | module = ref$[i$]; 108 | actions = eval('(' + user.permissions[module] + ')'); 109 | for (j$ = 0, len1$ = actions.length; j$ < len1$; ++j$) { 110 | action = actions[j$]; 111 | can(action, module); 112 | } 113 | } 114 | } 115 | return rules; 116 | }; 117 | 118 | function authorize(user){ 119 | user.authorization = defineRulesFor(user); 120 | }; -------------------------------------------------------------------------------- /template/src/client/utils/initClient.js: -------------------------------------------------------------------------------- 1 | // import io from 'socket.io-client' 2 | 3 | // import socketio from '@feathersjs/socketio-client' 4 | 5 | 6 | // import { addListener } from 'storyboard'; 7 | // import wsClientListener from 'storyboard-listener-ws-client'; 8 | // import browserExtListener from 'storyboard-listener-browser-extension'; 9 | 10 | // import * as storyboard from 'storyboard'; 11 | 12 | // import AuthManagement from 'feathers-authentication-management/lib/client' 13 | 14 | import { hooks, clearCookie } from '~utils' 15 | 16 | import rest from '@feathersjs/rest-client' 17 | import feathers from '@feathersjs/feathers' 18 | import authentication from '@feathersjs/authentication-client' 19 | 20 | 21 | export default async function(ctx) { 22 | // initialize feathers rest client 23 | if(process.client) { // on browser pocess only 24 | 25 | const { app, nuxtState } = ctx 26 | const { protocol, host, port } = nuxtState.config 27 | const storage = window.localStorage 28 | 29 | const feathersClient = feathers() 30 | 31 | feathersClient.hooks(hooks) 32 | feathersClient.configure(rest(`${protocol}://${host}:${port}/api`).axios(app.$axios)) 33 | feathersClient.configure(authentication({ storage, service: 'logins', jwtStrategy: 'jwt', path: '/authentication' })) 34 | 35 | // automatically logout once jwt expires 36 | feathersClient.on('authenticated', ({user}) => setTimeout(feathersClient.logout, ( new Date(user.validTill) - Date.now() )) ) 37 | 38 | // automatically navigate to login page after logout 39 | feathersClient.on('logout', () => app.router.push({ path: '/' }) ) 40 | 41 | const deleteCookie = (name) => document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;' 42 | 43 | // logout if token invalidated 44 | if(app.store.state.auth.errorOnAuthenticate) { 45 | feathersClient.logout().then(() => { 46 | deleteCookie('feathers-jwt') 47 | app.store.commit(`auth/clearAuthenticateError`) 48 | }) 49 | } 50 | 51 | return feathersClient 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/auth/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async authenticate(store, credentials) { 3 | const { commit, state, dispatch } = store 4 | const feathersClient = this.app.api 5 | 6 | commit('setAuthenticatePending') 7 | 8 | if (state.errorOnAuthenticate) { 9 | commit('clearAuthenticateError') 10 | } 11 | 12 | try { 13 | const response = await feathersClient.authenticate(credentials) 14 | // Populate the user if the userService was provided 15 | // console.log('#######response', response) 16 | commit('setAccessToken', response.accessToken) 17 | if (state.userService && response.hasOwnProperty('user')) { 18 | await dispatch('populateUser', response.user) 19 | } 20 | return response 21 | } catch (error) { 22 | commit('setAuthenticateError', error) 23 | return Promise.reject(error) 24 | } finally { 25 | commit('unsetAuthenticatePending') 26 | } 27 | 28 | }, 29 | populateUser({ commit, state }, user) { 30 | commit('setUser', user) 31 | // const feathersClient = this.app.api 32 | // return feathersClient.service(state.userService) 33 | // .get(user.id) 34 | // .then(user => { 35 | // commit('setUser', user) 36 | // return user 37 | // }) 38 | }, 39 | async logout({ commit }) { 40 | commit('setLogoutPending') 41 | const feathersClient = this.app.api 42 | try { 43 | const response = feathersClient.logout() 44 | commit('logout') 45 | commit('unsetLogoutPending') 46 | console.log('commited logout') 47 | return response 48 | } catch (error) { 49 | console.error('auth/logout action error error', error) 50 | return Promise.reject(error) 51 | } 52 | } 53 | } 54 | 55 | 56 | 57 | // // Import the Feathers client module that we've created before 58 | // import api from 'src/api' 59 | 60 | // const auth = { 61 | 62 | // // keep track of the logged in user 63 | // user: null, 64 | 65 | // getUser() { 66 | // return this.user 67 | // }, 68 | 69 | // fetchUser (accessToken) { 70 | 71 | // return api.passport.verifyJWT(accessToken) 72 | // .then(payload => { 73 | // return api.service('users').get(payload.userId) 74 | // }) 75 | // .then(user => { 76 | // return Promise.resolve(user) 77 | // }) 78 | // }, 79 | 80 | // authenticate () { 81 | 82 | // return api.authenticate() 83 | // .then((response) => { 84 | // return this.fetchUser(response.accessToken) 85 | // }) 86 | // .then(user => { 87 | // this.user = user 88 | // return Promise.resolve(user) 89 | // }) 90 | // .catch((err) => { 91 | // this.user = null 92 | // return Promise.reject(err) 93 | // }) 94 | // }, 95 | 96 | // authenticated () { 97 | // return this.user != null 98 | // }, 99 | 100 | // signout () { 101 | 102 | // return api.logout() 103 | // .then(() => { 104 | // this.user = null 105 | // }) 106 | // .catch((err) => { 107 | // return Promise.reject(err) 108 | // }) 109 | // }, 110 | 111 | // onLogout (callback) { 112 | 113 | // api.on('logout', () => { 114 | // this.user = null 115 | // callback() 116 | // }) 117 | // }, 118 | 119 | // onAuthenticated (callback) { 120 | 121 | // api.on('authenticated', response => { 122 | // this.fetchUser(response.accessToken) 123 | // .then(user => { 124 | // this.user = user 125 | // callback(this.user) 126 | // }) 127 | // .catch((err) => { 128 | // callback(this.user) 129 | // }) 130 | // }) 131 | // }, 132 | 133 | // register (email, password) { 134 | // return api.service('users').create({ 135 | // email: email, 136 | // password: password 137 | // }) 138 | // }, 139 | 140 | // login (email, password) { 141 | // return api.authenticate({ 142 | // strategy: 'local', 143 | // email: email, 144 | // password: password 145 | // }) 146 | // } 147 | 148 | // } 149 | 150 | // export default auth -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/auth/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: require('./state').default, 3 | actions: require('./actions').default, 4 | mutations: require('./mutations').default 5 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/auth/mutations.js: -------------------------------------------------------------------------------- 1 | import serializeError from 'serialize-error' 2 | 3 | export default { 4 | 5 | setAccessToken(state, payload) { 6 | state.accessToken = payload 7 | }, 8 | setPayload(state, payload) { 9 | state.payload = payload 10 | }, 11 | setUser(state, payload) { 12 | state.user = payload 13 | }, 14 | 15 | setAuthenticatePending(state) { 16 | state.isAuthenticatePending = true 17 | }, 18 | unsetAuthenticatePending(state) { 19 | state.isAuthenticatePending = false 20 | }, 21 | setLogoutPending(state) { 22 | state.isLogoutPending = true 23 | }, 24 | unsetLogoutPending(state) { 25 | state.isLogoutPending = false 26 | }, 27 | 28 | setAuthenticateError(state, error) { 29 | state.errorOnAuthenticate = Object.assign({}, serializeError(error)) 30 | }, 31 | clearAuthenticateError(state) { 32 | state.errorOnAuthenticate = null 33 | }, 34 | setLogoutError(state, error) { 35 | state.errorOnLogout = Object.assign({}, serializeError(error)) 36 | }, 37 | clearLogoutError(state) { 38 | state.errorOnLogout = null 39 | }, 40 | 41 | logout(state) { 42 | state.payload = null 43 | state.accessToken = null 44 | if (state.user) { 45 | state.user = null 46 | } 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/auth/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | isSignUpPending: false, 4 | errorOnSignUp: null, 5 | errorOnSignIn: null, 6 | errorOnSignOut: null, 7 | 8 | 9 | accessToken: null, // The JWT 10 | payload: null, // The JWT payload 11 | isAuthenticatePending: false, 12 | isLogoutPending: false, 13 | errorOnAuthenticate: null, 14 | errorOnLogout: null, 15 | userService: 'api/proxyusers', 16 | redirectTo: '/messages/compose', 17 | user: null 18 | } 19 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/crash/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setError({ commit }, error) { 3 | commit('setError', error) 4 | }, 5 | clearError({ commit }) { 6 | commit('clearError') 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/crash/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: require('./state').default, 3 | actions: require('./actions').default, 4 | mutations: require('./mutations').default 5 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/crash/mutations.js: -------------------------------------------------------------------------------- 1 | import serializeError from 'serialize-error' 2 | 3 | export default { 4 | 5 | setError(state, error) { 6 | state.error = Object.assign({}, serializeError(error)) 7 | }, 8 | 9 | clearError(state) { 10 | state.error = null 11 | }, 12 | 13 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/crash/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | error: null 4 | } 5 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/lookup/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | populateUser({ commit, state }, user) { 3 | commit('setUser', user) 4 | // const feathersClient = this.app.api 5 | // return feathersClient.service(state.userService) 6 | // .get(user.id) 7 | // .then(user => { 8 | // commit('setUser', user) 9 | // return user 10 | // }) 11 | }, 12 | async sourceAddresses({ commit, state }) { 13 | // if(state.lookup.sourceAddresses.length) return 14 | const feathersClient = this.app.api 15 | try { 16 | const res = await feathersClient.services.sourceaddresses.find() 17 | commit('setSourceAddresses', res.data) 18 | return res.data 19 | } catch(error) { 20 | commit('setLookupError', error) 21 | return Promise.reject(error) 22 | } 23 | }, 24 | async permissions({ commit, state }) { 25 | // if(state.lookup.permissions.length) return 26 | const feathersClient = this.app.api 27 | try { 28 | const res = await feathersClient.services.roles.find({ 29 | query: { 30 | $limit: '-1', 31 | $select: [ 'id', 'code' ] 32 | } 33 | }) 34 | commit('setPermissions', res) 35 | return res.data 36 | } catch(error) { 37 | commit('setLookupError', error) 38 | return Promise.reject(error) 39 | } 40 | }, 41 | async messageTypes({ commit, state }) { 42 | // if(state.lookup.permissions.length) return 43 | const feathersClient = this.app.api 44 | try { 45 | const res = await feathersClient.services.messagetypes.find() 46 | // console.log('@@@@@@res', res) 47 | commit('setmessageTypes', res.data) 48 | return res.data 49 | } catch(error) { 50 | commit('setLookupError', error) 51 | return Promise.reject(error) 52 | } 53 | }, 54 | async contactGroups({ commit, state }) { 55 | // if(state.lookup.permissions.length) return 56 | const feathersClient = this.app.api 57 | try { 58 | const res = await feathersClient.services.contactgroups.find() 59 | commit('setContactGroups', res.data) 60 | return res.data 61 | } catch(error) { 62 | commit('setLookupError', error) 63 | return Promise.reject(error) 64 | } 65 | }, 66 | 67 | async partners({ commit, state }) { 68 | // if(state.lookup.permissions.length) return 69 | const feathersClient = this.app.api 70 | try { 71 | const res = await feathersClient.services.proxypartners.find() 72 | commit('setPartners', res.data) 73 | return res.data 74 | } catch(error) { 75 | commit('setLookupError', error) 76 | return Promise.reject(error) 77 | } 78 | }, 79 | 80 | async groups({ commit, state }) { 81 | // if(state.lookup.permissions.length) return 82 | const feathersClient = this.app.api 83 | try { 84 | const res = await feathersClient.services.proxygroups.find() 85 | commit('setGroups', res.data) 86 | return res.data 87 | } catch(error) { 88 | commit('setLookupError', error) 89 | return Promise.reject(error) 90 | } 91 | }, 92 | 93 | 94 | async shortcodes({ commit, state }) { 95 | // if(state.lookup.permissions.length) return 96 | const feathersClient = this.app.api 97 | try { 98 | const res = await feathersClient.services.proxyshortcodes.find() 99 | commit('setShortcodes', res.data) 100 | return res.data 101 | } catch(error) { 102 | commit('setLookupError', error) 103 | return Promise.reject(error) 104 | } 105 | }, 106 | 107 | async contactLists({ commit, state }) { 108 | // if(state.lookup.permissions.length) return 109 | const feathersClient = this.app.api 110 | try { 111 | const res = await feathersClient.services.contactlists.find() 112 | // console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<', res) 113 | commit('setContactLists', res.data) 114 | return res.data 115 | } catch(error) { 116 | commit('setLookupError', error) 117 | return Promise.reject(error) 118 | } 119 | }, 120 | 121 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/lookup/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: require('./state').default, 3 | actions: require('./actions').default, 4 | mutations: require('./mutations').default 5 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/lookup/mutations.js: -------------------------------------------------------------------------------- 1 | import serializeError from 'serialize-error' 2 | 3 | export default { 4 | 5 | setLookupError(state, error) { 6 | state.errorOnLookup = Object.assign({}, serializeError(error)) 7 | }, 8 | 9 | // source addresses 10 | setSourceAddresses(state, payload) { 11 | state.sourceAddresses = payload 12 | }, 13 | unsetSourceAddresses(state) { 14 | state.sourceAddresses = [] 15 | }, 16 | 17 | // source addresses 18 | setPermissions(state, payload) { 19 | state.permissions = payload 20 | }, 21 | unsetPermissions(state) { 22 | state.permissions = [] 23 | }, 24 | 25 | // source addresses 26 | setmessageTypes(state, payload) { 27 | state.messageTypes = payload 28 | }, 29 | unsetmessageTypes(state) { 30 | state.messageTypes = [] 31 | }, 32 | 33 | // ContactGroups 34 | setContactGroups(state, payload) { 35 | state.contactGroups = payload 36 | }, 37 | unsetContactGroups(state) { 38 | state.contactGroups = [] 39 | }, 40 | 41 | // partners 42 | setPartners(state, payload) { 43 | state.partners = payload 44 | }, 45 | unsetPartners(state) { 46 | state.partners = [] 47 | }, 48 | 49 | // partners 50 | setGroups(state, payload) { 51 | state.groups = payload 52 | }, 53 | unsetGroups(state) { 54 | state.groups = [] 55 | }, 56 | 57 | // partners 58 | setShortcodes(state, payload) { 59 | state.shortcodes = payload 60 | }, 61 | unsetShortcodes(state) { 62 | state.shortcodes = [] 63 | }, 64 | 65 | // partners 66 | setContactLists(state, payload) { 67 | state.contactLists = payload 68 | }, 69 | unsetContactLists(state) { 70 | state.contactLists = [] 71 | }, 72 | 73 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/lookup/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | errorOnLookup: null, 4 | 5 | sourceAddresses: [], 6 | permissions: [], 7 | messageTypes: [], 8 | partners: [], 9 | groups: [], 10 | shortcodes: [], 11 | contactGroups: [], 12 | contactLists: [] 13 | } 14 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/network/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | 4 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/network/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: require('./state').default, 3 | actions: require('./actions').default, 4 | mutations: require('./mutations').default 5 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/network/mutations.js: -------------------------------------------------------------------------------- 1 | import serializeError from 'serialize-error' 2 | 3 | export default { 4 | 5 | setOnline(state, payload) { 6 | state.online = payload 7 | }, 8 | 9 | setOffline(state) { 10 | state.online = false 11 | }, 12 | 13 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/modules/network/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | online: true 4 | } 5 | } -------------------------------------------------------------------------------- /template/src/client/utils/store/plugins/casl.js: -------------------------------------------------------------------------------- 1 | import { Ability } from '@casl/ability' 2 | 3 | export const abilityInstance = new Ability() 4 | 5 | export const abilityPlugin = (store) => { 6 | 7 | abilityInstance.update(store.state.rules) 8 | 9 | return store.subscribe((mutation) => { 10 | switch (mutation.type) { 11 | case 'auth/setUser': 12 | // store.$router.app.$storyboard.mainStory.info('casl:store:plugin', 'user logged in, setting access rules') 13 | store.app.api.storyboard.mainStory.trace('casl:vuex', '@store/plugins/casl update permissions') 14 | abilityInstance.update(mutation.payload.rules) 15 | break 16 | case 'auth/logout': 17 | console.log('@store/plugins/casl user logged out, REsetting access rules') 18 | abilityInstance.update([{ actions: 'read', subject: 'all' }]) 19 | break 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /template/src/client/utils/store/plugins/crash.js: -------------------------------------------------------------------------------- 1 | 2 | export const crashPlugin = (store) => { 3 | 4 | return store.subscribe((mutation) => { 5 | switch (mutation.type) { 6 | case 'crash/setError': 7 | // store.$router.app.$storyboard.mainStory.info('casl:store:plugin', 'user logged in, setting access rules') 8 | // store.app.api.storyboard.mainStory.trace('casl:vuex', '@store/plugins/crash app crashed') 9 | console.log('######crash', store.state.crash.error.code ) 10 | // store.commit('route/') 11 | 12 | break 13 | } 14 | }) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /template/src/client/utils/vue-media-query-mixin/index.js: -------------------------------------------------------------------------------- 1 | import client from 'vue-media-query-mixin'; 2 | import server from './server'; 3 | 4 | export {client, server}; 5 | -------------------------------------------------------------------------------- /template/src/client/utils/vue-media-query-mixin/server.js: -------------------------------------------------------------------------------- 1 | const VueMediaQueryMixin = { 2 | install(Vue, options) { 3 | Vue.mixin({ 4 | data: function () { 5 | return { 6 | windowWidth: 666, 7 | windowHeight: 0, 8 | wXS: true, 9 | wSM: false, 10 | wMD: false, 11 | wLG: false, 12 | wXL: false 13 | } 14 | } 15 | }) 16 | } 17 | }; 18 | 19 | export default VueMediaQueryMixin; 20 | -------------------------------------------------------------------------------- /template/src/server/api.ls: -------------------------------------------------------------------------------- 1 | feathers = require '@feathersjs/feathers' 2 | express = require '@feathersjs/express' 3 | 4 | winston = require './utils/winston' 5 | 6 | {json, urlencoded, rest, errorHandler} = express 7 | 8 | module.exports = api = express feathers! # Create feathers instance and make it compatible with express v4+ <% if(resque) { %> 9 | api.configure require './jobs' # set up persistent background jobs <% } %> 10 | 11 | api.configure (require '@feathersjs/configuration')! # Load configuration parameters into app instance 12 | api.configure (require 'feathers-hooks-validator')! # Validate request bodies against service schema 13 | api.configure (require 'feathers-logger') winston # Add .info .error .warn etc for invoking winston 14 | api.configure require './db/orm' # set up database connection and ORM using <% if(database == 'sql'){%>sequelize<%}else{%>mongoose<%}%> 15 | 16 | api.use (require 'cors')! # Enable Cross-origin resource sharing 17 | api.use (require 'helmet')! # Add HTTP response headers for security 18 | api.use json limit: '10mb' # Parse every request body with JSON payload 19 | api.use urlencoded limit: '10mb' extended: true # Parse request bodies with urlencoded payload 20 | 21 | api.configure rest! # Register transport to avail services via REST 22 | api.configure require './services' # Register feathers services 23 | api.configure require './channels' # Register channels 24 | 25 | api.use errorHandler logger: winston # Catch and log all errors 26 | 27 | api.hooks require './hooks' # Register global application hooks 28 | -------------------------------------------------------------------------------- /template/src/server/channels.ls: -------------------------------------------------------------------------------- 1 | module.exports = (app) -> 2 | return if typeof app.channel isnt 'function' 3 | app.on 'connection', (connection) -> 4 | # app.storyboard.mainStory.info 'app in channel', connection 5 | (app.channel 'anonymous').join connection 6 | return 7 | app.on 'login', (authResult, {connection}) -> 8 | if connection 9 | (app.channel 'anonymous').leave connection 10 | (app.channel 'authenticated').join connection 11 | return 12 | app.publish ((data, hook) -> 13 | # app.info 'Publishing all events to all authenticated users.' 14 | app.channel 'authenticated') 15 | return -------------------------------------------------------------------------------- /template/src/server/config/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | host: 127.0.0.1 3 | port: 3030 4 | protocal: http 5 | public: "../../client/static" 6 | logger: 7 | dir: "../../../logs" 8 | levels: 9 | - error 10 | - info 11 | paginate: 12 | default: 10 13 | max: 50 14 | heml: 15 | validate: soft 16 | cheerio: {} 17 | juice: {} 18 | beautify: {} 19 | elements: [] <% if(smtp) { %> 20 | smtp: 21 | port: <%= smtp_port %> 22 | host: <%= smtp_host %> 23 | username: <%= smtp_username %> 24 | password: <%= smtp_password %> <% } %> 25 | database: <% if(cache || resque) { %> 26 | resque: 27 | host: <%= redis_host %> 28 | password: <%= redis_password %> 29 | port: <%= redis_port %> 30 | database: <%= redis_database %> 31 | pkg: ioredis <% } %> <% if(database == 'sql') { %> 32 | sequelize: 33 | username: <%= sequelize_username %> 34 | password: <%= sequelize_password %> 35 | database: <%= sequelize_database %> 36 | dialect: <%= sequelize_dialect %> 37 | host: <%= sequelize_host %> 38 | port: <%= sequelize_port %> <% if(sequelize_dialect == 'sqlite') { %> 39 | storage: db/data/<%= sequelize_database %>.sqlite <% } %> 40 | logging: false 41 | operatorsAliases: false <% } %><% if(database == 'nosql') { %> 42 | mongodb: <%=nosql_dialect%>://<%=nosql_host%>:<%=nosql_port%>/<%= nosql_database %> <% } %> 43 | authentication: 44 | secret: 01f2b6eeccc0150f5982c91627a68a47fc3850d9d0869733721b90fa4c83329b7260c3617a5e79b69b5b55dfcc61cb3173c0211c867e285e3a82ad2178a5cb1382079cbbcd85e426554a715d32884b0043a016cfb0b6e58ed5d6bea3a867bc6ce803a7ff6dfa38a3f90574c94334877117b178d7681e57b0a42a384b23210ba56ca4eb103ea3556894c8e00e17eb1e314e672033cf40e0c30c0d5724776919222e752ca8537944a4416bfe199e72288f21515abe0096feae92b3ccf5722bf208d482032f749ac624b887d93536dbbfe142be27cc82ca22b87983d2347425d9d69064f96036b791d9a38b3f85082026f60f0b83573ee3b5494119462f3f2dfaf7 45 | strategies: 46 | - jwt 47 | - local 48 | path: authentication 49 | service: users 50 | entity: user 51 | jwt: 52 | header: 53 | type: access 54 | audience: https://<%=name%>.com 55 | subject: anonymous 56 | issuer: bulksms 57 | algorithm: HS256 58 | expiresIn: 1d 59 | local: 60 | entity: user 61 | service: users 62 | usernameField: username 63 | passwordField: password 64 | cookie: 65 | enabled: true 66 | name: feathers-jwt 67 | httpOnly: false 68 | secure: false -------------------------------------------------------------------------------- /template/src/server/config/production-0.yml: -------------------------------------------------------------------------------- 1 | --- {} -------------------------------------------------------------------------------- /template/src/server/config/production.yml: -------------------------------------------------------------------------------- 1 | --- 2 | host: 0.0.0.0 3 | port: 3030 4 | protocal: http 5 | public: "../../client/static" 6 | paginate: 7 | default: 10 8 | max: 50 9 | heml: 10 | validate: soft 11 | cheerio: {} 12 | juice: {} 13 | beautify: {} 14 | elements: [] <% if(smtp) { %> 15 | smtp: 16 | port: <%= smtp_port %> 17 | host: <%= smtp_host %> 18 | username: <%= smtp_username %> 19 | password: <%= smtp_password %> <% } %> 20 | database: <% if(cache || resque) { %> 21 | resque: 22 | host: <%= redis_host %> 23 | password: <%= redis_password %> 24 | port: <%= redis_port %> 25 | database: <%= redis_database %> 26 | pkg: ioredis <% } %> <% if(database == 'sql') { %> 27 | sequelize: 28 | username: <%= sequelize_username %> 29 | password: <%= sequelize_password %> 30 | database: <%= sequelize_database %> 31 | dialect: <%= sequelize_dialect %> 32 | host: <%= sequelize_host %> 33 | port: <%= sequelize_port %> <% if(sequelize_dialect == 'sqlite') { %> 34 | storage: db/data/<%= sequelize_database %>.sqlite <% } %> 35 | logging: false 36 | operatorsAliases: false <% } %><% if(database == 'nosql') { %> 37 | mongodb: <%=nosql_dialect%>://<%=nosql_host%>:<%=nosql_port%>/<%= nosql_database %> <% } %> 38 | authentication: 39 | secret: 01f2b6eeccc0150f5982c91627a68a47fc3850d9d0869733721b90fa4c83329b7260c3617a5e79b69b5b55dfcc61cb3173c0211c867e285e3a82ad2178a5cb1382079cbbcd85e426554a715d32884b0043a016cfb0b6e58ed5d6bea3a867bc6ce803a7ff6dfa38a3f90574c94334877117b178d7681e57b0a42a384b23210ba56ca4eb103ea3556894c8e00e17eb1e314e672033cf40e0c30c0d5724776919222e752ca8537944a4416bfe199e72288f21515abe0096feae92b3ccf5722bf208d482032f749ac624b887d93536dbbfe142be27cc82ca22b87983d2347425d9d69064f96036b791d9a38b3f85082026f60f0b83573ee3b5494119462f3f2dfaf7 40 | strategies: 41 | - jwt 42 | - local 43 | - rest 44 | path: authentication 45 | service: users 46 | entity: user 47 | jwt: 48 | header: 49 | type: access 50 | audience: https://<%=name%>.com 51 | subject: anonymous 52 | issuer: bulksms 53 | algorithm: HS256 54 | expiresIn: 1d 55 | local: 56 | entity: user 57 | service: users 58 | usernameField: username 59 | passwordField: password 60 | cookie: 61 | enabled: true 62 | name: feathers-jwt 63 | httpOnly: false 64 | secure: false -------------------------------------------------------------------------------- /template/src/server/db/migrations/00-userTypes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | up: function (db, _) { 4 | return db.createTable('account_types', { 5 | 6 | id: { 7 | type: _.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | validate: { 12 | } 13 | }, 14 | code: { 15 | type: _.STRING, 16 | allowNull: false, 17 | validate: { 18 | } 19 | }, 20 | description: { 21 | type: _.STRING, 22 | allowNull: false, 23 | primaryKey: false, 24 | validate: { 25 | } 26 | }, 27 | 28 | // Timestamps 29 | createdAt: _.DATE, 30 | updatedAt: _.DATE, 31 | deletedAt: _.DATE 32 | 33 | }); 34 | }, 35 | 36 | down: function (db, _) { 37 | return db.dropTable('account_types'); 38 | } 39 | 40 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/01-roles.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(db, _) { 3 | return db.createTable('roles', { 4 | 5 | id: { 6 | type: _.INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | validate: { 11 | } 12 | }, 13 | code: { 14 | type: _.STRING, 15 | allowNull: false, 16 | validate: { 17 | } 18 | }, 19 | description: { 20 | type: _.STRING, 21 | allowNull: false, 22 | validate: { 23 | } 24 | }, 25 | 26 | // Timestamps 27 | createdAt: _.DATE, 28 | updatedAt: _.DATE, 29 | deletedAt: _.DATE 30 | 31 | }); 32 | }, 33 | 34 | down(db, _) { 35 | return db.dropTable('roles'); 36 | }, 37 | 38 | async seed(app, assert) { 39 | 40 | let permissions = []; 41 | const actions = ['manage', 'find', 'get', 'create', 'update', 'patch', 'remove']; 42 | 43 | for (i$ = 0, len$ = (ref$ = Object.keys(app.services)).length; i$ < len$; ++i$) { 44 | resource = ref$[i$]; 45 | for (j$ = 0, len1$ = actions.length; j$ < len1$; ++j$) { 46 | action = actions[j$]; 47 | permissions.push({ 48 | code: action.toUpperCase() + "_" + resource.toUpperCase(), 49 | description: action + " " + resource 50 | }); 51 | } 52 | } 53 | 54 | return await assert('roles', permissions) 55 | } 56 | 57 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/02-accountStatus.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | up: function (db, _) { 4 | return db.createTable('account_statuses', { 5 | 6 | id: { 7 | type: _.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | validate: { 12 | } 13 | }, 14 | code: { 15 | type: _.STRING, 16 | allowNull: false, 17 | validate: { 18 | } 19 | }, 20 | description: { 21 | type: _.STRING, 22 | allowNull: false, 23 | validate: { 24 | } 25 | }, 26 | 27 | // Timestamps 28 | createdAt: _.DATE, 29 | updatedAt: _.DATE, 30 | deletedAt: _.DATE 31 | 32 | }); 33 | }, 34 | 35 | down: function (db, _) { 36 | return db.dropTable('account_statuses'); 37 | } 38 | 39 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/03-accountGroups.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | up: function (db, _) { 4 | return db.createTable('account_groups', { 5 | 6 | id: { 7 | type: _.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | validate: { 12 | } 13 | }, 14 | name: { 15 | type: _.STRING, 16 | allowNull: false, 17 | validate: { 18 | } 19 | }, 20 | description: { 21 | type: _.STRING, 22 | allowNull: false, 23 | validate: { 24 | } 25 | }, 26 | 27 | // Timestamps 28 | createdAt: _.DATE, 29 | updatedAt: _.DATE, 30 | deletedAt: _.DATE 31 | 32 | }); 33 | }, 34 | 35 | down: function (db, _) { 36 | return db.dropTable('account_groups'); 37 | } 38 | 39 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/04-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: function (db, _) { 3 | return db.createTable('users', { 4 | 5 | id: { 6 | type: _.INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | validate: { 11 | } 12 | }, 13 | surname: { 14 | type: _.STRING, 15 | allowNull: false, 16 | validate: { 17 | } 18 | }, 19 | otherNames: { 20 | type: _.STRING, 21 | allowNull: false, 22 | validate: { 23 | } 24 | }, 25 | phone: { 26 | type: _.STRING, 27 | allowNull: false, 28 | validate: { 29 | } 30 | }, 31 | email: { 32 | type: _.STRING, 33 | allowNull: false, 34 | validate: { 35 | } 36 | }, 37 | 38 | // Timestamps 39 | createdAt: _.DATE, 40 | updatedAt: _.DATE, 41 | deletedAt: _.DATE 42 | 43 | }); 44 | }, 45 | 46 | down: function (db, _) { 47 | // return db.dropAllTables(); 48 | return db.dropTable('users'); 49 | } 50 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/05-userAccounts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | up: function (db, _) { 4 | return db.createTable('user_accounts', { 5 | 6 | id: { 7 | type: _.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | validate: { 12 | } 13 | }, 14 | username: { 15 | type: _.STRING, 16 | allowNull: false, 17 | validate: { 18 | } 19 | }, 20 | password: { 21 | type: _.STRING, 22 | allowNull: false, 23 | validate: { 24 | } 25 | }, 26 | statusId: { 27 | type: _.INTEGER, 28 | allowNull: false, 29 | onDelete: 'CASCADE', 30 | references: { 31 | model: 'account_statuses', 32 | key: 'id' 33 | }, 34 | validate: { 35 | } 36 | }, 37 | userId: { 38 | type: _.INTEGER, 39 | allowNull: false, 40 | onDelete: 'CASCADE', 41 | references: { 42 | model: 'users', 43 | key: 'id' 44 | }, 45 | validate: { 46 | } 47 | }, 48 | 49 | // Timestamps 50 | createdAt: _.DATE, 51 | updatedAt: _.DATE, 52 | deletedAt: _.DATE 53 | 54 | }); 55 | }, 56 | 57 | down: function (db, _) { 58 | return db.dropTable('user_accounts'); 59 | } 60 | 61 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/06-userAccountGroups.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | up: function (db, _) { 4 | return db.createTable('user_account_groups', { 5 | 6 | id: { 7 | type: _.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | validate: { 12 | } 13 | }, 14 | accountId: { 15 | type: _.INTEGER, 16 | allowNull: false, 17 | onDelete: 'CASCADE', 18 | references: { 19 | model: 'user_accounts', 20 | key: 'id' 21 | }, 22 | validate: { 23 | } 24 | }, 25 | groupId: { 26 | type: _.INTEGER, 27 | allowNull: false, 28 | onDelete: 'CASCADE', 29 | references: { 30 | model: 'account_groups', 31 | key: 'id' 32 | }, 33 | validate: { 34 | } 35 | }, 36 | 37 | // Timestamps 38 | createdAt: _.DATE, 39 | updatedAt: _.DATE, 40 | deletedAt: _.DATE 41 | 42 | }); 43 | }, 44 | 45 | down: function (db, _) { 46 | return db.dropTable('user_account_groups'); 47 | } 48 | 49 | }; -------------------------------------------------------------------------------- /template/src/server/db/migrations/07-userRoles.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | up: function (db, _) { 4 | return db.createTable('user_roles', { 5 | 6 | id: { 7 | type: _.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | validate: { 12 | } 13 | }, 14 | userId: { 15 | type: _.INTEGER, 16 | allowNull: false, 17 | onDelete: 'CASCADE', 18 | references: { 19 | model: 'users', 20 | key: 'id' 21 | }, 22 | validate: { 23 | } 24 | }, 25 | roleId: { 26 | type: _.INTEGER, 27 | allowNull: false, 28 | onDelete: 'CASCADE', 29 | references: { 30 | model: 'roles', 31 | key: 'id' 32 | }, 33 | validate: { 34 | } 35 | }, 36 | 37 | // Timestamps 38 | createdAt: _.DATE, 39 | updatedAt: _.DATE, 40 | deletedAt: _.DATE 41 | 42 | }); 43 | }, 44 | 45 | down: function (db, _) { 46 | return db.dropTable('user_roles'); 47 | } 48 | 49 | }; -------------------------------------------------------------------------------- /template/src/server/db/mongoose-orm.ls: -------------------------------------------------------------------------------- 1 | mongoose = require \mongoose 2 | mongoose.Promise = global.Promise 3 | 4 | module.exports = -> 5 | {mongodb} = @get \database 6 | mongoose.connect mongodb 7 | @set \mongoose mongoose -------------------------------------------------------------------------------- /template/src/server/db/seed.ls: -------------------------------------------------------------------------------- 1 | # functions to create initial data 2 | fs = require 'fs' 3 | path = require 'path' 4 | 5 | module.exports = (app, assert, seeds) ->> 6 | try 7 | console.log 'seeds.roles', seeds 8 | await seed-all app, assert, seeds 9 | process.exit 0 10 | catch 11 | console.log e 12 | process.exit 1 13 | 14 | seed-all = (app, assert, seeds) ->> 15 | 16 | seed-roles = (require './migrations/01-roles.js').seed 17 | 18 | # seed-roles = ->> 19 | # permissions = [] 20 | # actions = <[ manage find get create update patch remove ]> 21 | # for resource in Object.keys app.services 22 | # for action in actions 23 | # permissions.push code: "#{action.to-upper-case!}_#{resource.to-upper-case!}" description: "#{action} #{resource}" 24 | # await assert 'roles' permissions 25 | 26 | # 02 userstatuses 27 | seed-account-statuses = ->> 28 | await assert 'accountstatuses' [ 29 | * code: 'CREATED' description: 'User can access system' 30 | * code: 'ACTIVED' description: 'User can access system' 31 | * code: 'DEACTIVATED' description: 'User can NOT access system' 32 | ] 33 | 34 | # 03 accountgroups 35 | seed-account-groups = ->> 36 | await assert 'accountgroups' [ 37 | * name: 'SYSTEM_ADMINISTRATOR' description: 'Unrestricted rights over system' 38 | * name: 'ORGANIZATION_OWNER' description: 'Unrestricted rights over partner organization' 39 | * name: 'ORGANIZATION_MANAGER' description: 'Limited rights over partner organization' 40 | * name: 'ORGANIZATION_MEMBER' description: 'Very limited rights over partner organization' 41 | ] 42 | 43 | # 04 users 44 | seed-user = ->> 45 | await assert 'users' surname: 'suedoe' otherNames: 'Sue Doe' phone: '254769609906' email: 'suedoe@gmail.com' 46 | 47 | # 05 useraccounts 48 | seed-user-account = (user, status) ->> 49 | await assert 'useraccounts' username: 'suedoe' password: 'su3do3' userId: user.id, statusId: status.id 50 | 51 | # 06 useraccountgroups 52 | seed-user-account-group = (account, group) ->> 53 | await assert 'useraccountgroups' accountId: account.id, groupId: group.id 54 | 55 | # 07 userroles 56 | seed-user-roles = (user, roles) ->> 57 | userroles = [] 58 | for role in roles 59 | userroles.push userId: user.id, roleId: role.id 60 | await assert 'userroles' userroles 61 | 62 | 63 | app.info '=========seeding database=========' 64 | 65 | # 01 create roles lookup 66 | roles = await seed-roles app, assert 67 | app.info 'seeded roles', roles.length 68 | 69 | # 02 create account statuses lookup 70 | statuses = await seed-account-statuses! 71 | app.info 'seeded accountstatus', statuses.length 72 | 73 | active_status = statuses[0] # note active status object 74 | app.info 'seeded active_status', active_status 75 | 76 | # 03 create account groups lookup 77 | groups = await seed-account-groups! 78 | app.info 'seeded accountgroups', groups.length 79 | 80 | sudoers_group = groups[0] # note sudoers group object 81 | app.info 'seeded sudoers_group', sudoers_group 82 | 83 | # 04 create root user 84 | root_user = await seed-user! 85 | root_user = root_user[0] if Array.isArray root_user 86 | app.info 'seeded root_user', root_user 87 | 88 | # 05 create root user account 89 | root_user_account = await seed-user-account root_user, active_status 90 | root_user_account = root_user_account[0] if Array.isArray root_user_account 91 | app.info 'seeded root_user_account', root_user_account 92 | 93 | # 06 create root user account group 94 | root_user_account_group = await seed-user-account-group root_user_account, sudoers_group 95 | app.info 'seeded root_user_account_group', root_user_account_group 96 | 97 | # 07 create root user roles 98 | root_user_roles = await seed-user-roles root_user, roles 99 | app.info 'seeded root_user_roles', root_user_roles.length 100 | 101 | app.info '=========seeded database=========' 102 | -------------------------------------------------------------------------------- /template/src/server/db/sequelize-orm.ls: -------------------------------------------------------------------------------- 1 | Sequelize = require 'sequelize' 2 | 3 | Op = Sequelize.Op 4 | operatorsAliases = 5 | $eq: Op.eq, 6 | $ne: Op.ne, 7 | $gte: Op.gte, 8 | $gt: Op.gt, 9 | $lte: Op.lte, 10 | $lt: Op.lt, 11 | $not: Op.not, 12 | $in: Op.in, 13 | $notIn: Op.notIn, 14 | $is: Op.is, 15 | $like: Op.like, 16 | $notLike: Op.notLike, 17 | $iLike: Op.iLike, 18 | $notILike: Op.notILike, 19 | $regexp: Op.regexp, 20 | $notRegexp: Op.notRegexp, 21 | $iRegexp: Op.iRegexp, 22 | $notIRegexp: Op.notIRegexp, 23 | $between: Op.between, 24 | $notBetween: Op.notBetween, 25 | $overlap: Op.overlap, 26 | $contains: Op.contains, 27 | $contained: Op.contained, 28 | $adjacent: Op.adjacent, 29 | $strictLeft: Op.strictLeft, 30 | $strictRight: Op.strictRight, 31 | $noExtendRight: Op.noExtendRight, 32 | $noExtendLeft: Op.noExtendLeft, 33 | $and: Op.and, 34 | $or: Op.or, 35 | $any: Op.any, 36 | $all: Op.all, 37 | $values: Op.values, 38 | $col: Op.col 39 | 40 | module.exports = -> 41 | app = @ 42 | 43 | {sequelize: config} = app.get 'database' 44 | 45 | if not config then 46 | app.error 'missing required config for sequelize' 47 | process.exit 1 48 | 49 | {database, username, password} = config 50 | config.operatorsAliases = operatorsAliases 51 | 52 | try 53 | connection = new Sequelize database, username, password, config 54 | app.set 'sequelize', connection 55 | catch e 56 | app.error 'Error connecting to the database:' 57 | console.log e 58 | console.log config 59 | process.exit 1 60 | -------------------------------------------------------------------------------- /template/src/server/hooks/abilities.ls: -------------------------------------------------------------------------------- 1 | { AbilityBuilder, Ability } = require('@casl/ability') 2 | { Forbidden } = require('@feathersjs/errors') 3 | 4 | Ability.addAlias 'update', 'patch' 5 | # Ability.addAlias 'read', ['get', 'find'] 6 | Ability.addAlias 'remove', 'delete' 7 | Ability.addAlias 'view', 'read' 8 | 9 | TYPE_KEY = Symbol.for 'type' 10 | 11 | subjectName = (subject) -> 12 | return subject if not subject or typeof subject is 'string' 13 | subject[TYPE_KEY] 14 | 15 | defineRulesFor = (user) -> 16 | { rules, can } = AbilityBuilder.extract! 17 | if user.permissions and typeof user.permissions is 'object' 18 | for module in Object.keys user.permissions 19 | #actions = user.permissions[module] 20 | actions = eval '(' + user.permissions[module] + ')' 21 | for action in actions 22 | can action, module 23 | rules 24 | 25 | defineAbilitiesFor = (user) -> 26 | rules = defineRulesFor user 27 | # if user 28 | # can 'manage', ['contacts', 'messages'], {createdBy: user._id} 29 | # can ['read', 'update'], 'users', {createdBy._id} 30 | new Ability rules, {subjectName: subjectName} 31 | 32 | canReadQuery = (query) -> query isnt null 33 | 34 | module.exports = (name) -> 35 | (hook) ->> 36 | action = hook.method 37 | service = if name then hook.app.service name else hook.service 38 | serviceName = name or hook.path 39 | rules = defineRulesFor hook.result.user 40 | ability = defineAbilitiesFor hook.result.user 41 | throwUnlessCan = (action, resource) -> 42 | throw new Forbidden "You are @@@@@@@@@@@@@@@@@@@@ not allowed to #{action} #{serviceName}" if ability.cannot action, resource 43 | return 44 | hook.params.ability = ability 45 | hook.result.user.authorization = rules 46 | if hook.method is 'create' 47 | hook.data[TYPE_KEY] = serviceName 48 | #throwUnlessCan 'create' hook.data 49 | # if not hook.id 50 | # query = toMongoQuery ability, serviceName, action 51 | # if canReadQuery query then Object.assign hook.params.query, query else hook.params.query.$limit = 0 52 | # return hook 53 | params = Object.assign {}, hook.params, {provider: null} 54 | #result = await service.get hook.id, params 55 | # console.log 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbility rules', action, hook.params.provider, hook.params.ability 56 | # result[TYPE_KEY] = serviceName 57 | #throwUnlessCan action, result if hook.path isnt hook.app.get('authentication').path 58 | #if action is 'get' then hook.result = result 59 | hook 60 | -------------------------------------------------------------------------------- /template/src/server/hooks/associate-current-partner.ls: -------------------------------------------------------------------------------- 1 | { get, set } = require('lodash') 2 | 3 | defaults = idField: 'id' as: 'partnerId' 4 | 5 | module.exports = (options = {}) -> 6 | (hook) -> 7 | console.log "associateCurrentPartnerassociateCurrentPartnerassociateCurrentPartnerassociateCurrentPartner", hook.params.user 8 | setId = (obj) -> set obj, options.as, id 9 | throw new Error "The 'associateCurrentPartner' hook should only be used as a 'before' hook." if hook.type isnt 'before' 10 | if not hook.params.user 11 | return hook if not hook.params.provider 12 | throw new Error 'There is no current user to associate.' 13 | options = Object.assign {}, defaults, (hook.app.get 'authentication'), options 14 | id = get hook.params.user, options.idField 15 | if id is void then throw new Error "Current user is missing '#{options.idField}' field." 16 | if Array.isArray hook.data then hook.data.forEach setId else setId hook.data 17 | return -------------------------------------------------------------------------------- /template/src/server/hooks/authenticate.ls: -------------------------------------------------------------------------------- 1 | { authenticate } = require('@feathersjs/authentication').hooks 2 | { NotAuthenticated } = require('@feathersjs/errors') 3 | verifyIdentity = authenticate('jwt') 4 | 5 | hasToken = (hook) -> 6 | console.log 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbility rules', Object.keys hook.data 7 | return false if hook.params.headers is void 8 | if hook.data.accessToken is void then return false 9 | hook.params.headers.authorization or hook.data.accessToken 10 | 11 | module.exports = (hook) ->> 12 | try 13 | return await verifyIdentity hook 14 | catch error 15 | return hook if error instanceof NotAuthenticated and not hasToken hook 16 | throw error 17 | return -------------------------------------------------------------------------------- /template/src/server/hooks/authorize.ls: -------------------------------------------------------------------------------- 1 | { AbilityBuilder, Ability } = require('@casl/ability') 2 | { Forbidden } = require('@feathersjs/errors') 3 | 4 | Ability.addAlias 'update', 'patch' 5 | # Ability.addAlias 'read', ['get', 'find'] 6 | Ability.addAlias 'remove', 'delete' 7 | Ability.addAlias 'view', 'read' 8 | 9 | TYPE_KEY = Symbol.for 'type' 10 | 11 | subjectName = (subject) -> 12 | return subject if not subject or typeof subject is 'string' 13 | subject[TYPE_KEY] 14 | 15 | defineRulesFor = (user) -> 16 | { rules, can } = AbilityBuilder.extract! 17 | if user.permissions and typeof user.permissions is 'object' 18 | for module in Object.keys user.permissions 19 | #actions = user.permissions[module] 20 | actions = eval '(' + user.permissions[module] + ')' 21 | for action in actions 22 | can action, module 23 | rules 24 | 25 | defineAbilitiesFor = (user) -> 26 | rules = defineRulesFor user 27 | # if user 28 | # can 'manage', ['contacts', 'messages'], {createdBy: user._id} 29 | # can ['read', 'update'], 'users', {createdBy._id} 30 | new Ability rules, {subjectName: subjectName} 31 | 32 | canReadQuery = (query) -> query isnt null 33 | 34 | module.exports = (name) -> 35 | (hook) ->> 36 | action = hook.method 37 | service = if name then hook.app.service name else hook.service 38 | serviceName = name or hook.path 39 | rules = defineRulesFor hook.result.user 40 | ability = defineAbilitiesFor hook.result.user 41 | throwUnlessCan = (action, resource) -> 42 | throw new Forbidden "You are not allowed to #{action} #{serviceName}" if ability.cannot action, resource 43 | return 44 | hook.params.ability = ability 45 | hook.result.user.authorization = rules 46 | if hook.method is 'create' 47 | hook.data[TYPE_KEY] = serviceName 48 | #throwUnlessCan 'create' hook.data 49 | # if not hook.id 50 | # query = toMongoQuery ability, serviceName, action 51 | # if canReadQuery query then Object.assign hook.params.query, query else hook.params.query.$limit = 0 52 | # return hook 53 | params = Object.assign {}, hook.params, {provider: null} 54 | #result = await service.get hook.id, params 55 | # console.log 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbility rules', action, hook.params.provider, hook.params.ability 56 | # result[TYPE_KEY] = serviceName 57 | #throwUnlessCan action, result if hook.path isnt hook.app.get('authentication').path 58 | #if action is 'get' then hook.result = result 59 | hook 60 | -------------------------------------------------------------------------------- /template/src/server/hooks/ensure-enabled.ls: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | {NotAuthenticated,Forbidden} = require '@feathersjs/errors' 3 | 4 | module.exports = (options = {}) -> 5 | (hook) -> 6 | return Promise.resolve hook if not hook.params.provider 7 | if (_.get hook, 'params.user.role') is 'admin' then return Promise.resolve hook 8 | if (not _.get hook, 'params.user') or _.isEmpty hook.params.user 9 | throw new NotAuthenticated 'Cannot check if the user is enabled. You must not be authenticated.' 10 | else 11 | if not _.get hook, 'params.user.isEnabled' 12 | name = (_.get hook, 'params.user.name') or _.get hook, 'params.user.email' or 'This user ' 13 | throw new Forbidden name + ' is disabled.' 14 | return -------------------------------------------------------------------------------- /template/src/server/hooks/global.ls: -------------------------------------------------------------------------------- 1 | _ = require('feathers-hooks-common') 2 | authentication = require '@feathersjs/authentication' 3 | 4 | logger = require './logger' 5 | 6 | openProfilerStory = (context) -> 7 | {app, path, method, params, service} = context 8 | clientStories = if params.query and params.query.storyId then Array.of params.query.storyId else void 9 | story = src: 'server' title: "#{method} /#{path}" level: 'DEBUG' extraParents: clientStories 10 | app.storyboard[path] = app.storyboard.profiler = app.storyboard.mainStory.child story 11 | # console.log '@@@@@@@@@@@@@@@@openProfilerStory', params.query 12 | delete params.query.storyId if params.query and params.query.storyId 13 | context 14 | 15 | closeProfilerStory = (context) -> 16 | {app, path} = context 17 | app.storyboard[path].close! 18 | context 19 | 20 | setParamsForRestProxy = (context) ->> 21 | {app, params} = context 22 | {query, user} = params 23 | console.log '@@@@@@@@@@@@@@@@setParamsForRestProxy' 24 | # if user and user.id and not params.token 25 | # login = await app.services.logins.get user.id 26 | # params.token = login.token 27 | if query 28 | if query.$limit 29 | query.limit = query.$limit 30 | delete query.$limit 31 | if query.$skip 32 | query.skip = query.$skip 33 | delete query.$skip 34 | context 35 | 36 | authenticationRequired = (hook) -> 37 | whitelist = Array.of hook.app.get('authentication').path, 'proxyauth', 'alerts' 38 | #hook.params.provider and hook.path isnt hook.app.get('authentication').path and hook.path isnt 'proxyauth' and hook.path isnt 'proxyauth' 39 | hook.params.provider and not whitelist.includes hook.path 40 | 41 | module.exports = 42 | before: 43 | all: [ 44 | # logger! 45 | openProfilerStory 46 | _.when( 47 | authenticationRequired, 48 | authentication.hooks.authenticate [ 'jwt' ] 49 | ) 50 | # setParamsForRestProxy 51 | ] 52 | find: [] 53 | get: [] 54 | create: [] 55 | update: [] 56 | patch: [] 57 | remove: [] 58 | after: 59 | all: [ 60 | # logger! 61 | ] 62 | find: [] 63 | get: [] 64 | create: [] 65 | update: [] 66 | patch: [] 67 | remove: [] 68 | error: 69 | all: [ 70 | ] 71 | find: [] 72 | get: [] 73 | create: [] 74 | update: [] 75 | patch: [] 76 | remove: [] 77 | finally: 78 | all: [ 79 | # logger! 80 | closeProfilerStory 81 | ] 82 | find: [] 83 | get: [] 84 | create: [] 85 | update: [] 86 | patch: [] 87 | remove: [] -------------------------------------------------------------------------------- /template/src/server/hooks/has-permission-boolean.ls: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | errors = require '@feathersjs/errors' 3 | 4 | module.exports = (permission) -> 5 | (hook) -> 6 | return true if not hook.params.provider 7 | if not (_.get hook, 'params.user.role') is 'admin' 8 | or not _.get hook, 'params.user.permissions' 9 | or not hook.params.user.permissions.includes permission 10 | then false else true -------------------------------------------------------------------------------- /template/src/server/hooks/has-permission.ls: -------------------------------------------------------------------------------- 1 | {NotAuthenticated, GeneralError, Forbidden} = require '@feathersjs/errors' 2 | _ = require 'lodash' 3 | 4 | module.exports = (permission) -> 5 | (hook) -> 6 | return hook if not hook.params.provider 7 | if (_.get hook, 'params.user.role') is 'admin' then return hook 8 | name = (_.get hook, 'params.user.name') or _.get hook, 'params.user.email' 9 | if not _.get hook, 'params.user' 10 | errmsg = 'Cannot read user permissions.' 11 | errmsg += 'The current user is missing. ' 12 | errmsg += 'Seems you are not authenticated.' 13 | throw new NotAuthenticated errmsg 14 | else 15 | if not _.get hook, 'params.user.permissions' 16 | throw new GeneralError name + ' does not have any permissions.' 17 | else 18 | errmsg = name + ' does not have permission to do that.' 19 | if not hook.params.user.permissions.includes permission then throw new Forbidden errmsg 20 | return -------------------------------------------------------------------------------- /template/src/server/hooks/index.ls: -------------------------------------------------------------------------------- 1 | _ = require('feathers-hooks-common') 2 | authentication = require '@feathersjs/authentication' 3 | 4 | PrettyError = require('pretty-error') 5 | 6 | logger = -> 7 | (hook) -> 8 | action = hook.type 9 | action = 'initiated' if hook.type is 'before' 10 | action = 'completed' if hook.type is 'after' 11 | action = 'cancelled' if hook.type is 'error' 12 | message = "HOOK #{action} #{hook.method.to-upper-case!} #{hook.path}" 13 | console.log "\n" if hook.params.provider and hook.type is 'before' 14 | if hook.type is 'error' 15 | hook.app.error message 16 | console.log (new PrettyError()).render(hook.error) 17 | else 18 | hook.app.info message 19 | # hook.app.debug 'hook.data', hook.data 20 | # hook.app.debug 'hook.params', hook.params 21 | # hook.app.debug 'hook.result', hook.result if hook.result 22 | hook 23 | 24 | openProfilerStory = (context) -> 25 | {app, path, method, params, service} = context 26 | clientStories = if params.query and params.query.storyId then Array.of params.query.storyId else void 27 | story = src: 'server' title: "#{method} /#{path}" level: 'DEBUG' extraParents: clientStories 28 | app.storyboard[path] = app.storyboard.profiler = app.storyboard.mainStory.child story 29 | # console.log '@@@@@@@@@@@@@@@@openProfilerStory', params.query 30 | delete params.query.storyId if params.query and params.query.storyId 31 | context 32 | 33 | closeProfilerStory = (context) -> 34 | {app, path} = context 35 | app.storyboard[path].close! 36 | context 37 | 38 | setParamsForRestProxy = (context) ->> 39 | {app, params} = context 40 | {query, user} = params 41 | console.log '@@@@@@@@@@@@@@@@setParamsForRestProxy' 42 | # if user and user.id and not params.token 43 | # login = await app.services.logins.get user.id 44 | # params.token = login.token 45 | if query 46 | if query.$limit 47 | query.limit = query.$limit 48 | delete query.$limit 49 | if query.$skip 50 | query.skip = query.$skip 51 | delete query.$skip 52 | context 53 | 54 | authenticationRequired = (hook) -> 55 | whitelist = Array.of hook.app.get('authentication').path, 'proxyauth', 'alerts' 56 | #hook.params.provider and hook.path isnt hook.app.get('authentication').path and hook.path isnt 'proxyauth' and hook.path isnt 'proxyauth' 57 | hook.params.provider and not whitelist.includes hook.path 58 | 59 | module.exports = 60 | before: 61 | all: [ 62 | logger! 63 | # openProfilerStory 64 | _.when( 65 | authenticationRequired, 66 | authentication.hooks.authenticate [ 'jwt' ] 67 | ) 68 | # setParamsForRestProxy 69 | ] 70 | find: [] 71 | get: [] 72 | create: [] 73 | update: [] 74 | patch: [] 75 | remove: [] 76 | after: 77 | all: [ 78 | ] 79 | find: [] 80 | get: [] 81 | create: [] 82 | update: [] 83 | patch: [] 84 | remove: [] 85 | error: 86 | all: [ 87 | ] 88 | find: [] 89 | get: [] 90 | create: [] 91 | update: [] 92 | patch: [] 93 | remove: [] 94 | finally: 95 | all: [ 96 | # closeProfilerStory 97 | logger! 98 | ] 99 | find: [] 100 | get: [] 101 | create: [] 102 | update: [] 103 | patch: [] 104 | remove: [] -------------------------------------------------------------------------------- /template/src/server/hooks/logger.ls: -------------------------------------------------------------------------------- 1 | serializeError = require 'serialize-error' 2 | 3 | module.exports = -> 4 | (hook) -> 5 | message = "#{hook.method.to-upper-case!} #{hook.path}" 6 | if hook.type is 'error' 7 | message += "::Error: #{hook.error.message}" 8 | hook.app.error message, serializeError hook.error 9 | # console.log 'hook' Object.keys hook 10 | else 11 | hook.app.info message 12 | # hook.app.debug 'hook.data', hook.data 13 | # hook.app.debug 'hook.params', hook.params 14 | # hook.app.debug 'hook.result', hook.result if hook.result 15 | hook -------------------------------------------------------------------------------- /template/src/server/hooks/prevent-disabled-admin.ls: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const {GeneralError,NotAcceptable,NotFound} = '@feathersjs/errors' 3 | 4 | module.exports = (options) -> 5 | (hook) ->> 6 | return hook if not hook.params.provider 7 | if hook.data.isEnabled is false 8 | query = {} 9 | if hook.id 10 | query = {_id: hook.id} 11 | else 12 | if _.get hook, 'data._id' 13 | query = {hook.data._id} 14 | else 15 | if _.get hook, 'data.name' then query = {hook.data.name} else if _.get hook, 'data.email' then query = {hook.data.email} 16 | [err, result] = await (hook.app.service 'users').find {query: query} 17 | user = (_.get result, 'data.0') or _.get result, '0' 18 | if err 19 | throw new GeneralError 'Something went wrong on the server and we could not search users.' 20 | else 21 | if user and user.role is 'admin' then throw new NotAcceptable 'An admin cannot be disabled.' else if not user then throw new NotFound 'Could not check if user is an admin.' 22 | hook 23 | -------------------------------------------------------------------------------- /template/src/server/hooks/send-verification-email.ls: -------------------------------------------------------------------------------- 1 | dispatcher = require '../notifications/dispatcher' 2 | 3 | module.exports = (opts) -> 4 | options = if opts then opts else {} 5 | (hook) -> 6 | return hook if not hook.params.provider 7 | user = hook.result 8 | (dispatcher hook.app).dispatch 'verifySignup', user, hook 9 | hook 10 | -------------------------------------------------------------------------------- /template/src/server/hooks/set-default-role.ls: -------------------------------------------------------------------------------- 1 | { getItems } = require('feathers-hooks-common') 2 | to = require '../utils/to' 3 | 4 | module.exports = (options = {}) -> 5 | (hook) -> 6 | new Promise((resolve, reject) ->> 7 | if hook.data 8 | [ err, defaultRole ] = await to hook.app.service('settings').find({ name: 'defaultRole' }) 9 | if not err 10 | defaultRole = _.get defaultRole, '0' 11 | role = (_.get defaultRole, 'value.role') or 'basic' 12 | (_.castArray getItems hook).forEach ((item) -> item.role = role) 13 | else 14 | console.log 'Error setting default role', err 15 | resolve hook 16 | ) 17 | -------------------------------------------------------------------------------- /template/src/server/hooks/set-first-user-to-role.ls: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | {getItems} = (require 'feathers-hooks-common') 3 | 4 | module.exports = (options) -> (hook) -> new Promise ( (resolve, reject) -> 5 | ((hook.app.service 'users').find {query: {}}).then ((found) -> 6 | console.log 'Checking if first user' 7 | found = found.data if (not Array.isArray found) && found.data 8 | if found.length is 0 9 | firstUser = (_.castArray getItems hook).0 10 | firstUser.role = options.role || 'admin' 11 | console.log 'set role to ' + firstUser.role 12 | resolve hook 13 | return ), (err) -> reject err ) -------------------------------------------------------------------------------- /template/src/server/index.ls: -------------------------------------------------------------------------------- 1 | # https://docs.feathersjs.com/api/configuration.html 2 | process.env['NODE_CONFIG_DIR'] = (require 'path').join __dirname, 'config/' 3 | 4 | api = require './api' # serve data as json 5 | 6 | app = (require '@feathersjs/express')! # express instance to namespaced route paths 7 | api.use (require 'compression')! # Compress response bodies so as to lessen size of payload 8 | app.use '/api', api # mount feathers instance as express sub-app to serve routes prefixed with /api 9 | app.use (require 'cookie-parser')!, (require './middleware') api # nuxt middleware to serve all other routes 10 | 11 | process.on 'unhandledRejection', (reason, p) -> console.log 'Unhandled Rejection ', p, reason 12 | 13 | process.on 'nuxt:build:done', (err) ->> 14 | console.log err and process.exit err if err 15 | try 16 | api.setup await app.listen api.get 'port' 17 | api.info "OK app listening on http://#{api.get('host')}:#{api.get('port')}" 18 | api.info "OK pid: #{process.pid} environment: #{process.env['NODE_ENV']}" 19 | catch 20 | console.log e and process.exit e -------------------------------------------------------------------------------- /template/src/server/jobs/index.ls: -------------------------------------------------------------------------------- 1 | startWorkers = require './workers' 2 | startScheduler = require './scheduler' 3 | prepareQueue = require './queue' 4 | 5 | module.exports = -> 6 | app = @ 7 | {resque} = @.get 'database' 8 | 9 | process.on 'nuxt:build:done', (err) ->> 10 | throw err if err 11 | 12 | try 13 | workers = await startWorkers app, resque 14 | scheduler = await startScheduler app, resque 15 | queue = await prepareQueue app, resque 16 | catch 17 | console.log '@@@@@@@@@@@@@@@@@@@@@anyone' e 18 | 19 | app.set 'resque', queue 20 | 21 | # ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].for-each (eventType) -> 22 | ['SIGINT', 'SIGTERM'].for-each (eventType) -> 23 | process.on eventType, ->> 24 | await workers.end! 25 | await scheduler.end! 26 | await queue.end! 27 | console.log '$hutdown.', eventType 28 | process.exit 1 29 | -------------------------------------------------------------------------------- /template/src/server/jobs/queue.ls: -------------------------------------------------------------------------------- 1 | NodeResque = require 'node-resque' 2 | 3 | createJobs = require './tasks' 4 | 5 | module.exports = (app, options) ->> 6 | queue = new NodeResque.Queue connection: options, createJobs app 7 | queue.on 'error', (e) -> console.log \NodeResque.Queue.Error e 8 | # ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].for-each (eventType) ->> 9 | # process.on eventType, await queue.end! if queue 10 | await queue.connect! 11 | await queue.cleanOldWorkers 666 12 | queue -------------------------------------------------------------------------------- /template/src/server/jobs/scheduler.ls: -------------------------------------------------------------------------------- 1 | NodeResque = require 'node-resque' 2 | 3 | # fn to init job scheduler 4 | module.exports = (app, options) ->> 5 | scheduler = new NodeResque.Scheduler connection: options 6 | 7 | scheduler.on 'start', -> 8 | app.info "scheduler started" 9 | 10 | scheduler.on 'end', -> 11 | app.info "scheduler ended" 12 | 13 | # scheduler.on 'poll', -> 14 | # app.info "scheduler polling" 15 | 16 | scheduler.on 'master', (state) -> 17 | app.info "scheduler became master" 18 | 19 | scheduler.on 'cleanStuckWorker', (workerName, errorPayload, delta) -> 20 | app.info "failing #{workerName} (stuck for #{delta}s) and failing job #{errorPayload}" 21 | 22 | scheduler.on 'error', (error) -> 23 | app.error "scheduler error >> #{error}" 24 | 25 | scheduler.on 'workingTimestamp', (timestamp) -> 26 | app.info "scheduler working timestamp #{timestamp}" 27 | 28 | scheduler.on 'transferredJob', (timestamp, job) -> 29 | app.info "scheduler enquing job #{timestamp} >> #{JSON.stringify(job)}" 30 | 31 | # ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].for-each (eventType) ->> 32 | # process.on eventType, (await scheduler).end! if scheduler 33 | 34 | 35 | await scheduler.connect! 36 | 37 | scheduler.start! 38 | 39 | scheduler -------------------------------------------------------------------------------- /template/src/server/jobs/tasks.ls: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | util = require 'util' 4 | 5 | _ = require 'highland' 6 | 7 | format = require 'string-template' 8 | 9 | toStream = require 'into-stream' 10 | {parseDataURI} = require 'dauria' 11 | exceljsStream = require 'exceljs-transform-stream' 12 | 13 | aw = require 'awaitify-stream' 14 | 15 | D = require 'pipedreams' 16 | { $, $async } = D 17 | 18 | streamConsumer = ( transform, done ) -> 19 | $transform = -> $async (data, send, end) ->> 20 | if end 21 | return end! if end 22 | console.log '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ended' 23 | done! 24 | if data? 25 | try 26 | x = await transform data 27 | console.log 'data send' data, x 28 | send.done data 29 | catch 30 | console.log '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@' e 31 | else 32 | console.log '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ done done done' 33 | done! 34 | input = D.new_stream! 35 | input 36 | .pipe D.$show! 37 | .pipe $transform! 38 | .pipe $ 'finish', done 39 | input 40 | 41 | 42 | parseUpload = (uri, validate, transform) -> 43 | thru = (transform) -> (err, x, push, next) ->> 44 | if err 45 | push err 46 | console.log '<< err', err 47 | next! 48 | else 49 | if x is _.nil 50 | push null, x 51 | console.log 'x is _.nil ', x 52 | else 53 | console.log 'next x is y ', x 54 | y = await transform x # invoke transform fn passing x; f returns a promise 55 | console.log 'next x is y ', x, y 56 | push null, y 57 | next! 58 | return x 59 | traverse = (buffer, transform, resolve, reject) ->> 60 | dataStream = (buffer |> toStream) .pipe exceljsStream! # convert xlsx buffer to json stream 61 | # headers = await _ ((buffer |> toStream) .pipe exceljsStream!) .head! .collect! .toPromise Promise 62 | # if validate headers[0] # validate stream 63 | if true 64 | try 65 | consumer = streamConsumer transform, resolve 66 | _ dataStream .take 30 #.ratelimit 1, 1000 67 | # .consume thru transform 68 | .tap (x) -> 69 | consumer.write x 70 | console.log 'tap x', x 71 | .errors (e) -> 72 | consumer.end! 73 | reject e 74 | .done -> 75 | consumer.end! 76 | # resolve! 77 | # delay = (ms) -> new Promise (resolve) -> setTimeout resolve, ms 78 | # s = fs.createReadStream '/home/kharhys/Downloads/Export Bulk.xlsx' 79 | # reader = aw.createReader s.pipe exceljsStream! 80 | # writer = aw.createWriter process.stdout 81 | # chunk = void 82 | # entries = 0 83 | # while null isnt (chunk = await reader.readAsync!) 84 | # console.log 'chunk', chunk 85 | # await delay 3000 86 | # entries++ 87 | # console.log 'done async read stream', entries 88 | # dataStream 89 | # .on 'data', (x) ->> 90 | # # s.pause! 91 | # # y = await transform x 92 | # console.log 'data', x 93 | # await delay 300 94 | # await transform x 95 | # # s.resume! 96 | # .on 'end', -> 97 | # console.log 'done' 98 | # resolve! 99 | catch 100 | console.log 'trasformStream Error ', e 101 | else 102 | console.log 'xlsx header validation error', headers[0] 103 | reject new Error 'Uploaded file missing required columns' 104 | try 105 | {buffer} = parseDataURI uri 106 | new Promise (resolve, reject) -> traverse buffer, transform, resolve, reject 107 | catch 108 | console.log 'parseUpload error', e 109 | Promise.reject e 110 | 111 | profile = (app, partnerId) -> (contact) ->> 112 | msisdn = contact['Phone Number'] 113 | profile = phoneNumber: msisdn, fullNames: ' ', active: true, partnerId: partnerId 114 | try 115 | lookup = await app.services.contacts.find query: phoneNumber: msisdn, partnerId: partnerId 116 | contactprofile = if lookup.total is 0 then await app.services.contacts.create profile else lookup.data[0] 117 | # console.log 'profiled contact', contactprofile.phoneNumber 118 | contactprofile 119 | catch 120 | Promise.reject e 121 | 122 | parseContactList = (app, uri, partnerId) -> 123 | validate = (header) -> if header.hasOwnProperty 'Phone Number' and header.hasOwnProperty 'Contact Name' then true else false 124 | parseUpload uri, validate, profile app, partnerId 125 | 126 | parseBulkSMS = (app, uri, partnerId, sendtime) -> 127 | validate = (header) -> if header.hasOwnProperty 'Phone Number' and header.hasOwnProperty 'Message' then true else false 128 | parseUpload uri, validate, (contact) ->> 129 | isTemplate = false 130 | SCHEDULED_STATUS = 2 131 | content = contact['Message'] 132 | return contact if content is void 133 | try 134 | contactprofile = await contact |> profile app, partnerId 135 | # console.log 'parseBulkSMS for contact', contactprofile.phoneNumber 136 | mes = {typeId: 3, content, partnerId, isTemplate} 137 | message = await app.services.messages.create mes 138 | out = messageId: message.id, messageStatusId: SCHEDULED_STATUS, sendTime: sendtime, contactId: contactprofile.id 139 | outbound = await app.services.outbound.create out 140 | sched = messageId: message.id, dateTime: sendtime, outboundId: outbound.id 141 | schedule = await app.services.schedules.create sched 142 | scheduledOutboundMessage = Object.assign {}, message, {outbound, schedule} 143 | console.log 'scheduled Outbound Message to Contact', contact 144 | scheduledOutboundMessage 145 | catch 146 | console.log 'profiled contact', 'parseupload err', e 147 | e 148 | 149 | module.exports = (app) -> 150 | scheduleBroadcast: 151 | perform: (message, sendtime) ->> 152 | SCHEDULED_STATUS = 2 153 | broadcast = recipients: [] 154 | for groupoutbound in message.outbound 155 | {contactGroupId} = groupoutbound 156 | contactlistlookup = await app.services.contactlists.find query: {contactGroupId} #TODO: pagination 157 | if contactlistlookup.total 158 | for contactlistentry in contactlistlookup.data 159 | id = contactlistentry.contactId 160 | sms = messageId: message.id, messageStatusId: SCHEDULED_STATUS, sendTime: sendtime, contactId: id 161 | outbound = await app.services.outbound.create sms 162 | schedule = await app.services.schedules.create messageId: message.id, dateTime: sendtime, outboundId: outbound.id 163 | broadcast.recipients.push Object.assign {}, message, {outbound, schedule} 164 | else 165 | app.error "personalizeBroadcast Error :: could not find contactgroup with id #{contactGroupId}" 166 | #broadcast.message = Object.assign {}, message, schedule: scheduled 167 | broadcast 168 | scheduleBulkBroadcast: 169 | perform: (template, sendtime) ->> 170 | SCHEDULED_STATUS = 2 171 | broadcast = messages: [] 172 | {typeId, partnerId} = template 173 | for groupoutbound in template.outbound 174 | {contactGroupId} = groupoutbound 175 | contactlistlookup = await app.services.contactlists.find query: {contactGroupId} #TODO: pagination 176 | if contactlistlookup.total 177 | for contactlistentry in contactlistlookup.data 178 | id = contactlistentry.contactId 179 | contactlookup = await app.services.contacts.find query: {id, partnerId} 180 | if contactlookup.total 181 | {phoneNumber, fullNames} = contactlookup.data[0] 182 | content = format template.content, {phoneNumber, fullNames} 183 | message = await app.services.messages.create {typeId, partnerId, content, sourceAddressId: template.sourceAddressId} 184 | bulkextract = await app.services.bulkextracts.create bulkMessageId: template.id, extractedMessageId: message.id 185 | sms = messageId: message.id, messageStatusId: SCHEDULED_STATUS, sendTime: sendtime, contactId: id 186 | outbound = await app.services.outbound.create sms 187 | schedule = await app.services.schedules.create messageId: message.id, dateTime: sendtime, outboundId: outbound.id 188 | broadcast.messages.push Object.assign {}, message, {outbound, schedule, bulkextract} 189 | else 190 | app.error "scheduleBulkBroadcast Error :: could not find contact with id #{id}" 191 | return 192 | else 193 | app.error "scheduleBulkBroadcast Error :: could not find contactlist entries in group with id #{contactGroupId}" 194 | return 195 | broadcast.template = Object.assign {}, template 196 | # console.log util.inspect broadcast, { showHidden: true, depth: null } 197 | broadcast 198 | scheduleBulk: 199 | perform: (message, sendtime) ->> 200 | try 201 | upload = await app.services.uploads.get message.content 202 | await parseBulkSMS app, upload.uri, message.partnerId, sendtime 203 | message 204 | catch 205 | console.log 'RESQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ::scheduleBulk Error', e 206 | throw e 207 | extractContacts: 208 | perform: (partnerId, uploadId) ->> 209 | try 210 | upload = await app.services.uploads.get uploadId 211 | await parseContactList app, upload.uri, partnerId 212 | uploadId 213 | catch 214 | console.log 'RESQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ::extractContacts Error', e 215 | -------------------------------------------------------------------------------- /template/src/server/jobs/workers.ls: -------------------------------------------------------------------------------- 1 | NodeResque = require 'node-resque' 2 | 3 | createJobs = require './tasks' 4 | 5 | # fn to init worker(s) 6 | module.exports = (app, options) ->> 7 | worker = new NodeResque.Worker connection: options, queues: <[ Bulk BulkBroadcast Broadcast ]>, createJobs app 8 | 9 | worker.on 'start', -> 10 | app.info 'worker started' 11 | 12 | worker.on 'end', -> 13 | app.info 'worker ended' 14 | 15 | worker.on 'cleaning_worker', (worker, pid) -> 16 | app.info "cleaning old worker #{worker}" 17 | 18 | # worker.on 'poll', (queue) -> 19 | # app.info "worker polling #{queue}" 20 | 21 | # worker.on 'ping', (time) -> 22 | # app.info "worker check in @ #{time}" 23 | 24 | worker.on 'job', (queue, job) -> 25 | # app.info "working job #{queue} #{JSON.stringify(job)}" 26 | app.info "working job in #{queue} queue" 27 | 28 | worker.on 'reEnqueue', (queue, job, plugin) -> 29 | app.info "reEnqueue job (#{plugin}) #{queue} #{JSON.stringify(job)}" 30 | 31 | worker.on 'success', (queue, job, result) -> 32 | # console.log "job success #{queue} #{JSON.stringify(job)} >> #{result}", result 33 | app.info 'job success ', Object.assign {}, task: job, result: result 34 | 35 | worker.on 'failure', (queue, job, failure) -> 36 | app.error "job failure #{queue} #{JSON.stringify(job)} >> #{failure}" 37 | 38 | worker.on 'error', (error, queue, job) -> 39 | app.error "error #{queue} #{JSON.stringify(job)} >> #{error}" 40 | 41 | # worker.on 'pause', -> 42 | # app.info 'worker paused' 43 | 44 | # ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].for-each (eventType) ->> 45 | # process.on eventType, await worker.end! if worker 46 | 47 | 48 | await worker.connect! 49 | 50 | worker.start! 51 | 52 | worker 53 | -------------------------------------------------------------------------------- /template/src/server/middleware/index.ls: -------------------------------------------------------------------------------- 1 | nuxt = require './nuxt' 2 | 3 | # module.exports = -> 4 | # app = @ 5 | # console.log 'nuxt middleware setup' 6 | # app.use (req, res, next) -> 7 | # console.log 'nuxt middleware pre render', req.user 8 | # req.app = app 9 | # req.app.cookie = req.headers.cookie 10 | # nuxt.render req, res, next 11 | # return 12 | 13 | module.exports = (api) -> (req, res, next) -> 14 | req.api = api 15 | req.api.cookie = req.headers.cookie 16 | nuxt.render req, res, next 17 | return -------------------------------------------------------------------------------- /template/src/server/middleware/nuxt.ls: -------------------------------------------------------------------------------- 1 | {Nuxt, Builder} = require 'nuxt' 2 | 3 | config = require '../../../f3.config' 4 | 5 | # config.dev = not (process.env.NODE_ENV is 'production') 6 | config.dev = true 7 | 8 | nuxt = new Nuxt config.nuxt 9 | 10 | if config.dev 11 | builder = new Builder nuxt 12 | builder.build!.then(-> process.emit 'nuxt:build:done') 13 | else 14 | console.log 'process.env.NODE_ENV', process.env.NODE_ENV 15 | process.emit 'nuxt:build:done' 16 | 17 | module.exports = nuxt -------------------------------------------------------------------------------- /template/src/server/notifications/dispatcher.ls: -------------------------------------------------------------------------------- 1 | heml = require 'heml' 2 | path = require 'path' 3 | pug = require 'pug' 4 | 5 | options = 6 | validate: 'soft' 7 | cheerio: {} 8 | juice: {} 9 | beautify: {} 10 | elements: [] 11 | 12 | getLink = (app, type, hash) -> 13 | protocal = 'http://' 14 | port = app.get 'port' 15 | host = app.get 'host' 16 | return protocal + host + port + '/login' + '/' + type + '/' + hash 17 | 18 | sendEmailNotification = (app, notification) -> 19 | console.log 'sendEmailNotification >>>', Object.keys notification 20 | (((app.service 'notifications').create notification).then ((result) -> 21 | # console.log 'Sent EmailNotification ++++++++', result 22 | return )).catch ((err) -> 23 | console.log 'Error sending EmailNotification', err 24 | return ) 25 | 26 | verifySignup = (app, user, hook) -> 27 | emailAccountTemplatesPath = path.join(app.get('src'), '..', 'src', 'server', 'notifications', 'templates') 28 | templatePath = path.join emailAccountTemplatesPath, 'verify-signup.pug' 29 | returnEmail = (app.get 'complaint_email') or process.env.COMPLAINT_EMAIL 30 | hashLink = getLink app, 'verify', user.verifyToken 31 | context = 32 | email: user.name or user.email 33 | returnEmail: returnEmail 34 | name: (hook.data.name) 35 | hashLink: hashLink 36 | logo: '' 37 | compiledHTML = (pug.compileFile templatePath) context 38 | (heml compiledHTML, options).then ((compiledEmail) -> 39 | # console.log 'compiledEmail >>>><<<<< ' 40 | notification = 41 | email: 42 | to: (hook.data.email) 43 | from: (app.get 'SMTP_USER') 44 | subject: 'subject' 45 | html: compiledEmail.html 46 | # console.log 'sending verification email ', notification 47 | sendEmailNotification app, notification ) 48 | 49 | module.exports = (app) -> 50 | dispatch: (notification, user, hook) -> 51 | console.log 'notifications dispatch > ', notification 52 | notification.apply null, [app, user, hook] if typeof notification is 'function' 53 | return -------------------------------------------------------------------------------- /template/src/server/notifications/templates/verify-signup.pug: -------------------------------------------------------------------------------- 1 | html 2 | heml 3 | head 4 | subject Welcome to <%= name %> 5 | style. 6 | .shaded { 7 | background-color: #f1f2f3; 8 | border: 1px solid #fff; 9 | margin: 0 33px; 10 | } 11 | .shaded column { 12 | } 13 | .shaded column .bordered { 14 | border-right: 1px solid #bdbdbd; 15 | margin: 33px auto; 16 | } 17 | .padded { 18 | padding: 20px; 19 | } 20 | .heading { 21 | max-width: 500px; 22 | } 23 | .subheading { 24 | max-width: 250px; 25 | } 26 | .centered { 27 | text-align: center; 28 | padding: 2px; 29 | margin: 0 auto; 30 | } 31 | .centeredpadded { 32 | text-align: center; 33 | padding: 30px; 34 | } 35 | p.logo { 36 | margin: 0; 37 | padding: 10px; 38 | background-color: #1f5268; 39 | } 40 | p.logo img { 41 | margin: 0 auto; 42 | height: 100px; 43 | } 44 | .imgholder { 45 | text-align: center; 46 | } 47 | .imgholder img { 48 | margin: 0 auto; 49 | height: 250px; 50 | } 51 | container { 52 | width: 100%; 53 | max-width: 1200px; 54 | border: 1px solid #f7f8f9; 55 | background-color: #f1f2f3; 56 | } 57 | footer { 58 | text-align: center; 59 | font-size: 12px; 60 | padding: 12px; 61 | } 62 | .bordered, .notbordered { 63 | width: 100%; 64 | margin: 33px auto; 65 | } 66 | .card { 67 | margin: 11px auto; 68 | } 69 | body 70 | container 71 | row 72 | p.logo 73 | img(src='/logo.png', alt='some image') 74 | row.padded 75 | p 76 | h1.centered.heading Welcome to #{name} 77 | p 78 | h3.centered.heading Lorem Ipsum dolorLorem Ipsum dolor 79 | p 80 | .centered.heading Lorem Ipsum dolorLorem Ipsum dolorLorem Ipsum dolor Lorem Ipsum dolor 81 | row.shaded 82 | row 83 | column 84 | .card 85 | .bordered 86 | .imgholder 87 | img(src='/logo.png', alt='some image') 88 | .narative 89 | h5.centered.subheading Lorem Ipsum dolor 90 | p.centered.subheading Lorem Ipsum doloLorem Ipsum dolor Lorem Ipsum doloLorem Ipsum dolor 91 | column 92 | .card 93 | .bordered 94 | .imgholder 95 | img(src='/logo.png', alt='some image') 96 | .narative 97 | h5.centered.subheading Lorem Ipsum dolor 98 | p.centered.subheading Lorem Ipsum dolLorem Ipsum dolor Lorem Ipsum doloLorem Ipsum dolor 99 | column 100 | .card 101 | .notbordered 102 | .imgholder 103 | img(src='/logo.png', alt='some image') 104 | .narative 105 | h5.centered.subheading Lorem Ipsum dolor 106 | p.centered.subheading Lorem Ipsum dolor Lorem Ipsum dolor Lorem Ipsum dolorLorem Ipsum dolor 107 | row 108 | column 109 | .card 110 | .bordered 111 | .imgholder 112 | img(src='/logo.png', alt='some image') 113 | .narative 114 | h5.centered.subheading Lorem Ipsum dolor 115 | p.centered.subheading Lorem Ipsum dolrLorem Isum dolorLorem Ipsum dolLorem Ipsm dolor 116 | column 117 | .card 118 | .bordered 119 | .imgholder 120 | img(src='https://www.tendapa.co.ke/png/manage-your-stock.png', alt='manage-your-stock') 121 | .narative 122 | h5.centered.subheading Lorem Ipsum dolor 123 | p.centered.subheading Lorem Ipsum dolorLorem Ipsum dolorLorem Ipsum dolorLorem Ipsum dolor 124 | column(valign="middle") 125 | .card 126 | .notbordered 127 | .imgholder 128 | img(src='https://www.tendapa.co.ke/png/stay-connected.png', alt='some image') 129 | .narative 130 | h5.centered.subheading Lorem Ipsum dolor 131 | p.centered.subheading Lorem Ipsum dolorLorem Ipsum dolor Lorem Ipsum dolor Lorem Ipsum dolor 132 | row 133 | column 134 | h2.centeredpadded See you in a bit 135 | row 136 | column 137 | footer 138 | | @<%= name %> 2018. All lefts reversed 139 | -------------------------------------------------------------------------------- /template/src/server/services/auth/auth.service.ls: -------------------------------------------------------------------------------- 1 | _ = require 'feathers-hooks-common' 2 | errors = require '@feathersjs/errors' 3 | jwt = require '@feathersjs/authentication-jwt' 4 | local = require '@feathersjs/authentication-local' 5 | authentication = require '@feathersjs/authentication' 6 | authManagement = require 'feathers-authentication-management' 7 | 8 | authorize = require '../../hooks/abilities' 9 | 10 | dispatcher = require '../../notifications/dispatcher' 11 | 12 | authManagementOptions = 13 | service: 'users' 14 | path: 'authManagement' 15 | notifier: dispatcher 16 | longTokenLen: 15 17 | shortTokenLen: 6 18 | shortTokenDigits: true 19 | # delay: 20 | # resetDelay: 21 | # identifyUserProps: 22 | 23 | # the string username is replaced with name of logged in user 24 | # https://github.com/feathersjs/authentication/issues/508 25 | # work around: manually set this configuration 26 | local_auth_config = 27 | # entity: 'data' 28 | service: 'users' 29 | usernameField: 'username' 30 | passwordField: 'password' 31 | 32 | isAction = (args) -> (hook) -> Array.of(args).includes hook.data.action 33 | 34 | updatePassword = (context) ->> 35 | {app, data, result} = context 36 | res = await app.services.users.patch result.id, password: data.password 37 | console.log '####### updatePassword' data, result, res 38 | context 39 | 40 | attachUserToResponse = (context) -> 41 | {app, params, result} = context 42 | result.id = params.user.id 43 | result.user = params.user 44 | context 45 | 46 | logAuthenticationDone = (context) -> 47 | {app, params, result} = context 48 | app.storyboard.profiler.trace 'auth' 'authentication successful' 49 | # console.log '####### attachUserToResponse' result 50 | context 51 | 52 | logAuthenticationError = (context) -> 53 | {app} = context 54 | context.app.storyboard.mainStory.error 'auth:error:hook' 'authentication error' 55 | console.log 'logAuthenticationError logAuthenticationError logAuthenticationError' , context.error 56 | context 57 | 58 | module.exports = -> 59 | app = @ 60 | config = app.get 'authentication' 61 | app.configure authentication config 62 | app.configure jwt! 63 | app.configure local local_auth_config 64 | app.configure authManagement authManagementOptions 65 | (app.service 'authentication').hooks { 66 | before: 67 | create: [ authentication.hooks.authenticate config.strategies ] 68 | remove: [ authentication.hooks.authenticate config.strategies ] 69 | after: 70 | # create: [ logAuthenticationDone, authorize! ] 71 | create: [ attachUserToResponse, logAuthenticationDone ] 72 | remove: [ ] 73 | error: 74 | all: [ logAuthenticationError ] 75 | } 76 | (app.service 'authManagement' ).hooks { 77 | after: 78 | create: [ 79 | _.iff(isAction('verifySignupLong'), updatePassword), 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /template/src/server/services/index.ls: -------------------------------------------------------------------------------- 1 | # any file or directory automatically configured as service 2 | fs = require 'fs' 3 | path = require 'path' 4 | 5 | services = fs.readdirSync path.join __dirname, '.' 6 | .filter (file) -> file isnt 'index.ls' 7 | .map (file) -> require "./#{file}/#{file}.service.ls" 8 | 9 | module.exports = !-> services.map (service) ~> @configure service 10 | -------------------------------------------------------------------------------- /template/src/server/services/notifications/notifications.hooks.ls: -------------------------------------------------------------------------------- 1 | # {disallow} = (require 'feathers-hooks-common') 2 | 3 | module.exports = 4 | before: 5 | # all: [disallow 'external'] 6 | all: [] 7 | find: [] 8 | get: [] 9 | create: [] 10 | update: [] 11 | patch: [] 12 | remove: [] 13 | after: 14 | all: [] 15 | find: [] 16 | get: [] 17 | create: [] 18 | update: [] 19 | patch: [] 20 | remove: [] 21 | error: 22 | all: [] 23 | find: [] 24 | get: [] 25 | create: [] 26 | update: [] 27 | patch: [] 28 | remove: [] -------------------------------------------------------------------------------- /template/src/server/services/notifications/notifications.service.ls: -------------------------------------------------------------------------------- 1 | createService = require '@feathers-nuxt/feathers-notifme' 2 | 3 | hooks = require './notifications.hooks' 4 | 5 | module.exports = -> 6 | app = this 7 | options = 8 | useNotificationCatcher: false 9 | channels: 10 | email: 11 | providers: [ 12 | #* type: 'logger' 13 | * type: 'smtp', 14 | port: 465, 15 | secure: true, 16 | host: (app.get 'SMTP_HOST'), 17 | auth: user: (app.get 'SMTP_USER'), pass: (app.get 'SMTP_PASSWORD') 18 | ] 19 | app.use '/notifications', createService options 20 | service = app.service 'notifications' 21 | service.hooks hooks 22 | return 23 | -------------------------------------------------------------------------------- /template/src/server/services/users/users.hooks.ls: -------------------------------------------------------------------------------- 1 | {addVerification,removeVerification} = require('feathers-authentication-management').hooks 2 | {hashPassword} = require('@feathersjs/authentication-local').hooks 3 | {authenticate} = require('@feathersjs/authentication').hooks 4 | {restrictToOwner} = require('feathers-authentication-hooks') 5 | _ = require('feathers-hooks-common') 6 | 7 | ensureEnabled = require('../../hooks/ensure-enabled') 8 | setDefaultRole = require('../../hooks/set-default-role') 9 | setFirstUserToRole = require('../../hooks/set-first-user-to-role') 10 | hasPermissionBoolean = require('../../hooks/has-permission-boolean') 11 | sendVerificationEmail = require('../../hooks/send-verification-email') 12 | preventDisabledAdmin = require('../../hooks/prevent-disabled-admin') 13 | 14 | restrict = [ 15 | # authenticate('jwt') 16 | # ensureEnabled! 17 | # _.unless (hasPermissionBoolean 'manageUsers'), restrictToOwner idField: '_id' ownerField: '_id' 18 | ] 19 | 20 | schema = 21 | include: [ 22 | service: 'roles' 23 | nameAs: 'access' 24 | parentField: 'role' 25 | childField: 'role' 26 | ] 27 | 28 | serializeSchema = 29 | computed: 30 | permissions: (item, hook) -> _.get item, 'access.permissions' 31 | exclude: ['access', '_include'] 32 | 33 | module.exports = 34 | before: 35 | all: [] 36 | find: [].concat(restrict), 37 | get: [].concat(restrict), 38 | create: [] 39 | update: [ 40 | _.disallow('external') 41 | ] 42 | patch: [ 43 | # preventDisabledAdmin!, 44 | _.iff( 45 | _.isProvider('external'), 46 | # _.preventChanges( 47 | # 'email', 48 | # 'isVerified', 49 | # 'verifyToken', 50 | # 'verifyShortToken', 51 | # 'verifyExpires', 52 | # 'verifyChanges', 53 | # 'resetToken', 54 | # 'resetShortToken', 55 | # 'resetExpires' 56 | # ) 57 | ), 58 | ].concat(restrict), 59 | remove: [].concat(restrict) 60 | after: 61 | all: [ 62 | # _.when( 63 | # (hook) -> hook.params.provider , 64 | # _.discard('password', '_computed', 'verifyExpires', 'resetExpires', 'verifyChanges') 65 | # ) 66 | ] 67 | find: [ 68 | # _.populate( schema: schema ), 69 | # _.serialize(serializeSchema), 70 | ] 71 | get: [ 72 | # _.populate( schema: schema ), 73 | # _.serialize(serializeSchema), 74 | ] 75 | create: [ 76 | # sendVerificationEmail!, 77 | # removeVerification! #removes verification/reset fields other than .isVerified 78 | ] 79 | update: [] 80 | patch: [] 81 | remove: [] 82 | error: 83 | all: [] 84 | find: [] 85 | get: [] 86 | create: [] 87 | update: [] 88 | patch: [] 89 | remove: [] 90 | -------------------------------------------------------------------------------- /template/src/server/services/users/users.model.ls: -------------------------------------------------------------------------------- 1 | schema = require './users.schema' 2 | <% if(database == 'sql'){%> 3 | options = 4 | # don't delete entries but set the newly added attribute deletedAt 5 | # to the current date (when deletion was done) 6 | # paranoid will only work if timestamps are enabled 7 | paranoid: true 8 | timestamps: true 9 | # create sequelize model using provided schema and options 10 | module.exports = (app) -> 11 | {define} = app.get 'sequelize' 12 | define 'users', schema, options 13 | <%}else{%> 14 | # create mongoose model using instantiated schema instance 15 | module.exports = (app) -> 16 | {model, Schema} = app.get 'mongoose' 17 | model \User new Schema schema 18 | <%}%> 19 | -------------------------------------------------------------------------------- /template/src/server/services/users/users.schema.ls: -------------------------------------------------------------------------------- 1 | <% if(database == 'sql'){%> 2 | Sequelize = require 'sequelize' 3 | 4 | module.exports = 5 | # userid: type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true 6 | # username: type: Sequelize.STRING, allowNull: false, validate: notNull: true, isAlphanumeric: true 7 | # email: type: Sequelize.STRING, allowNull: false, unique: true, validate: notNull: true, isEmail: true 8 | # password: type: Sequelize.STRING, allowNull: false, validate: notNull: true 9 | # isEnabled: type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true 10 | # role: allowNull: false, type: Sequelize.STRING, validate: isAlpha: true, notNull: true 11 | # #gender: type: String, 12 | # dateOfBirth: type: Sequelize.DATE, allowNull: true 13 | # isVerified: type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true 14 | # verifyToken: type: Sequelize.STRING, allowNull: true 15 | # verifyExpires: type: Sequelize.DATE, allowNull: true 16 | # verifyChanges: type: Sequelize.STRING, allowNull: true 17 | # resetToken: type: Sequelize.STRING, allowNull: true 18 | # resetExpires: type: Sequelize.DATE, allowNull: true 19 | id: type: Sequelize.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true, validate: {} 20 | surname: type: Sequelize.STRING, allowNull: false, validate: {} 21 | otherNames: type: Sequelize.STRING, allowNull: false, validate: {} 22 | phone: type: Sequelize.STRING, allowNull: false, validate: {} 23 | email: type: Sequelize.STRING, allowNull: false, validate: {} 24 | createdAt: Sequelize.DATE 25 | updatedAt: Sequelize.DATE 26 | 27 | <%}else{%> 28 | {Schema} = require 'mongoose' 29 | 30 | colors = <[ #1ABC9C #16A085 #2ECC71 #27AE60 #3498DB #2980B9 #34495E #EA4C88 #CA2C68 #9B59B6 #8E44AD #F1C40F #F39C12 #E74C3C #C0392B ]> 31 | 32 | module.exports = 33 | # bitbucketId: type: String , 34 | # bitbucket: type: Schema.Types.Mixed , 35 | # dropboxId: type: String , 36 | # dropbox: type: Schema.Types.Mixed , 37 | # facebookId: type: String , 38 | # facebook: type: Schema.Types.Mixed , 39 | # githubId: type: String , 40 | # github: type: Schema.Types.Mixed , 41 | # googleId: type: String , 42 | # google: type: Schema.Types.Mixed , 43 | # instagramId: type: String , 44 | # instagram: type: Schema.Types.Mixed , 45 | # linkedinId: type: String , 46 | # linkedin: type: Schema.Types.Mixed , 47 | # paypalId: type: String , 48 | # paypal: type: Schema.Types.Mixed , 49 | # spotifyId: type: String , 50 | # spotify: type: Schema.Types.Mixed , 51 | email: type: String, required: true unique: true 52 | password: type: String, required: true 53 | name: type: String, required: false 54 | isEnabled: 55 | type: Boolean 56 | 'default': true 57 | role: 58 | required: true 59 | type: String 60 | trim: true 61 | color: 62 | required: false 63 | type: String 64 | trim: true 65 | enum: colors 66 | default: -> colors[Math.floor(Math.random()*colors.length)] 67 | gender: type: String 68 | dob: type: Date 69 | createdAt: type: Date, 'default': Date.now 70 | updatedAt: type: Date, 'default': Date.now 71 | isVerified: type: Boolean 72 | verifyToken: type: String 73 | verifyExpires: type: Date 74 | verifyChanges: type: Object 75 | resetToken: type: String 76 | resetExpires: type: Date 77 | <% } %> -------------------------------------------------------------------------------- /template/src/server/services/users/users.service.ls: -------------------------------------------------------------------------------- 1 | createService = require <% if(database == 'sql'){%>'feathers-sequelize'<%}else{%>'feathers-mongoose'<%}%> 2 | 3 | createModel = require './users.model' 4 | hooks = require './users.hooks' 5 | 6 | module.exports = -> 7 | app = @ 8 | options = 9 | Model: createModel app 10 | paginate: app.get 'paginate' 11 | app.use '/users', createService options 12 | (app.service 'users').hooks hooks 13 | return -------------------------------------------------------------------------------- /template/src/server/utils/patterns.ls: -------------------------------------------------------------------------------- 1 | messages = {} 2 | 3 | exports.isTitle = //^[A-Za-z0-9@:?&=.\/ _\-]*$// 4 | messages.isTitle = 'Can only contain letters, numbers, and @ : ? & = . / _ -' 5 | 6 | exports.isURI = //(((http|https|ftp):\/\/([\w-\d]+\.)+[\w-\d]+){0,1}((\/|#)[\w~,\-\.\/?%&+#=]*))// 7 | messages.isURI = 'Must be a valid internet link address' 8 | 9 | exports.isFilePath = //^[0-9A-Za-z \/*_.\\\-]*$// 10 | messages.isFilePath = 'Can only contain letters, numbers, and / * _ . -' 11 | 12 | exports.isCSSClass = //^[A-Za-z0-9_ \-*]*$// 13 | messages.isCSSClass = 'Can only contain letters, numbers, and _ - *' 14 | 15 | exports.isAnchorTarget = //^[_blank|_self|_parent|_top]*$// 16 | messages.isAnchorTarget = 'Must be _blank, _self, _parent, or _top' 17 | 18 | exports.messages = messages -------------------------------------------------------------------------------- /template/src/server/utils/repl.ls: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | 3 | module.exports = (api) -> 4 | repl = (require 'repl').start 'f3 > ' 5 | repl.context.api = api 6 | repl.defineCommand 'sh' help: 'Execute shell command' action: (arg) -> 7 | tokens = arg .replace /\s+/g, " " .split ' ' 8 | cmd = tokens.shift 1 9 | opts = if tokens.length then [ tokens.join ' ' ] else [] 10 | child = spawn cmd, opts, shell: true stdio: 'inherit' 11 | clear = (code) ~> @clearBufferedCommand(); @displayPrompt() 12 | <[ exit error ]> .map (ev) -> child.on ev, clear -------------------------------------------------------------------------------- /template/src/server/utils/storyboard.ls: -------------------------------------------------------------------------------- 1 | storyboard = require 'storyboard' 2 | {addListener} = storyboard 3 | 4 | module.exports.consoleListener = (api) -> 5 | consoleListener = (require 'storyboard-listener-console').default 6 | addListener consoleListener #if process.env.NODE_ENV isnt 'production' 7 | api.storyboard = storyboard 8 | api 9 | 10 | 11 | module.exports.wsServerListener = (api) -> (socketServer) -> 12 | authenticate = ({ login, password }) -> 13 | isAuthorized(login, password) 14 | # If your application uses sockets with auth, namespace them 15 | # so that they don't clash with the log server's: 16 | # At the server... 17 | # socketServer = io.of('/storyboard') 18 | # socketServer.use(authenticate) 19 | # socketServer.on('connection', myConnectFunction) 20 | wsServerListener = (require 'storyboard-listener-ws-server').default 21 | addListener wsServerListener, { socketServer } 22 | -------------------------------------------------------------------------------- /template/src/server/utils/to.ls: -------------------------------------------------------------------------------- 1 | exports.to = (promise) -> (promise.then ((result) -> [null, result])).catch ((err) -> [err, null]) -------------------------------------------------------------------------------- /template/src/server/utils/validate-pattern.ls: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | 3 | patterns = require './patterns' 4 | 5 | module.exports = (key) -> 6 | validator: (v) -> (_.get patterns, key).test v 7 | message: _.get patterns, 'messages.' + key 8 | 9 | exports.patterns = patterns -------------------------------------------------------------------------------- /template/src/server/utils/winston.ls: -------------------------------------------------------------------------------- 1 | {createLogger, format, transports} = require 'winston' 2 | 3 | markup = (info) -> 4 | ts = info.timestamp #.slice 0, 19 .replace 'T', ' ' 5 | if info.args 6 | inspectable = info.level > 3 and ((Object.keys info.args).length or (Object.getOwnPropertyNames info.args).length) 7 | args = if inspectable then JSON.stringify info.args, null, 2 else '' 8 | "#{ts} [#{info.level}] #{info.message} #{args}"; 9 | else 10 | "#{ts} [#{process.pid}] [#{info.level}] #{info.message.trim!}" 11 | 12 | fileFormat = format.combine format.timestamp!, format.align!, format.splat!, format.printf markup 13 | consoleFormat = format.combine format.colorize!, format.timestamp!, format.align!, format.splat!, format.printf markup 14 | 15 | winston = createLogger ( 16 | level: 'debug' 17 | format: fileFormat 18 | transports: 19 | new transports.File filename: 'error.log', level: 'error' 20 | new transports.File filename: 'combined.log' 21 | ... 22 | ) 23 | 24 | winston.add new transports.Console format: consoleFormat, colorize: true, level: 'debug' 25 | 26 | # if process.env.NODE_ENV isnt 'production' 27 | # winston.add new transports.Console format: consoleFormat, colorize: true, level: 'info' 28 | 29 | module.exports = winston 30 | -------------------------------------------------------------------------------- /template/tests/README.md: -------------------------------------------------------------------------------- 1 | This folder is for tests -------------------------------------------------------------------------------- /template/tests/features/hooks.ls: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | { After, Before, BeforeAll, AfterAll } = require('cucumber') 3 | 4 | signale = require('signale') 5 | signale.config({ 6 | displayFilename: true, 7 | displayTimestamp: true, 8 | displayDate: false 9 | }) 10 | 11 | scope = require('./support/scope') 12 | logger = require('../../src/server/utils/winston') 13 | 14 | BeforeAll -> 15 | console.log ' ' 16 | signale.time 'Running End to End tests...' 17 | 18 | Before (testCase) ->> 19 | console.log ' ' 20 | signale.await testCase.pickle.name 21 | 22 | After (testCase) ->> 23 | console.log ' ' 24 | signale.complete testCase.pickle.name 25 | 26 | AfterAll ->> 27 | console.log ' ' 28 | signale.timeEnd 'Running End to End tests...' -------------------------------------------------------------------------------- /template/tests/features/is-it-friday-yet.feature: -------------------------------------------------------------------------------- 1 | Feature: I can use cucumber.mink to navigate through my website 2 | 3 | Background: 4 | Given I browse "http://127.0.0.1:3030/" 5 | 6 | Scenario: Render Homepage and navigate 7 | Given I am on the homepage 8 | Then I should be on the homepage 9 | And I should be on "/" 10 | And the url should match ^\/ 11 | 12 | Scenario: Render Home and reload 13 | Given I am on the homepage 14 | And I reload the page 15 | Then I should be on the homepage 16 | 17 | # Scenario: Navigate backward 18 | # Given I am on the homepage 19 | # And I follow "h2.post-title a" 20 | # Then I wait 1 second 21 | # Then I should be on "/post/1" 22 | # And the url should match ^\/post\/\d+ 23 | # Then I move backward one page 24 | # Then I should be on the homepage 25 | 26 | Scenario: Render post detail 27 | Given I am on "/api/docs" 28 | Then I should see "Swagger" 29 | Then I take a screenshot 30 | -------------------------------------------------------------------------------- /template/tests/features/step_definitions/mink-gherkin.ls: -------------------------------------------------------------------------------- 1 | cucumber = require('cucumber') 2 | mink = require('cucumber-mink') 3 | 4 | mink.gherkin(cucumber) -------------------------------------------------------------------------------- /template/tests/features/support/mink.ls: -------------------------------------------------------------------------------- 1 | cucumber = require('cucumber') 2 | {Mink} = require('cucumber-mink') 3 | 4 | driver = new Mink({ 5 | baseUrl: 'http://127.0.0.1:3030', 6 | viewport: { 7 | width: 1366, 8 | height: 768, 9 | }, 10 | }) 11 | 12 | driver.hook(cucumber) -------------------------------------------------------------------------------- /template/tests/features/support/scope.ls: -------------------------------------------------------------------------------- 1 | module.exports = {} -------------------------------------------------------------------------------- /template/tests/features/world.ls: -------------------------------------------------------------------------------- 1 | process.env['mink'] = "*:*" 2 | 3 | { setDefaultTimeout } = require('cucumber') 4 | 5 | setDefaultTimeout(2 * 60 * 1000) -------------------------------------------------------------------------------- /template/uploads/README.md: -------------------------------------------------------------------------------- 1 | This folder is for holding uploaded files. --------------------------------------------------------------------------------