├── README.md ├── client ├── .gitignore ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.vue │ ├── assets │ └── logo.png │ ├── components │ ├── Activities.vue │ ├── AppAvatar.vue │ ├── AppNavbar.vue │ ├── CreateCard.vue │ ├── NewBoardForm.vue │ ├── NewListForm.vue │ ├── SingleBoard.vue │ └── SingleList.vue │ ├── feathers-client.js │ ├── main.js │ ├── router.js │ ├── store │ ├── board.js │ ├── feathersVuex.js │ ├── index.js │ └── localAuth.js │ ├── validators.js │ └── views │ ├── Board.vue │ ├── Boards.vue │ ├── Home.vue │ ├── Login.vue │ └── SignUp.vue ├── server ├── .editorconfig ├── .env.sample ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── config │ ├── default.json │ └── production.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── JWTVerifier.js │ ├── LocalVerifier.js │ ├── app.hooks.js │ ├── app.js │ ├── authentication.js │ ├── channels.js │ ├── hooks │ │ ├── authorization.js │ │ └── logger.js │ ├── index.js │ ├── middleware │ │ └── index.js │ ├── models │ │ ├── activities.model.js │ │ ├── boards.model.js │ │ ├── cards.model.js │ │ ├── lists.model.js │ │ └── users.model.js │ ├── mongoose.js │ └── services │ │ ├── activities │ │ ├── activities.hooks.js │ │ └── activities.service.js │ │ ├── boards │ │ ├── boards.hooks.js │ │ └── boards.service.js │ │ ├── cards │ │ ├── cards.hooks.js │ │ └── cards.service.js │ │ ├── index.js │ │ ├── lists │ │ ├── lists.hooks.js │ │ └── lists.service.js │ │ └── users │ │ ├── users.hooks.js │ │ └── users.service.js └── test │ ├── app.test.js │ └── services │ ├── activities.test.js │ ├── boards.test.js │ ├── cards.test.js │ ├── lists.test.js │ └── users.test.js └── trello-clone-erd.png /README.md: -------------------------------------------------------------------------------- 1 | # Trello Clone 2 | 3 | * [x] Generate Server 4 | * [x] Generate Client 5 | * [x] Add local auth to server 6 | * [x] Add vuetify to client 7 | * [x] Add feathers-vuex to client 8 | * [x] Add login to client 9 | * [x] Add boards service to server 10 | * [x] User can create/list/view boards on client 11 | * [x] Create 12 | * [x] Add ownerId 13 | * [x] List 14 | * [x] Restrict to owner 15 | * [x] View 16 | * [x] Restrict to owner 17 | * [x] Add lists service to server 18 | * [x] User can create/list/view lists for boards on client 19 | * [x] Create 20 | * [ ] Restrict to board owner 21 | * [x] List 22 | * [ ] Restrict to board owner 23 | * [x] Store should update boards/lists when login/logout 24 | * [x] Add cards service to server 25 | * [ ] User can create/list/view cards for lists on client 26 | * [x] Create 27 | * [ ] Restrict to board owner 28 | * [x] List 29 | * [ ] Restrict to board owner 30 | * [x] User can move cards from one list to another 31 | * [ ] Restrict editing/creating lists/cards to board owner 32 | * [ ] Update to work for no boardId in query 33 | * [x] Create activities service on server 34 | * [x] All activities are logged 35 | * [ ] User can edit title of card 36 | * [ ] restrict to board members 37 | * [ ] User can add description to a card 38 | * [ ] restrict to board members 39 | * [ ] User can assign a member to a card 40 | * [ ] restrict to board members 41 | * [ ] User can re-arrange cards in list 42 | * [ ] restrict to board members 43 | * [ ] User can re-arrange lists 44 | * [ ] restrict to board members 45 | * [ ] User can add comments to a card 46 | * [ ] restrict to board members 47 | 48 | 49 | ## Extra 50 | 51 | * [ ] Move component state into the Vuex Store 52 | * [ ] Refactor large components into many smaller components 53 | * [ ] Make it look prettier 54 | 55 | ![](./trello-clone-erd.png) 56 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --open", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@feathersjs/authentication-client": "^1.0.2", 12 | "@feathersjs/feathers": "^3.1.5", 13 | "@feathersjs/socketio-client": "^1.1.0", 14 | "feathers-vuex": "^1.2.1", 15 | "marked": "^0.3.19", 16 | "socket.io-client": "^2.1.0", 17 | "vue": "^2.5.16", 18 | "vue-router": "^3.0.1", 19 | "vuetify": "^1.0.16", 20 | "vuex": "^3.0.1" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "^3.0.0-beta.9", 24 | "@vue/cli-plugin-eslint": "^3.0.0-beta.9", 25 | "@vue/cli-service": "^3.0.0-beta.9", 26 | "@vue/eslint-config-airbnb": "^3.0.0-beta.9", 27 | "lint-staged": "^6.0.0", 28 | "stylus": "^0.54.5", 29 | "stylus-loader": "^3.0.1", 30 | "vue-cli-plugin-vuetify": "^0.1.2", 31 | "vue-template-compiler": "^2.5.13" 32 | }, 33 | "babel": { 34 | "presets": [ 35 | "@vue/app" 36 | ] 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "extends": [ 41 | "plugin:vue/essential", 42 | "@vue/airbnb" 43 | ], 44 | "rules": { 45 | "import/prefer-default-export": 0, 46 | "no-shadow": 0, 47 | "import/extensions": 0, 48 | "no-param-reassign": 0, 49 | "no-underscore-dangle": 0, 50 | "no-return-assign": 0, 51 | "vue/valid-v-on": 0 52 | } 53 | }, 54 | "postcss": { 55 | "plugins": { 56 | "autoprefixer": {} 57 | } 58 | }, 59 | "browserslist": [ 60 | "> 1%", 61 | "last 2 versions", 62 | "not ie <= 8" 63 | ], 64 | "gitHooks": { 65 | "pre-commit": "lint-staged" 66 | }, 67 | "lint-staged": { 68 | "*.js": [ 69 | "vue-cli-service lint", 70 | "git add" 71 | ], 72 | "*.vue": [ 73 | "vue-cli-service lint", 74 | "git add" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/trello-clone/c5c998cfb51274f9da1b925041e7bef0624fd1bf/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Trello Clone 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/trello-clone/c5c998cfb51274f9da1b925041e7bef0624fd1bf/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/components/Activities.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /client/src/components/AppAvatar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /client/src/components/AppNavbar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /client/src/components/CreateCard.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | -------------------------------------------------------------------------------- /client/src/components/NewBoardForm.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 65 | 66 | -------------------------------------------------------------------------------- /client/src/components/NewListForm.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 61 | -------------------------------------------------------------------------------- /client/src/components/SingleBoard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /client/src/components/SingleList.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 61 | -------------------------------------------------------------------------------- /client/src/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 | 6 | let API_URL = 'https://cg-trello-clone.now.sh'; 7 | 8 | if (window.location.hostname === 'localhost') { 9 | API_URL = 'http://localhost:3030'; 10 | } 11 | 12 | const socket = io(API_URL, { 13 | transports: ['websocket'], 14 | }); 15 | 16 | const feathersClient = feathers() 17 | .configure(socketio(socket)) 18 | .configure(auth({ 19 | storage: window.localStorage, 20 | })); 21 | 22 | export default feathersClient; 23 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify'; 3 | 4 | import 'vuetify/dist/vuetify.min.css'; 5 | 6 | import App from './App.vue'; 7 | import router from './router'; 8 | import store from './store'; 9 | 10 | Vue.use(Vuetify); 11 | 12 | Vue.config.productionTip = false; 13 | 14 | new Vue({ 15 | router, 16 | store, 17 | render: h => h(App), 18 | }).$mount('#app'); 19 | -------------------------------------------------------------------------------- /client/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from './views/Home.vue'; 4 | import SignUp from './views/SignUp.vue'; 5 | import Login from './views/Login.vue'; 6 | import Boards from './views/Boards.vue'; 7 | import Board from './views/Board.vue'; 8 | 9 | import store from './store'; 10 | 11 | Vue.use(Router); 12 | 13 | function isLoggedIn(to, from, next) { 14 | store.dispatch('auth/authenticate').then(() => { 15 | next(); 16 | }).catch(() => { 17 | next('/login'); 18 | }); 19 | } 20 | 21 | export default new Router({ 22 | routes: [ 23 | { 24 | path: '/', 25 | name: 'home', 26 | component: Home, 27 | beforeEnter(to, from, next) { 28 | store.dispatch('auth/authenticate').then(() => { 29 | next('/boards'); 30 | }).catch(() => { 31 | next('/login'); 32 | }); 33 | }, 34 | }, 35 | { 36 | path: '/signup', 37 | name: 'signup', 38 | component: SignUp, 39 | }, 40 | { 41 | path: '/login', 42 | name: 'login', 43 | component: Login, 44 | }, 45 | { 46 | path: '/boards', 47 | name: 'boards', 48 | component: Boards, 49 | beforeEnter: isLoggedIn, 50 | }, 51 | { 52 | path: '/boards/:id', 53 | name: 'board', 54 | component: Board, 55 | beforeEnter: isLoggedIn, 56 | }, 57 | ], 58 | }); 59 | -------------------------------------------------------------------------------- /client/src/store/board.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | droppingList: null, 3 | draggingCard: null, 4 | }; 5 | 6 | const mutations = { 7 | setDroppingList: (state, droppingList) => state.droppingList = droppingList, 8 | setDraggingCard: (state, draggingCard) => state.draggingCard = draggingCard, 9 | }; 10 | 11 | export default { 12 | namespaced: true, 13 | state, 14 | mutations, 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/store/feathersVuex.js: -------------------------------------------------------------------------------- 1 | import feathersVuex from 'feathers-vuex'; 2 | import feathersClient from '../feathers-client'; 3 | 4 | const { 5 | service, 6 | auth, 7 | FeathersVuex, 8 | } = feathersVuex(feathersClient, { 9 | idField: '_id', 10 | }); 11 | 12 | export default { 13 | service, 14 | auth, 15 | FeathersVuex, 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import feathersVuex from './feathersVuex'; 5 | 6 | import localAuth from './localAuth'; 7 | import board from './board'; 8 | 9 | const { 10 | service, 11 | auth, 12 | FeathersVuex, 13 | } = feathersVuex; 14 | 15 | Vue.use(Vuex); 16 | Vue.use(FeathersVuex); 17 | 18 | export default new Vuex.Store({ 19 | modules: { 20 | localAuth, 21 | board, 22 | }, 23 | plugins: [ 24 | service('users', { 25 | instanceDefaults: { 26 | username: '', 27 | password: '', 28 | displayName: '', 29 | imageUrl: '', 30 | }, 31 | }), 32 | service('boards', { 33 | instanceDefaults: { 34 | name: '', 35 | background: '', 36 | }, 37 | }), 38 | service('lists', { 39 | instanceDefaults: { 40 | name: '', 41 | order: 0, 42 | boardId: '', 43 | archived: false, 44 | }, 45 | }), 46 | service('cards', { 47 | instanceDefaults: { 48 | title: '', 49 | description: '', 50 | order: 0, 51 | archived: false, 52 | listId: '', 53 | boardId: '', 54 | members: [], 55 | }, 56 | }), 57 | service('activities', { 58 | instanceDefaults: { 59 | text: '', 60 | boardId: '', 61 | userId: null, 62 | user: 'User', 63 | }, 64 | }), 65 | auth({ 66 | userService: 'users', 67 | }), 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /client/src/store/localAuth.js: -------------------------------------------------------------------------------- 1 | import router from '@/router'; 2 | 3 | const actions = { 4 | async login({ dispatch }, { valid, user }) { 5 | if (valid) { 6 | dispatch('auth/authenticate', { 7 | strategy: 'local', 8 | ...user, 9 | }, { root: true }).then(async () => { 10 | router.push('/boards'); 11 | }).catch((e) => { 12 | console.error('Authentication error', e); 13 | }); 14 | } 15 | }, 16 | }; 17 | 18 | export default { 19 | namespaced: true, 20 | actions, 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/validators.js: -------------------------------------------------------------------------------- 1 | export const notEmptyRules = [value => !!value || 'Cannot be empty.']; 2 | -------------------------------------------------------------------------------- /client/src/views/Board.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 183 | -------------------------------------------------------------------------------- /client/src/views/Boards.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 63 | -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /client/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 59 | -------------------------------------------------------------------------------- /client/src/views/SignUp.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 92 | -------------------------------------------------------------------------------- /server/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /server/.env.sample: -------------------------------------------------------------------------------- 1 | AUTH_SECRET= 2 | MONGO_DB_URI= -------------------------------------------------------------------------------- /server/.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 | -------------------------------------------------------------------------------- /server/.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 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | 46 | ### Linux ### 47 | *~ 48 | 49 | # temporary files which can be created if a process still has a handle open of a deleted file 50 | .fuse_hidden* 51 | 52 | # KDE directory preferences 53 | .directory 54 | 55 | # Linux trash folder which might appear on any partition or disk 56 | .Trash-* 57 | 58 | # .nfs files are created when an open file is removed but is still being accessed 59 | .nfs* 60 | 61 | ### OSX ### 62 | *.DS_Store 63 | .AppleDouble 64 | .LSOverride 65 | 66 | # Icon must end with two \r 67 | Icon 68 | 69 | 70 | # Thumbnails 71 | ._* 72 | 73 | # Files that might appear in the root of a volume 74 | .DocumentRevisions-V100 75 | .fseventsd 76 | .Spotlight-V100 77 | .TemporaryItems 78 | .Trashes 79 | .VolumeIcon.icns 80 | .com.apple.timemachine.donotpresent 81 | 82 | # Directories potentially created on remote AFP share 83 | .AppleDB 84 | .AppleDesktop 85 | Network Trash Folder 86 | Temporary Items 87 | .apdisk 88 | 89 | ### Windows ### 90 | # Windows thumbnail cache files 91 | Thumbs.db 92 | ehthumbs.db 93 | ehthumbs_vista.db 94 | 95 | # Folder config file 96 | Desktop.ini 97 | 98 | # Recycle Bin used on file shares 99 | $RECYCLE.BIN/ 100 | 101 | # Windows Installer files 102 | *.cab 103 | *.msi 104 | *.msm 105 | *.msp 106 | 107 | # Windows shortcuts 108 | *.lnk 109 | 110 | # Others 111 | lib/ 112 | data/ 113 | .env 114 | -------------------------------------------------------------------------------- /server/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 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 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # trello-clone-server 2 | 3 | > A trello clone. 4 | 5 | ## About 6 | 7 | This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications. 8 | 9 | ## Getting Started 10 | 11 | Getting up and running is as easy as 1, 2, 3. 12 | 13 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 14 | 2. Install your dependencies 15 | 16 | ``` 17 | cd path/to/trello-clone-server; npm install 18 | ``` 19 | 20 | 3. Copy the .env.sample file to .env 21 | 22 | ``` 23 | cp .env.sample .env 24 | ``` 25 | 26 | 4. Update the new .env with your environmental variables 27 | 28 | 5. Start your app 29 | 30 | ``` 31 | npm start 32 | ``` 33 | 34 | ## Testing 35 | 36 | Simply run `npm test` and all your tests in the `test/` directory will be run. 37 | 38 | ## Scaffolding 39 | 40 | Feathers has a powerful command line interface. Here are a few things it can do: 41 | 42 | ``` 43 | $ npm install -g @feathersjs/cli # Install Feathers CLI 44 | 45 | $ feathers generate service # Generate a new Service 46 | $ feathers generate hook # Generate a new Hook 47 | $ feathers generate model # Generate a new Model 48 | $ feathers help # Show all commands 49 | ``` 50 | 51 | ## Help 52 | 53 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 54 | 55 | ## Changelog 56 | 57 | __0.1.0__ 58 | 59 | - Initial release 60 | 61 | ## License 62 | 63 | Copyright (c) 2016 64 | 65 | Licensed under the [MIT license](LICENSE). 66 | -------------------------------------------------------------------------------- /server/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 500, 7 | "max": 1000 8 | }, 9 | "authentication": { 10 | "secret": "AUTH_SECRET", 11 | "strategies": [ 12 | "jwt", 13 | "local" 14 | ], 15 | "path": "/authentication", 16 | "service": "users", 17 | "jwt": { 18 | "header": { 19 | "typ": "access" 20 | }, 21 | "audience": "https://cg-trello-clone.now.sh", 22 | "subject": "auth", 23 | "issuer": "coding.garden", 24 | "algorithm": "HS256", 25 | "expiresIn": "1d" 26 | }, 27 | "local": { 28 | "entity": "user", 29 | "usernameField": "\\username", 30 | "passwordField": "password" 31 | } 32 | }, 33 | "mongodb": "MONGO_DB_URI" 34 | } 35 | -------------------------------------------------------------------------------- /server/config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "cg-trello-clone.now.sh", 3 | "port": "PORT" 4 | } 5 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trello-clone-server", 3 | "description": "A trello clone.", 4 | "version": "0.0.0", 5 | "homepage": "", 6 | "main": "src", 7 | "keywords": [ 8 | "feathers" 9 | ], 10 | "author": { 11 | "name": "CJ R", 12 | "email": "cj@null.computer" 13 | }, 14 | "contributors": [], 15 | "bugs": {}, 16 | "directories": { 17 | "lib": "src", 18 | "test": "test/" 19 | }, 20 | "engines": { 21 | "node": "^8.0.0", 22 | "npm": ">= 3.0.0" 23 | }, 24 | "scripts": { 25 | "test": "npm run eslint && npm run mocha", 26 | "eslint": "eslint src/. test/. --config .eslintrc.json", 27 | "start": "node src/", 28 | "dev": "nodemon src/", 29 | "mocha": "mocha test/ --recursive --exit" 30 | }, 31 | "dependencies": { 32 | "@feathersjs/authentication": "^2.1.5", 33 | "@feathersjs/authentication-jwt": "^2.0.1", 34 | "@feathersjs/authentication-local": "^1.2.1", 35 | "@feathersjs/configuration": "^1.0.2", 36 | "@feathersjs/errors": "^3.3.0", 37 | "@feathersjs/express": "^1.2.2", 38 | "@feathersjs/feathers": "^3.1.5", 39 | "@feathersjs/socketio": "^3.2.1", 40 | "compression": "^1.7.2", 41 | "cors": "^2.8.4", 42 | "dotenv": "^6.0.0", 43 | "feathers-authentication-hooks": "^0.2.0", 44 | "feathers-mongoose": "^6.1.1", 45 | "helmet": "^3.12.0", 46 | "mongoose": "^5.7.5", 47 | "serve-favicon": "^2.5.0", 48 | "winston": "^2.4.2" 49 | }, 50 | "devDependencies": { 51 | "eslint": "^4.19.1", 52 | "mocha": "^5.1.1", 53 | "nodemon": "^1.17.4", 54 | "request": "^2.85.0", 55 | "request-promise": "^4.2.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/trello-clone/c5c998cfb51274f9da1b925041e7bef0624fd1bf/server/public/favicon.ico -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to Feathers 4 | 62 | 63 | 64 |
65 | 66 |

