├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app.json ├── client ├── components │ └── navigation.vue ├── feathers-client.js ├── layouts │ ├── default.vue │ └── error.vue ├── middleware │ └── auth.js ├── pages │ ├── authenticate.vue │ ├── index.vue │ └── secret.vue ├── static │ └── favicon.ico ├── store │ ├── index.js │ ├── services │ │ └── user.js │ └── store.auth.js └── styles.css ├── config ├── default.json ├── production.json └── test.json ├── jsconfig.json ├── nuxt.config.js ├── package-lock.json ├── package.json ├── src ├── app.hooks.js ├── app.js ├── authentication.js ├── channels.js ├── hooks │ └── log.js ├── index.js ├── logger.js ├── middleware │ ├── index.js │ └── nuxt.js ├── models │ └── users.model.js └── services │ ├── index.js │ └── users │ ├── users.class.js │ ├── users.hooks.js │ └── users.service.js └── test ├── app.test.js └── services └── users.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | quote_type = single 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .nyc_output 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | 31 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 32 | /.idea 33 | .project 34 | .classpath 35 | .c9/ 36 | *.launch 37 | .settings/ 38 | *.sublime-workspace 39 | 40 | # IDE - VSCode 41 | .vscode/* 42 | !.vscode/settings.json 43 | !.vscode/tasks.json 44 | !.vscode/launch.json 45 | !.vscode/extensions.json 46 | 47 | ### Linux ### 48 | *~ 49 | 50 | # temporary files which can be created if a process still has a handle open of a deleted file 51 | .fuse_hidden* 52 | 53 | # KDE directory preferences 54 | .directory 55 | 56 | # Linux trash folder which might appear on any partition or disk 57 | .Trash-* 58 | 59 | # .nfs files are created when an open file is removed but is still being accessed 60 | .nfs* 61 | 62 | ### OSX ### 63 | *.DS_Store 64 | .AppleDouble 65 | .LSOverride 66 | 67 | # Icon must end with two \r 68 | Icon 69 | 70 | 71 | # Thumbnails 72 | ._* 73 | 74 | # Files that might appear in the root of a volume 75 | .DocumentRevisions-V100 76 | .fseventsd 77 | .Spotlight-V100 78 | .TemporaryItems 79 | .Trashes 80 | .VolumeIcon.icns 81 | .com.apple.timemachine.donotpresent 82 | 83 | # Directories potentially created on remote AFP share 84 | .AppleDB 85 | .AppleDesktop 86 | Network Trash Folder 87 | Temporary Items 88 | .apdisk 89 | 90 | ### Windows ### 91 | # Windows thumbnail cache files 92 | Thumbs.db 93 | ehthumbs.db 94 | ehthumbs_vista.db 95 | 96 | # Folder config file 97 | Desktop.ini 98 | 99 | # Recycle Bin used on file shares 100 | $RECYCLE.BIN/ 101 | 102 | # Windows Installer files 103 | *.cab 104 | *.msi 105 | *.msm 106 | *.msp 107 | 108 | # Windows shortcuts 109 | *.lnk 110 | 111 | # Others 112 | lib/ 113 | data/ 114 | .nuxt/ 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Silvestre Herrera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-nuxt 2 | 3 | > A Feathers + Nuxt demo/boilerplate 4 | 5 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 6 | 7 | ## About 8 | 9 | This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications. 10 | 11 | It also uses NeDB to store users. This is because NeDB handles data persistence in text files and it won't require you to have MongoDB, PostgreSQL, or any other database installed to just try this boilerplate. 12 | 13 | Feathers and Vue/Nuxt integration is done through the amazing [`feathers-vuex`](https://github.com/feathers-plus/feathers-vuex). 14 | 15 | Remember to set these environment variables when running in production: 16 | 17 | ```shell 18 | NODE_ENV=production 19 | PORT=3030 # or whatever port you want to use 20 | API_URL=https://yourappdomain.com/ # this is where the API is running 21 | AUTH_SECRET=[super-secret-hash] # make it long (>512 chars), and NEVER check it into source control 22 | ``` 23 | 24 | After that, you can safely start the application by using `npm start`, which will trigger a Nuxt build and then start the app. 25 | 26 | Here's a [working demo](https://feathers-nuxt.herokuapp.com/) running on Heroku. 27 | 28 | ## ⚠️ Support 29 | 30 | You may use this boilerplate for whatever projects you want, but beware that updates to it will be scarse. You're very much welcome to contribute if you find something is off. 31 | 32 | > **Latest Update:** 2021-02-16 33 | 34 | ## Getting Started 35 | 36 | Getting up and running is as easy as 1, 2, 3. 37 | 38 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 39 | 2. Install your dependencies 40 | 41 | ``` 42 | cd path/to/feathers-nuxt; npm install 43 | ``` 44 | 45 | 3. Start your app 46 | 47 | ``` 48 | npm start 49 | ``` 50 | 51 | ## Testing 52 | 53 | Simply run `npm test` and all your tests in the `test/` directory will be run. 54 | 55 | ## Scaffolding 56 | 57 | Feathers has a powerful command line interface. Here are a few things it can do: 58 | 59 | ``` 60 | $ npm install -g @feathersjs/cli # Install Feathers CLI 61 | 62 | $ feathers generate service # Generate a new Service 63 | $ feathers generate hook # Generate a new Hook 64 | $ feathers help # Show all commands 65 | ``` 66 | 67 | ## Help 68 | 69 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com) and join us on [Slack](https://slack.feathersjs.com/). 70 | 71 | For more information on `feathers-vuex`, visit [their docs](https://feathers-plus.github.io/v1/feathers-vuex/index.html). 72 | 73 | For more information on `nuxt`, visit [their docs](https://nuxtjs.org/guide). 74 | 75 | ## Changelog 76 | 77 | **0.0.0** 78 | 79 | - Initial release 80 | 81 | ## License 82 | 83 | Copyright (c) 2019 Silvestre Herrera. 84 | 85 | Licensed under the [MIT license](LICENSE). 86 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Feathers + Nuxt", 3 | "description": "A FeathersJS + Nuxt demo/boilerplate", 4 | "repository": "https://github.com/silvestreh/feathers-nuxt", 5 | "logo": "https://feathersjs.com/img/feathers-circle-logo-grey.png", 6 | "keywords": [ 7 | "node", 8 | "express", 9 | "feathers", 10 | "vuejs", 11 | "nuxtjs", 12 | "nuxt", 13 | "vue", 14 | "feathersjs", 15 | "realtime", 16 | "socketio", 17 | "ssr" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /client/components/navigation.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 47 | 48 | -------------------------------------------------------------------------------- /client/feathers-client.js: -------------------------------------------------------------------------------- 1 | import feathers from '@feathersjs/feathers'; 2 | import socketio from '@feathersjs/socketio-client'; 3 | import auth from '@feathersjs/authentication-client'; 4 | import io from 'socket.io-client'; 5 | import { iff, discard } from 'feathers-hooks-common'; 6 | import feathersVuex from 'feathers-vuex'; 7 | import { CookieStorage } from 'cookie-storage'; 8 | 9 | const socket = io(process.env.API_URL); 10 | const feathersClient = feathers() 11 | .configure(socketio(socket), { transports: ['websocket'] }) 12 | .configure(auth({ storage: new CookieStorage() })) 13 | .hooks({ 14 | before: { 15 | all: [ 16 | iff( 17 | context => ['create', 'update', 'patch'].includes(context.method), 18 | discard('__id', '__isTemp') 19 | ) 20 | ] 21 | } 22 | }); 23 | 24 | // Setting up feathers-vuex 25 | const { 26 | makeServicePlugin, 27 | makeAuthPlugin, 28 | BaseModel, 29 | models, 30 | FeathersVuex 31 | } = feathersVuex(feathersClient, { 32 | serverAlias: 'api', // optional for working with multiple APIs (this is the default value) 33 | idField: '_id', // Must match the id field in your database table/collection 34 | whitelist: ['$regex', '$options'] 35 | }); 36 | 37 | export default feathersClient; 38 | export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex }; 39 | -------------------------------------------------------------------------------- /client/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /client/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /client/middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default function (context) { 2 | const { store, redirect, route } = context; 3 | const { auth } = store.state; 4 | 5 | if (!auth.publicPages.includes(route.name) && !auth.payload) { 6 | return redirect('/authenticate'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/pages/authenticate.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | 52 | 62 | 63 | -------------------------------------------------------------------------------- /client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /client/pages/secret.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvestreh/feathers-nuxt/8863d3fd27aa1e55f591def0b844e8b0c7094c6e/client/static/favicon.ico -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import { FeathersVuex } from '../feathers-client'; 4 | import auth from './store.auth'; 5 | 6 | Vue.use(Vuex); 7 | Vue.use(FeathersVuex); 8 | 9 | const requireModule = require.context( 10 | // The path where the service modules live 11 | './services', 12 | // Whether to look in subfolders 13 | false, 14 | // Only include .js files (prevents duplicate imports`) 15 | /\.js$/ 16 | ); 17 | const servicePlugins = requireModule 18 | .keys() 19 | .map(modulePath => requireModule(modulePath).default); 20 | 21 | export default () => 22 | new Vuex.Store({ 23 | state: {}, 24 | mutations: {}, 25 | actions: {}, 26 | plugins: [...servicePlugins, auth] 27 | }); 28 | -------------------------------------------------------------------------------- /client/store/services/user.js: -------------------------------------------------------------------------------- 1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client' 2 | 3 | class User extends BaseModel { 4 | constructor(data, options) { 5 | super(data, options) 6 | } 7 | // Required for $FeathersVuex plugin to work after production transpile. 8 | static modelName = 'User' 9 | // Define default properties here 10 | static instanceDefaults() { 11 | return { 12 | email: '', 13 | password: '' 14 | } 15 | } 16 | } 17 | const servicePath = 'users' 18 | const servicePlugin = makeServicePlugin({ 19 | Model: User, 20 | service: feathersClient.service(servicePath), 21 | servicePath 22 | }) 23 | 24 | // Setup the client-side Feathers hooks. 25 | feathersClient.service(servicePath).hooks({ 26 | before: { 27 | all: [], 28 | find: [], 29 | get: [], 30 | create: [], 31 | update: [], 32 | patch: [], 33 | remove: [] 34 | }, 35 | after: { 36 | all: [], 37 | find: [], 38 | get: [], 39 | create: [], 40 | update: [], 41 | patch: [], 42 | remove: [] 43 | }, 44 | error: { 45 | all: [], 46 | find: [], 47 | get: [], 48 | create: [], 49 | update: [], 50 | patch: [], 51 | remove: [] 52 | } 53 | }) 54 | 55 | export default servicePlugin 56 | -------------------------------------------------------------------------------- /client/store/store.auth.js: -------------------------------------------------------------------------------- 1 | import { makeAuthPlugin } from '../feathers-client'; 2 | 3 | export default makeAuthPlugin({ 4 | userService: 'users', 5 | state: { 6 | // Pages which do not require auth. SEE: middleware/auth 7 | publicPages: ['index', 'authenticate'] 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | * { box-sizing: border-box; } 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 5 | font-weight: 400; 6 | font-size: 16px; 7 | color: #333; 8 | } 9 | 10 | section { 11 | padding: 0 2em; 12 | } 13 | 14 | button { 15 | align-items: center; 16 | background: coral; 17 | border: 1px solid transparent; 18 | border-radius: .25em; 19 | color: white; 20 | cursor: pointer; 21 | display: inline-flex; 22 | font-size: .85em; 23 | line-height: 2.5; 24 | padding: 0 1em; 25 | } 26 | 27 | button.ghost { 28 | border-color: coral; 29 | background: none; 30 | color: coral; 31 | } 32 | 33 | button.is-loading::before { 34 | animation: spin 1s infinite linear; 35 | border: .125em solid white; 36 | border-radius: 50%; 37 | border-top-color: transparent; 38 | content: ''; 39 | display: inline-block; 40 | height: 1em; 41 | margin-right: 1em; 42 | width: 1em; 43 | } 44 | 45 | button.ghost.is-loading::before { 46 | border-color: coral; 47 | border-top-color: transparent; 48 | } 49 | 50 | input { 51 | border: 1px solid #DDD; 52 | border-radius: .25em; 53 | line-height: 2; 54 | padding: 0 .75em; 55 | } 56 | 57 | input:focus { 58 | border-color: coral; 59 | outline: none; 60 | } 61 | 62 | @keyframes spin { 63 | from { transform: rotate(0deg); } 64 | to { transform: rotate(359deg); } 65 | } 66 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 10, 7 | "max": 50 8 | }, 9 | "nedb": "../data", 10 | "authentication": { 11 | "secret": "AUTH_SECRET", 12 | "authStrategies": [ 13 | "jwt", 14 | "local" 15 | ], 16 | "entity": "user", 17 | "service": "users", 18 | "jwtOptions": { 19 | "header": { 20 | "typ": "access" 21 | }, 22 | "audience": "https://yourdomain.com", 23 | "issuer": "feathers", 24 | "algorithm": "HS256", 25 | "expiresIn": "1d" 26 | }, 27 | "local": { 28 | "usernameField": "email", 29 | "passwordField": "password" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "feathers-nuxt-app.feathersjs.com", 3 | "port": "PORT" 4 | } 5 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3333 3 | } 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6" 5 | }, 6 | "exclude": ["node_modules"] 7 | } 8 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const package = require('./package.json'); 3 | const config = require('./config/default.json'); 4 | 5 | const isDev = !['production', 'test'].includes(process.env.NODE_ENV); 6 | 7 | module.exports = { 8 | dev: isDev, 9 | telemetry: false, 10 | srcDir: resolve(__dirname, './client'), 11 | router: { middleware: ['auth'] }, 12 | build: { 13 | transpile: ['feathers-vuex'] 14 | }, 15 | env: { API_URL: process.env.API_URL || `http://localhost:${config.port}` }, 16 | head: { 17 | title: `${package.name} — ${package.description}`, 18 | meta: [ 19 | { charset: 'utf-8' }, 20 | { name: 'viewport', content: 'width=device-width, initial-scale=1' } 21 | ] 22 | }, 23 | css: ['normalize.css/normalize.css', './client/styles.css'], 24 | render: { 25 | csp: { 26 | reportOnly: false, 27 | // Note: the following is the default directives from helmet: 28 | // `helmet.contentSecurityPolicy.getDefaultDirectives()` 29 | policies: { 30 | 'default-src': ["'self'"], 31 | 'base-uri': ["'self'"], 32 | 'block-all-mixed-content': [], 33 | 'font-src': ["'self'", 'https:', 'data:'], 34 | 'frame-ancestors': ["'self'"], 35 | 'img-src': ["'self'", 'data:'], 36 | 'object-src': ["'none'"], 37 | 'script-src': ["'self'"], 38 | 'script-src-attr': ["'none'"], 39 | 'style-src': ["'self'", 'https:', "'unsafe-inline'"], 40 | 'upgrade-insecure-requests': [] 41 | } 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-nuxt", 3 | "description": "A Feathers + Nuxt demo/boilerplate", 4 | "version": "0.0.0", 5 | "homepage": "https://github.com/silvestreh/feathers-nuxt", 6 | "main": "src", 7 | "keywords": [ 8 | "feathers" 9 | ], 10 | "author": { 11 | "name": "Silvestre Herrera", 12 | "email": "silvestre.herrera@gmail.com" 13 | }, 14 | "contributors": [], 15 | "bugs": {}, 16 | "directories": { 17 | "lib": "src", 18 | "test": "test/", 19 | "config": "config/" 20 | }, 21 | "engines": { 22 | "node": "^12 || ^14", 23 | "npm": ">= 5.0.0" 24 | }, 25 | "scripts": { 26 | "build": "nuxt build", 27 | "coverage": "cross-env NODE_ENV=test nyc npm run mocha", 28 | "test": "npm run eslint && npm run coverage", 29 | "eslint": "eslint src/. test/. --config .eslintrc.json", 30 | "dev": "cross-env DEBUG=nuxt:* nodemon src/", 31 | "start": "npm run build && node src/", 32 | "mocha": "cross-env NODE_ENV=test mocha test/ --recursive --exit" 33 | }, 34 | "dependencies": { 35 | "@feathersjs/authentication": "^4.5.11", 36 | "@feathersjs/authentication-client": "^4.5.11", 37 | "@feathersjs/authentication-local": "^4.5.11", 38 | "@feathersjs/authentication-oauth": "^4.5.11", 39 | "@feathersjs/configuration": "^4.5.11", 40 | "@feathersjs/errors": "^4.5.11", 41 | "@feathersjs/express": "^4.5.11", 42 | "@feathersjs/feathers": "^4.5.11", 43 | "@feathersjs/socketio": "^4.5.11", 44 | "@feathersjs/socketio-client": "^4.5.11", 45 | "@feathersjs/transport-commons": "^4.5.11", 46 | "@vue/composition-api": "^1.0.0-rc.1", 47 | "compression": "^1.7.4", 48 | "cookie-storage": "^6.1.0", 49 | "cors": "^2.8.5", 50 | "feathers-hooks-common": "^5.0.4", 51 | "feathers-nedb": "^5.1.0", 52 | "feathers-vuex": "^3.15.0", 53 | "helmet": "^4.4.1", 54 | "nedb": "^1.8.0", 55 | "normalize.css": "^8.0.1", 56 | "nuxt": "^2.14.12", 57 | "socket.io-client": "^2.4.0", 58 | "winston": "^3.3.3" 59 | }, 60 | "devDependencies": { 61 | "axios": "^0.21.1", 62 | "cross-env": "^7.0.3", 63 | "eslint": "^7.20.0", 64 | "mocha": "^8.2.1", 65 | "nodemon": "^2.0.7", 66 | "nyc": "^15.1.0", 67 | "request": "^2.88.2", 68 | "request-promise": "^4.2.6" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app.hooks.js: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | const log = require('./hooks/log'); 3 | 4 | module.exports = { 5 | before: { 6 | all: [ log() ], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [ log() ], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [ log() ], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const compress = require('compression'); 3 | const helmet = require('helmet'); 4 | const cors = require('cors'); 5 | const logger = require('./logger'); 6 | 7 | const feathers = require('@feathersjs/feathers'); 8 | const configuration = require('@feathersjs/configuration'); 9 | const express = require('@feathersjs/express'); 10 | const socketio = require('@feathersjs/socketio'); 11 | 12 | const middleware = require('./middleware'); 13 | const services = require('./services'); 14 | const appHooks = require('./app.hooks'); 15 | const channels = require('./channels'); 16 | 17 | const authentication = require('./authentication'); 18 | 19 | const app = express(feathers()); 20 | 21 | // Load app configuration 22 | app.configure(configuration()); 23 | // Enable security, CORS, compression, favicon and body parsing 24 | app.use( 25 | helmet({ 26 | // NOTE: CSP headers set by nuxt (SEE: nuxt.config) 27 | contentSecurityPolicy: false 28 | }) 29 | ); 30 | app.use(cors()); 31 | app.use(compress()); 32 | app.use(express.json()); 33 | app.use(express.urlencoded({ extended: true })); 34 | // NOTE: all static assets are served from the nuxt static folder (including favicon.ico) 35 | 36 | // Set up Plugins and providers 37 | app.configure(express.rest()); 38 | app.configure(socketio()); 39 | 40 | // Configure other middleware (see `middleware/index.js`) 41 | app.configure(middleware); 42 | app.configure(authentication); 43 | // Set up our services (see `services/index.js`) 44 | app.configure(services); 45 | // Set up event channels (see channels.js) 46 | app.configure(channels); 47 | 48 | // Configure a middleware for 404s and the error handler 49 | app.use(express.notFound()); 50 | app.use(express.errorHandler({ logger })); 51 | 52 | app.hooks(appHooks); 53 | 54 | module.exports = app; 55 | -------------------------------------------------------------------------------- /src/authentication.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication'); 2 | const { LocalStrategy } = require('@feathersjs/authentication-local'); 3 | const { expressOauth } = require('@feathersjs/authentication-oauth'); 4 | 5 | module.exports = app => { 6 | const authentication = new AuthenticationService(app); 7 | 8 | authentication.register('jwt', new JWTStrategy()); 9 | authentication.register('local', new LocalStrategy()); 10 | 11 | app.use('/authentication', authentication); 12 | app.configure(expressOauth()); 13 | }; 14 | -------------------------------------------------------------------------------- /src/channels.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | if(typeof app.channel !== 'function') { 3 | // If no real-time functionality has been configured just return 4 | return; 5 | } 6 | 7 | app.on('connection', connection => { 8 | // On a new real-time connection, add it to the anonymous channel 9 | app.channel('anonymous').join(connection); 10 | }); 11 | 12 | app.on('login', (authResult, { connection }) => { 13 | // connection can be undefined if there is no 14 | // real-time connection, e.g. when logging in via REST 15 | if(connection) { 16 | // Obtain the logged in user from the connection 17 | // const user = connection.user; 18 | 19 | // The connection is no longer anonymous, remove it 20 | app.channel('anonymous').leave(connection); 21 | 22 | // Add it to the authenticated user channel 23 | app.channel('authenticated').join(connection); 24 | 25 | // Channels can be named anything and joined on any condition 26 | 27 | // E.g. to send real-time events only to admins use 28 | // if(user.isAdmin) { app.channel('admins').join(connection); } 29 | 30 | // If the user has joined e.g. chat rooms 31 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection)); 32 | 33 | // Easily organize users by email and userid for things like messaging 34 | // app.channel(`emails/${user.email}`).join(connection); 35 | // app.channel(`userIds/${user.id}`).join(connection); 36 | } 37 | }); 38 | 39 | // eslint-disable-next-line no-unused-vars 40 | app.publish((data, hook) => { 41 | // Here you can add event publishers to channels set up in `channels.js` 42 | // To publish only for a specific event use `app.publish(eventname, () => {})` 43 | 44 | console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 45 | 46 | // e.g. to publish all service events to all authenticated users use 47 | return app.channel('authenticated'); 48 | }); 49 | 50 | // Here you can also add service specific event publishers 51 | // e.g. the publish the `users` service `created` event to the `admins` channel 52 | // app.service('users').publish('created', () => app.channel('admins')); 53 | 54 | // With the userid and email organization from above you can easily select involved users 55 | // app.service('messages').publish(() => { 56 | // return [ 57 | // app.channel(`userIds/${data.createdBy}`), 58 | // app.channel(`emails/${data.recipientEmail}`) 59 | // ]; 60 | // }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/hooks/log.js: -------------------------------------------------------------------------------- 1 | // A hook that logs service method before, after and error 2 | // See https://github.com/winstonjs/winston for documentation 3 | // about the logger. 4 | const logger = require('../logger'); 5 | const util = require('util'); 6 | 7 | // To see more detailed messages, uncomment the following line: 8 | // logger.level = 'debug'; 9 | 10 | module.exports = function () { 11 | return context => { 12 | // This debugs the service call and a stringified version of the hook context 13 | // You can customize the message (and logger) to your needs 14 | logger.debug(`${context.type} app.service('${context.path}').${context.method}()`); 15 | 16 | if(typeof context.toJSON === 'function' && logger.level === 'debug') { 17 | logger.debug('Hook Context', util.inspect(context, {colors: false})); 18 | } 19 | 20 | if(context.error && !context.result) { 21 | logger.error(context.error.stack); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const logger = require('./logger'); 3 | const app = require('./app'); 4 | const port = app.get('port'); 5 | const server = app.listen(port); 6 | 7 | process.on('unhandledRejection', (reason, p) => 8 | logger.error('Unhandled Rejection at: Promise ', p, reason) 9 | ); 10 | 11 | server.on('listening', () => 12 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 13 | ); 14 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | const { render } = require('./nuxt'); 2 | const isTestEnv = process.env.NODE_ENV === 'test'; 3 | 4 | module.exports = function (app) { 5 | // Add your custom middleware here. Remember that 6 | // in Express, the order matters. 7 | app.use((req, res, next) => { 8 | const accepts = req.accepts('html', 'json'); 9 | if (accepts === 'json') return next(); 10 | if (!isTestEnv) render(req, res, next); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/middleware/nuxt.js: -------------------------------------------------------------------------------- 1 | const { Nuxt, Builder } = require('nuxt'); 2 | const nuxtConfig = require('../../nuxt.config'); 3 | 4 | const nuxt = new Nuxt(nuxtConfig); 5 | 6 | if (nuxtConfig.dev) { 7 | const builder = new Builder(nuxt); 8 | builder.build(); 9 | } 10 | 11 | module.exports = nuxt; 12 | -------------------------------------------------------------------------------- /src/models/users.model.js: -------------------------------------------------------------------------------- 1 | const NeDB = require('nedb'); 2 | const path = require('path'); 3 | 4 | module.exports = function (app) { 5 | const dbPath = app.get('nedb'); 6 | const Model = new NeDB({ 7 | filename: path.join(dbPath, 'users.db'), 8 | autoload: true 9 | }); 10 | 11 | Model.ensureIndex({ fieldName: 'email', unique: true }); 12 | 13 | return Model; 14 | }; 15 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users/users.service.js'); 2 | 3 | module.exports = function (app) { 4 | app.configure(users); 5 | }; 6 | -------------------------------------------------------------------------------- /src/services/users/users.class.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-nedb'); 2 | 3 | exports.Users = class Users extends Service { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /src/services/users/users.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | const { 4 | hashPassword, protect 5 | } = require('@feathersjs/authentication-local').hooks; 6 | 7 | module.exports = { 8 | before: { 9 | all: [], 10 | find: [ authenticate('jwt') ], 11 | get: [ authenticate('jwt') ], 12 | create: [ hashPassword('password') ], 13 | update: [ hashPassword('password'), authenticate('jwt') ], 14 | patch: [ hashPassword('password'), authenticate('jwt') ], 15 | remove: [ authenticate('jwt') ] 16 | }, 17 | 18 | after: { 19 | all: [ 20 | // Make sure the password field is never sent to the client 21 | // Always must be the last hook 22 | protect('password') 23 | ], 24 | find: [], 25 | get: [], 26 | create: [], 27 | update: [], 28 | patch: [], 29 | remove: [] 30 | }, 31 | 32 | error: { 33 | all: [], 34 | find: [], 35 | get: [], 36 | create: [], 37 | update: [], 38 | patch: [], 39 | remove: [] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/services/users/users.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `users` service on path `/users` 2 | const { Users } = require('./users.class'); 3 | const createModel = require('../../models/users.model'); 4 | const hooks = require('./users.hooks'); 5 | 6 | module.exports = function (app) { 7 | const options = { 8 | Model: createModel(app), 9 | paginate: app.get('paginate') 10 | }; 11 | 12 | // Initialize our service with any options it requires 13 | app.use('/users', new Users(options, app)); 14 | 15 | // Get our initialized service so that we can register hooks 16 | const service = app.service('users'); 17 | 18 | service.hooks(hooks); 19 | }; 20 | -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const rp = require('request-promise'); 3 | const url = require('url'); 4 | const app = require('../src/app'); 5 | 6 | const port = app.get('port') || 3030; 7 | const getUrl = pathname => url.format({ 8 | hostname: app.get('host') || 'localhost', 9 | protocol: 'http', 10 | port, 11 | pathname 12 | }); 13 | 14 | require('../src'); 15 | 16 | describe('Feathers application tests', () => { 17 | describe('404', () => { 18 | it('shows a 404 JSON error without stack trace', () => { 19 | return rp({ 20 | url: getUrl('path/to/nowhere'), 21 | json: true 22 | }).catch(res => { 23 | assert.equal(res.statusCode, 404); 24 | assert.equal(res.error.code, 404); 25 | assert.equal(res.error.message, 'Page not found'); 26 | assert.equal(res.error.name, 'NotFound'); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/services/users.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'users\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('users'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | --------------------------------------------------------------------------------