A REST and realtime API layer for modern applications.

67 | 68 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /server/src/JWTVerifier.js: -------------------------------------------------------------------------------- 1 | const jwt = require('@feathersjs/authentication-jwt'); 2 | 3 | class JWTVerifier extends jwt.Verifier { 4 | async verify(req, payload, done) { 5 | const User = require('mongoose').model('users'); 6 | 7 | const user = await User.findOne({ 8 | _id: payload.userId 9 | }, '_id username displayName imageUrl').lean(); 10 | 11 | payload.user = user; 12 | 13 | done(null, user, payload); 14 | } 15 | } 16 | 17 | module.exports = JWTVerifier; 18 | -------------------------------------------------------------------------------- /server/src/LocalVerifier.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const { 3 | omit 4 | } = require('lodash'); 5 | 6 | const debug = Debug('@feathersjs/authentication-local:verify'); 7 | 8 | const Verifier = require('@feathersjs/authentication-local/lib/verifier'); 9 | 10 | class LocalVerifier extends Verifier { 11 | verify(req, username, password, done) { 12 | debug('Checking credentials', username, password); 13 | 14 | const id = this.service.id; 15 | const usernameField = this.options.entityUsernameField || this.options.usernameField; 16 | const params = Object.assign({ 17 | 'query': { 18 | [usernameField]: username, 19 | '$limit': 1 20 | } 21 | }, omit(req.params, 'query', 'provider', 'headers', 'session', 'cookies')); 22 | 23 | if (id === null || id === undefined) { 24 | debug('failed: the service.id was not set'); 25 | return done(new Error('the `id` property must be set on the entity service for authentication')); 26 | } 27 | 28 | // Look up the entity 29 | this.service.find(params) 30 | .then(response => { 31 | const results = response.data || response; 32 | if (!results.length) { 33 | debug(`a record with ${usernameField} of '${username}' did not exist`); 34 | } 35 | return this._normalizeResult(response); 36 | }) 37 | .then(entity => this._comparePassword(entity, password)) 38 | .then(entity => { 39 | const id = entity[this.service.id]; 40 | const payload = { 41 | [`${this.options.entity}Id`]: id 42 | }; 43 | 44 | delete entity.password; 45 | payload.user = entity; 46 | 47 | done(null, entity, payload); 48 | }) 49 | .catch(error => error ? done(error) : done(null, error, { 50 | message: 'Invalid login' 51 | })); 52 | } 53 | } 54 | 55 | module.exports = LocalVerifier; 56 | -------------------------------------------------------------------------------- /server/src/app.hooks.js: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | const logger = require('./hooks/logger'); 3 | 4 | module.exports = { 5 | before: { 6 | all: [ logger() ], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [ logger() ], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [ logger() ], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /server/src/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const favicon = require('serve-favicon'); 3 | const compress = require('compression'); 4 | const cors = require('cors'); 5 | const helmet = require('helmet'); 6 | const logger = require('winston'); 7 | 8 | const feathers = require('@feathersjs/feathers'); 9 | const configuration = require('@feathersjs/configuration'); 10 | const express = require('@feathersjs/express'); 11 | const socketio = require('@feathersjs/socketio'); 12 | 13 | require('dotenv').config(); 14 | 15 | const middleware = require('./middleware'); 16 | const services = require('./services'); 17 | const appHooks = require('./app.hooks'); 18 | const channels = require('./channels'); 19 | 20 | const authentication = require('./authentication'); 21 | 22 | const mongoose = require('./mongoose'); 23 | 24 | const app = express(feathers()); 25 | 26 | // Load app configuration 27 | app.configure(configuration()); 28 | // Enable CORS, security, compression, favicon and body parsing 29 | app.use(cors()); 30 | app.use(helmet()); 31 | app.use(compress()); 32 | app.use(express.json()); 33 | app.use(express.urlencoded({ extended: true })); 34 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 35 | // Host the public folder 36 | app.use('/', express.static(app.get('public'))); 37 | 38 | // Set up Plugins and providers 39 | app.configure(express.rest()); 40 | app.configure(socketio()); 41 | 42 | app.configure(mongoose); 43 | 44 | // Configure other middleware (see `middleware/index.js`) 45 | app.configure(middleware); 46 | app.configure(authentication); 47 | // Set up our services (see `services/index.js`) 48 | app.configure(services); 49 | // Set up event channels (see channels.js) 50 | app.configure(channels); 51 | 52 | // Configure a middleware for 404s and the error handler 53 | app.use(express.notFound()); 54 | app.use(express.errorHandler({ logger })); 55 | 56 | app.hooks(appHooks); 57 | 58 | module.exports = app; 59 | -------------------------------------------------------------------------------- /server/src/authentication.js: -------------------------------------------------------------------------------- 1 | const authentication = require('@feathersjs/authentication'); 2 | const jwt = require('@feathersjs/authentication-jwt'); 3 | const local = require('@feathersjs/authentication-local'); 4 | 5 | const JWTVerifier = require('./JWTVerifier'); 6 | const LocalVerifier = require('./LocalVerifier'); 7 | 8 | module.exports = function (app) { 9 | const config = app.get('authentication'); 10 | 11 | // Set up authentication with the secret 12 | app.configure(authentication(config)); 13 | // app.configure(local(localConfig)); 14 | 15 | app.configure(jwt({ 16 | Verifier: JWTVerifier 17 | })); 18 | 19 | app.configure(local({ 20 | Verifier: LocalVerifier 21 | })); 22 | 23 | // The `authentication` service is used to create a JWT. 24 | // The before `create` hook registers strategies that can be used 25 | // to create a new valid JWT (e.g. local or oauth2) 26 | app.service('authentication').hooks({ 27 | before: { 28 | create: [ 29 | authentication.hooks.authenticate(config.strategies) 30 | ], 31 | remove: [ 32 | authentication.hooks.authenticate('jwt') 33 | ] 34 | } 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /server/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(channel)); 32 | 33 | // Easily organize users by email and userid for things like messaging 34 | // app.channel(`emails/${user.email}`).join(channel); 35 | // app.channel(`userIds/$(user.id}`).join(channel); 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 | -------------------------------------------------------------------------------- /server/src/hooks/authorization.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | async function isBoardOwner(context) { 4 | let boardId = context.params.query.boardId || context.data.boardId; 5 | 6 | if (!boardId) { 7 | return context; 8 | } 9 | 10 | const { 11 | _id 12 | } = context.params.user; 13 | 14 | const boards = mongoose.model('boards'); 15 | const board = await boards.findOne({ 16 | _id: boardId 17 | }); 18 | 19 | if (board) { 20 | if (board.ownerId.toString() == _id.toString()) { 21 | return context; 22 | } else { 23 | return Promise.reject(new Error('Un-Authorized')); 24 | } 25 | } 26 | 27 | return context; 28 | } 29 | 30 | module.exports = { 31 | isBoardOwner 32 | }; 33 | -------------------------------------------------------------------------------- /server/src/hooks/logger.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('winston'); 5 | 6 | // To see more detailed messages, uncomment the following line: 7 | // logger.level = 'debug'; 8 | 9 | module.exports = function () { 10 | return context => { 11 | // This debugs the service call and a stringified version of the hook context 12 | // You can customize the message (and logger) to your needs 13 | logger.debug(`${context.type} app.service('${context.path}').${context.method}()`); 14 | 15 | if(typeof context.toJSON === 'function') { 16 | logger.debug('Hook Context', JSON.stringify(context, null, ' ')); 17 | } 18 | 19 | if (context.error) { 20 | logger.error(context.error); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const logger = require('winston'); 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 | -------------------------------------------------------------------------------- /server/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | module.exports = function (app) { 3 | // Add your custom middleware here. Remember that 4 | // in Express, the order matters. 5 | }; 6 | -------------------------------------------------------------------------------- /server/src/models/activities.model.js: -------------------------------------------------------------------------------- 1 | // activities-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const mongooseClient = app.get('mongooseClient'); 7 | const { Schema } = mongooseClient; 8 | const activities = new Schema({ 9 | text: { 10 | type: String, 11 | required: true 12 | }, 13 | userId: { 14 | type: Schema.Types.ObjectId, 15 | required: true 16 | }, 17 | boardId: { 18 | type: Schema.Types.ObjectId, 19 | ref: 'boards' 20 | }, 21 | }, { 22 | timestamps: true 23 | }); 24 | 25 | return mongooseClient.model('activities', activities); 26 | }; 27 | -------------------------------------------------------------------------------- /server/src/models/boards.model.js: -------------------------------------------------------------------------------- 1 | // boards-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const mongooseClient = app.get('mongooseClient'); 7 | const { Schema } = mongooseClient; 8 | const boards = new Schema({ 9 | name: { 10 | type: String, 11 | required: true 12 | }, 13 | background: { 14 | type: String, 15 | required: true 16 | }, 17 | ownerId: { 18 | type: Schema.Types.ObjectId, 19 | ref: 'users' 20 | }, 21 | memberIds: [{ 22 | type: Schema.Types.ObjectId, 23 | ref: 'users' 24 | }], 25 | }, { 26 | timestamps: true 27 | }); 28 | 29 | return mongooseClient.model('boards', boards); 30 | }; 31 | -------------------------------------------------------------------------------- /server/src/models/cards.model.js: -------------------------------------------------------------------------------- 1 | // cards-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const mongooseClient = app.get('mongooseClient'); 7 | const { Schema } = mongooseClient; 8 | const cards = new Schema({ 9 | title: { 10 | type: String, 11 | required: true 12 | }, 13 | description: { 14 | type: String 15 | }, 16 | order: { 17 | type: Number, 18 | required: true, 19 | default: 0, 20 | }, 21 | archived: { 22 | type: Boolean, 23 | required: true, 24 | default: false, 25 | }, 26 | listId: { 27 | type: Schema.Types.ObjectId, 28 | ref: 'lists' 29 | }, 30 | boardId: { 31 | type: Schema.Types.ObjectId, 32 | ref: 'boards' 33 | }, 34 | members: [{ 35 | type: Schema.Types.ObjectId, 36 | ref: 'users' 37 | }], 38 | }, { 39 | timestamps: true 40 | }); 41 | 42 | return mongooseClient.model('cards', cards); 43 | }; 44 | -------------------------------------------------------------------------------- /server/src/models/lists.model.js: -------------------------------------------------------------------------------- 1 | // lists-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const mongooseClient = app.get('mongooseClient'); 7 | const { Schema } = mongooseClient; 8 | const lists = new Schema({ 9 | name: { 10 | type: String, 11 | required: true 12 | }, 13 | order: { 14 | type: Number, 15 | required: true, 16 | default: 0, 17 | }, 18 | archived: { 19 | type: Boolean, 20 | required: true, 21 | default: false, 22 | }, 23 | boardId: { 24 | type: Schema.Types.ObjectId, 25 | ref: 'boards' 26 | }, 27 | }, { 28 | timestamps: true 29 | }); 30 | 31 | return mongooseClient.model('lists', lists); 32 | }; 33 | -------------------------------------------------------------------------------- /server/src/models/users.model.js: -------------------------------------------------------------------------------- 1 | // users-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const mongooseClient = app.get('mongooseClient'); 7 | const users = new mongooseClient.Schema({ 8 | username: { 9 | type: String, 10 | unique: true 11 | }, 12 | displayName: { 13 | type: String, 14 | required: true 15 | }, 16 | imageUrl: { 17 | type: String, 18 | required: true 19 | }, 20 | password: { 21 | type: String, 22 | required: true 23 | }, 24 | }, { 25 | timestamps: true 26 | }); 27 | 28 | return mongooseClient.model('users', users); 29 | }; 30 | -------------------------------------------------------------------------------- /server/src/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | module.exports = function (app) { 4 | mongoose.connect(app.get('mongodb'), {}); 5 | mongoose.Promise = global.Promise; 6 | 7 | app.set('mongooseClient', mongoose); 8 | }; 9 | -------------------------------------------------------------------------------- /server/src/services/activities/activities.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const hooks = require('feathers-authentication-hooks'); 3 | const { isBoardOwner } = require('../../hooks/authorization'); 4 | 5 | module.exports = { 6 | before: { 7 | all: [ authenticate('jwt'), isBoardOwner ], 8 | find: [], 9 | get: [], 10 | create: [ 11 | hooks.associateCurrentUser({ 12 | as: 'userId' 13 | }) 14 | ], 15 | update: [], 16 | patch: [], 17 | remove: [] 18 | }, 19 | 20 | after: { 21 | all: [], 22 | find: [], 23 | get: [], 24 | create: [], 25 | update: [], 26 | patch: [], 27 | remove: [] 28 | }, 29 | 30 | error: { 31 | all: [], 32 | find: [], 33 | get: [], 34 | create: [], 35 | update: [], 36 | patch: [], 37 | remove: [] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /server/src/services/activities/activities.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `activities` service on path `/activities` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/activities.model'); 4 | const hooks = require('./activities.hooks'); 5 | 6 | module.exports = function (app) { 7 | const Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | name: 'activities', 12 | Model, 13 | paginate 14 | }; 15 | 16 | // Initialize our service with any options it requires 17 | app.use('/activities', createService(options)); 18 | 19 | // Get our initialized service so that we can register hooks and filters 20 | const service = app.service('activities'); 21 | 22 | service.hooks(hooks); 23 | }; 24 | -------------------------------------------------------------------------------- /server/src/services/boards/boards.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const hooks = require('feathers-authentication-hooks'); 3 | 4 | module.exports = { 5 | before: { 6 | all: [ authenticate('jwt') ], 7 | find: [ 8 | hooks.restrictToOwner({ 9 | ownerField: 'ownerId' 10 | }) 11 | ], 12 | get: [ 13 | hooks.restrictToOwner({ 14 | ownerField: 'ownerId' 15 | }) 16 | ], 17 | create: [ 18 | hooks.associateCurrentUser({ 19 | as: 'ownerId' 20 | }) 21 | ], 22 | update: [ 23 | hooks.restrictToOwner({ 24 | ownerField: 'ownerId' 25 | }) 26 | ], 27 | patch: [ 28 | hooks.restrictToOwner({ 29 | ownerField: 'ownerId' 30 | }) 31 | ], 32 | remove: [ 33 | hooks.restrictToOwner({ 34 | ownerField: 'ownerId' 35 | }) 36 | ] 37 | }, 38 | 39 | after: { 40 | all: [], 41 | find: [], 42 | get: [], 43 | create: [], 44 | update: [], 45 | patch: [], 46 | remove: [] 47 | }, 48 | 49 | error: { 50 | all: [], 51 | find: [], 52 | get: [], 53 | create: [], 54 | update: [], 55 | patch: [], 56 | remove: [] 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /server/src/services/boards/boards.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `boards` service on path `/boards` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/boards.model'); 4 | const hooks = require('./boards.hooks'); 5 | 6 | module.exports = function (app) { 7 | const Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | name: 'boards', 12 | Model, 13 | paginate 14 | }; 15 | 16 | // Initialize our service with any options it requires 17 | app.use('/boards', createService(options)); 18 | 19 | // Get our initialized service so that we can register hooks and filters 20 | const service = app.service('boards'); 21 | 22 | service.hooks(hooks); 23 | }; 24 | -------------------------------------------------------------------------------- /server/src/services/cards/cards.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const { isBoardOwner } = require('../../hooks/authorization'); 3 | 4 | module.exports = { 5 | before: { 6 | all: [authenticate('jwt'), isBoardOwner], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /server/src/services/cards/cards.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `cards` service on path `/cards` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/cards.model'); 4 | const hooks = require('./cards.hooks'); 5 | 6 | module.exports = function (app) { 7 | const Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | name: 'cards', 12 | Model, 13 | paginate 14 | }; 15 | 16 | // Initialize our service with any options it requires 17 | app.use('/cards', createService(options)); 18 | 19 | // Get our initialized service so that we can register hooks and filters 20 | const service = app.service('cards'); 21 | 22 | service.hooks(hooks); 23 | }; 24 | -------------------------------------------------------------------------------- /server/src/services/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users/users.service.js'); 2 | const boards = require('./boards/boards.service.js'); 3 | const lists = require('./lists/lists.service.js'); 4 | const cards = require('./cards/cards.service.js'); 5 | const activities = require('./activities/activities.service.js'); 6 | // eslint-disable-next-line no-unused-vars 7 | module.exports = function (app) { 8 | app.configure(users); 9 | app.configure(boards); 10 | app.configure(lists); 11 | app.configure(cards); 12 | app.configure(activities); 13 | }; 14 | -------------------------------------------------------------------------------- /server/src/services/lists/lists.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | const { isBoardOwner } = require('../../hooks/authorization'); 3 | 4 | module.exports = { 5 | before: { 6 | all: [authenticate('jwt'), isBoardOwner], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /server/src/services/lists/lists.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `lists` service on path `/lists` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/lists.model'); 4 | const hooks = require('./lists.hooks'); 5 | 6 | module.exports = function (app) { 7 | const Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | name: 'lists', 12 | Model, 13 | paginate 14 | }; 15 | 16 | // Initialize our service with any options it requires 17 | app.use('/lists', createService(options)); 18 | 19 | // Get our initialized service so that we can register hooks and filters 20 | const service = app.service('lists'); 21 | 22 | service.hooks(hooks); 23 | }; 24 | -------------------------------------------------------------------------------- /server/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() ], 13 | update: [ hashPassword(), authenticate('jwt') ], 14 | patch: [ hashPassword(), 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 | -------------------------------------------------------------------------------- /server/src/services/users/users.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `users` service on path `/users` 2 | const createService = require('feathers-mongoose'); 3 | const createModel = require('../../models/users.model'); 4 | const hooks = require('./users.hooks'); 5 | 6 | module.exports = function (app) { 7 | const Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | name: 'users', 12 | Model, 13 | paginate 14 | }; 15 | 16 | // Initialize our service with any options it requires 17 | app.use('/users', createService(options)); 18 | 19 | // Get our initialized service so that we can register hooks and filters 20 | const service = app.service('users'); 21 | 22 | service.hooks(hooks); 23 | }; 24 | -------------------------------------------------------------------------------- /server/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 | describe('Feathers application tests', () => { 15 | before(function(done) { 16 | this.server = app.listen(port); 17 | this.server.once('listening', () => done()); 18 | }); 19 | 20 | after(function(done) { 21 | this.server.close(done); 22 | }); 23 | 24 | it('starts and shows the index page', () => { 25 | return rp(getUrl()).then(body => 26 | assert.ok(body.indexOf('') !== -1) 27 | ); 28 | }); 29 | 30 | describe('404', function() { 31 | it('shows a 404 HTML page', () => { 32 | return rp({ 33 | url: getUrl('path/to/nowhere'), 34 | headers: { 35 | 'Accept': 'text/html' 36 | } 37 | }).catch(res => { 38 | assert.equal(res.statusCode, 404); 39 | assert.ok(res.error.indexOf('') !== -1); 40 | }); 41 | }); 42 | 43 | it('shows a 404 JSON error without stack trace', () => { 44 | return rp({ 45 | url: getUrl('path/to/nowhere'), 46 | json: true 47 | }).catch(res => { 48 | assert.equal(res.statusCode, 404); 49 | assert.equal(res.error.code, 404); 50 | assert.equal(res.error.message, 'Page not found'); 51 | assert.equal(res.error.name, 'NotFound'); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /server/test/services/activities.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'activities\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('activities'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /server/test/services/boards.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'boards\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('boards'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /server/test/services/cards.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'cards\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('cards'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /server/test/services/lists.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../../src/app'); 3 | 4 | describe('\'lists\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('lists'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /server/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 | -------------------------------------------------------------------------------- /trello-clone-erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingGarden/trello-clone/c5c998cfb51274f9da1b925041e7bef0624fd1bf/trello-clone-erd.png --------------------------------------------------------------------------------