├── .gitignore ├── LICENSE.md ├── README.md ├── client ├── .env.sample ├── .eslintrc.js ├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── src │ ├── API_URL.js │ ├── App.js │ ├── App.test.js │ ├── components │ ├── FileUpload.js │ ├── Login.js │ └── Map.js │ ├── feathersClient.js │ ├── hooks │ └── useLocation.js │ ├── index.css │ └── index.js └── server ├── .editorconfig ├── .env.sample ├── .eslintrc.js ├── .eslintrc.json ├── .gitignore ├── README.md ├── config ├── default.json ├── production.json └── test.json ├── now.json ├── package-lock.json ├── package.json ├── src ├── app.hooks.js ├── app.js ├── authentication.js ├── channels.js ├── index.js ├── logger.js ├── middleware │ └── index.js ├── models │ └── users.model.js ├── mongoose.js └── services │ ├── index.js │ ├── upload │ ├── upload.class.js │ ├── upload.hooks.js │ └── upload.service.js │ └── users │ ├── users.class.js │ ├── users.hooks.js │ └── users.service.js └── test ├── app.test.js ├── authentication.test.js └── services ├── upload.test.js └── users.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # gatsby files 85 | .cache/ 86 | public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2019 Coding Garden 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnapGarden 2 | 3 | Realtime ephemeral image sharing + maps. 4 | 5 | ## Features 6 | 7 | * [x] Login with Google 8 | * [x] View Map with your current location 9 | * [ ] Upload an image from your current location. 10 | * [ ] Set the radius at which the image can be viewed 11 | * [ ] Set when the image will expire / disappear: 12 | * [ ] After X amount of views 13 | * [ ] After X amount of time 14 | * [ ] Users can see image markers on the map 15 | * [ ] Can choose to view images IF within the specified image radius 16 | 17 | ## Config 18 | 19 | #### Client 20 | * Create a mapbox token [here](https://www.mapbox.com) 21 | * Update client/.env with your mapbox token 22 | 23 | #### Server 24 | * Create a google client id and secret [here](https://console.developers.google.com/) 25 | * Update server/.env with your google client id and secret 26 | 27 | ## Setup / Run 28 | 29 | #### Client 30 | 31 | ```sh 32 | cd client 33 | npm install 34 | npm start 35 | ``` 36 | 37 | #### Server 38 | 39 | ```sh 40 | cd server 41 | npm install 42 | npm run dev 43 | ``` 44 | 45 | ## Stack 46 | 47 | #### Front-end 48 | * [React](https://reactjs.org/) 49 | * [@feathersjs](https://docs.feathersjs.com/api/client.html) client 50 | * A framework for real-time applications and REST APIs 51 | * [socket.io-client](https://github.com/socketio/socket.io-client) 52 | * Realtime client 53 | * [reactn](https://github.com/CharlesStover/reactn) 54 | * React, but with built-in global state management. 55 | * [figbird](https://humaans.github.io/figbird/) 56 | * Declarative data fetching for Feathers and React 57 | * [react-map-gl](https://uber.github.io/react-map-gl/#/) 58 | * React components for Mapbox GL JS 59 | * [react-bootstrap](https://react-bootstrap.github.io) with [bootswatch](https://bootswatch.com/) [lux theme](https://bootswatch.com/lux/) 60 | 61 | #### Back-end 62 | * [Node.js](https://nodejs.org/en/) 63 | * [FeathersJS](https://docs.feathersjs.com/) 64 | * A framework for real-time applications and REST APIs 65 | * [Express](http://expressjs.com/) 66 | * [Mongoose](https://mongoosejs.com) 67 | * elegant mongodb object modeling for node.js 68 | * [socket.io](https://socket.io/) 69 | * Realtime server events 70 | * [grant](https://github.com/simov/grant) with google oauth 71 | * OAuth Middleware for Express, Koa and Hapi 72 | 73 | ## Ideas 74 | 75 | * Joshua Ferris 76 | * You could also add the option to "randomize their location" it slightly skews it - enough to be non specific 77 | --- 78 | * Jesus Bibieca 79 | * allow the user to select how much you should randomize the location 80 | --- 81 | * Joshua Ferris 82 | * Within x distance to view the image 83 | --- 84 | * John Smith 85 | * Remove exif data for privacy reasons 86 | --- 87 | * Omar ElKhatib 88 | * use an ready AI to detect if the photo posted it does not contain any nudes or violence 89 | --- 90 | * "Trusted Users" 91 | * Joshua Ferris 92 | * I mean when they login and you get their profile data - only accept users that have filled fields on their account provider 93 | --- 94 | * Infi 95 | * Image shall be able to be captioned with markdown, with a high character limit (1024) 96 | --- 97 | * Bojan Dedić 98 | * Adding a karma to pics, so the image stays longer on the map if the karma is high, like on reddit. 99 | --- 100 | * Aman Singh 101 | * lets just write a decay rate algorithm per post based on views, likes and comments 102 | 103 | ### Image Filter APIs 104 | 105 | * https://uploadfilter.io/#pricingSection 106 | * https://cloud.google.com/blog/products/gcp/filtering-inappropriate-content-with-the-cloud-vision-api 107 | * https://cloud.google.com/vision/docs/detecting-properties 108 | -------------------------------------------------------------------------------- /client/.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_MapboxAccessToken= -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true 6 | }, 7 | extends: [ 8 | 'airbnb', 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly' 13 | }, 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: 2018, 19 | sourceType: 'module', 20 | }, 21 | plugins: [ 22 | 'react', 23 | ], 24 | rules: { 25 | 'react/jsx-filename-extension': 0, 26 | 'jsx-a11y/accessible-emoji': 0, 27 | 'react/jsx-props-no-spreading': 0 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@feathersjs/authentication-client": "^4.3.3", 7 | "@feathersjs/feathers": "^4.3.3", 8 | "@feathersjs/socketio-client": "^4.3.3", 9 | "bootswatch": "^4.3.1", 10 | "figbird": "^0.3.0", 11 | "react": "^16.9.0", 12 | "react-bootstrap": "^1.0.0-beta.12", 13 | "react-dom": "^16.9.0", 14 | "react-map-gl": "^5.0.11", 15 | "react-scripts": "3.1.2", 16 | "reactn": "^2.2.4", 17 | "socket.io-client": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject", 24 | "lint": "eslint src/" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "eslint": "^6.5.0", 43 | "eslint-config-airbnb": "^18.0.1", 44 | "eslint-plugin-import": "^2.18.2", 45 | "eslint-plugin-jsx-a11y": "^6.2.3", 46 | "eslint-plugin-react": "^7.14.3", 47 | "eslint-plugin-react-hooks": "^1.7.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/src/API_URL.js: -------------------------------------------------------------------------------- 1 | export default window.location.hostname === 'snap.garden' ? 'https://api.snap.garden' : 'http://localhost:3030'; 2 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { setGlobal, useGlobal } from 'reactn'; 2 | import { Provider as FigbirdProvider } from 'figbird'; 3 | 4 | import feathersClient from './feathersClient'; 5 | import Login from './components/Login'; 6 | import Map from './components/Map'; 7 | 8 | setGlobal({ 9 | user: null, 10 | }); 11 | 12 | export default function App() { 13 | const [user] = useGlobal('user'); 14 | return ( 15 | 16 | { 17 | user ? : 18 | } 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/FileUpload.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useMutation } from 'figbird'; 3 | import { Button } from 'react-bootstrap'; 4 | 5 | const FileUpload = () => { 6 | const inputRef = useRef(null); 7 | const { create, loading } = useMutation('upload'); 8 | return ( 9 | <> 10 | 17 | { 19 | const reader = new FileReader(); 20 | reader.addEventListener('load', async () => { 21 | const image = reader.result.slice(22); 22 | const result = await create({ image }); 23 | console.log(result); 24 | }, false); 25 | reader.readAsDataURL(inputRef.current.files[0]); 26 | }} 27 | ref={inputRef} 28 | type="file" 29 | style={{ display: 'none' }} 30 | /> 31 | 32 | ); 33 | }; 34 | 35 | export default FileUpload; 36 | -------------------------------------------------------------------------------- /client/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useGlobal } from 'reactn'; 2 | import { useFeathers } from 'figbird'; 3 | import Button from 'react-bootstrap/Button'; 4 | 5 | import API_URL from '../API_URL'; 6 | 7 | const Login = () => { 8 | const { 1: setUser } = useGlobal('user'); 9 | const feathers = useFeathers(); 10 | 11 | useEffect(() => { 12 | feathers.reAuthenticate().then((user) => { 13 | setUser(user.user); 14 | }).catch((error) => { 15 | console.log('oh no!', error); 16 | }); 17 | }, [feathers, setUser]); 18 | 19 | return ( 20 |
29 | 30 |
31 | ); 32 | }; 33 | 34 | export default Login; 35 | -------------------------------------------------------------------------------- /client/src/components/Map.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'reactn'; 2 | import ReactMapGL, { Marker } from 'react-map-gl'; 3 | import { useFeathers } from 'figbird'; 4 | import Button from 'react-bootstrap/Button'; 5 | import Navbar from 'react-bootstrap/Navbar'; 6 | 7 | import useLocation from '../hooks/useLocation'; 8 | import FileUpload from './FileUpload'; 9 | 10 | const Map = () => { 11 | const feathers = useFeathers(); 12 | const location = useLocation(); 13 | const [viewport, setViewport] = useState({ 14 | width: window.innerWidth, 15 | height: window.innerHeight, 16 | latitude: 31.9742044, 17 | longitude: -49.25875, 18 | zoom: 2, 19 | }); 20 | 21 | useEffect(() => { 22 | const handleResize = () => { 23 | setViewport({ 24 | ...viewport, 25 | width: window.innerWidth, 26 | height: window.innerHeight, 27 | }); 28 | }; 29 | window.addEventListener('resize', handleResize); 30 | return () => { 31 | window.removeEventListener('resize', handleResize); 32 | }; 33 | }); 34 | 35 | useEffect(() => { 36 | if (location) { 37 | setViewport((vp) => ({ 38 | ...vp, 39 | ...location, 40 | zoom: 8, 41 | })); 42 | } 43 | }, [location, setViewport]); 44 | 45 | return ( 46 | <> 47 | 48 | 🌱📸 SnapGarden 📸🌱 49 |
50 | 51 | 60 |
61 |
62 | setViewport(vp)} 67 | > 68 | {location ? ( 69 | 75 | 📸 76 | 77 | ) : null} 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default Map; 84 | 85 | 86 | -------------------------------------------------------------------------------- /client/src/feathersClient.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | import feathers from '@feathersjs/feathers'; 3 | import socketio from '@feathersjs/socketio-client'; 4 | import auth from '@feathersjs/authentication-client'; 5 | 6 | import API_URL from './API_URL'; 7 | 8 | const socket = io(API_URL); 9 | const client = feathers(); 10 | client.configure(socketio(socket)); 11 | client.configure(auth({ 12 | storage: window.localStorage, 13 | })); 14 | 15 | export default client; 16 | -------------------------------------------------------------------------------- /client/src/hooks/useLocation.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'reactn'; 2 | 3 | const options = { 4 | enableHighAccuracy: true, 5 | timeout: 10000, 6 | maximumAge: 0, 7 | }; 8 | 9 | export default function useLocation() { 10 | const [location, setLocation] = useState(null); 11 | useEffect(() => { 12 | console.log('Getting Position...'); 13 | 14 | const onSuccess = ({ 15 | coords: { 16 | latitude, 17 | longitude, 18 | }, 19 | }) => { 20 | setLocation({ 21 | latitude, 22 | longitude, 23 | }); 24 | }; 25 | 26 | const onError = (error) => { 27 | console.error(error); 28 | }; 29 | 30 | navigator.geolocation.getCurrentPosition(onSuccess, onError, options); 31 | }, []); 32 | 33 | return location; 34 | } 35 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #root { 7 | width: 100vw; 8 | height: 100vh; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | .navbar-buttons { 14 | width: 100%; 15 | display: flex; 16 | justify-content: space-between; 17 | } -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import 'bootswatch/dist/lux/bootstrap.min.css'; 5 | import './index.css'; 6 | import App from './App'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | -------------------------------------------------------------------------------- /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 | GOOGLE_OAUTH_KEY= 2 | GOOGLE_OAUTH_SECRET= 3 | IMGUR_CLIENT_ID= -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly', 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | }, 17 | rules: { 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2018 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/tasks.json 42 | !.vscode/launch.json 43 | !.vscode/extensions.json 44 | 45 | ### Linux ### 46 | *~ 47 | 48 | # temporary files which can be created if a process still has a handle open of a deleted file 49 | .fuse_hidden* 50 | 51 | # KDE directory preferences 52 | .directory 53 | 54 | # Linux trash folder which might appear on any partition or disk 55 | .Trash-* 56 | 57 | # .nfs files are created when an open file is removed but is still being accessed 58 | .nfs* 59 | 60 | ### OSX ### 61 | *.DS_Store 62 | .AppleDouble 63 | .LSOverride 64 | 65 | # Icon must end with two \r 66 | Icon 67 | 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Files that might appear in the root of a volume 73 | .DocumentRevisions-V100 74 | .fseventsd 75 | .Spotlight-V100 76 | .TemporaryItems 77 | .Trashes 78 | .VolumeIcon.icns 79 | .com.apple.timemachine.donotpresent 80 | 81 | # Directories potentially created on remote AFP share 82 | .AppleDB 83 | .AppleDesktop 84 | Network Trash Folder 85 | Temporary Items 86 | .apdisk 87 | 88 | ### Windows ### 89 | # Windows thumbnail cache files 90 | Thumbs.db 91 | ehthumbs.db 92 | ehthumbs_vista.db 93 | 94 | # Folder config file 95 | Desktop.ini 96 | 97 | # Recycle Bin used on file shares 98 | $RECYCLE.BIN/ 99 | 100 | # Windows Installer files 101 | *.cab 102 | *.msi 103 | *.msm 104 | *.msp 105 | 106 | # Windows shortcuts 107 | *.lnk 108 | 109 | # Others 110 | lib/ 111 | data/ 112 | 113 | database-data 114 | .env -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # snap-garden 2 | 3 | > 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/snap-garden 18 | npm install 19 | ``` 20 | 21 | 3. Start your app 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Testing 28 | 29 | Simply run `npm test` and all your tests in the `test/` directory will be run. 30 | 31 | ## Scaffolding 32 | 33 | Feathers has a powerful command line interface. Here are a few things it can do: 34 | 35 | ``` 36 | $ npm install -g @feathersjs/cli # Install Feathers CLI 37 | 38 | $ feathers generate service # Generate a new Service 39 | $ feathers generate hook # Generate a new Hook 40 | $ feathers help # Show all commands 41 | ``` 42 | 43 | ## Help 44 | 45 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 46 | -------------------------------------------------------------------------------- /server/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 10, 7 | "max": 50 8 | }, 9 | "authentication": { 10 | "entity": "user", 11 | "service": "users", 12 | "secret": "MAR6KUU694pMf44B2gWmOQ3jjTQ=", 13 | "authStrategies": [ 14 | "jwt", 15 | "local" 16 | ], 17 | "jwtOptions": { 18 | "header": { 19 | "typ": "access" 20 | }, 21 | "audience": "https://snap.garden", 22 | "issuer": "feathers", 23 | "algorithm": "HS256", 24 | "expiresIn": "1d" 25 | }, 26 | "local": { 27 | "usernameField": "email", 28 | "passwordField": "password" 29 | }, 30 | "oauth": { 31 | "redirect": "http://localhost:3000/", 32 | "google": { 33 | "key": "GOOGLE_OAUTH_KEY", 34 | "secret": "GOOGLE_OAUTH_SECRET", 35 | "scope": [ 36 | "email", 37 | "profile", 38 | "openid" 39 | ] 40 | } 41 | } 42 | }, 43 | "mongodb": "mongodb://localhost:27017/snap_garden", 44 | "imgur": "IMGUR_CLIENT_ID" 45 | } 46 | -------------------------------------------------------------------------------- /server/config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "api.snap.garden", 3 | "mongodb": "MONGO_URI", 4 | "authentication": { 5 | "oauth": { 6 | "redirect": "https://snap.garden" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/config/test.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /server/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "alias": [ 4 | "api.snap.garden" 5 | ] 6 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snap-garden", 3 | "description": "", 4 | "version": "0.0.0", 5 | "homepage": "", 6 | "main": "src", 7 | "keywords": [ 8 | "feathers" 9 | ], 10 | "author": { 11 | "name": "w3cj", 12 | "email": "cj@null.computer" 13 | }, 14 | "contributors": [], 15 | "bugs": {}, 16 | "directories": { 17 | "lib": "src", 18 | "test": "test/", 19 | "config": "config/" 20 | }, 21 | "engines": { 22 | "node": "^12.0.0", 23 | "npm": ">= 3.0.0" 24 | }, 25 | "scripts": { 26 | "test": "npm run eslint && npm run jest", 27 | "eslint": "eslint src/. test/. --config .eslintrc.json", 28 | "dev": "nodemon src/", 29 | "start": "node src/", 30 | "jest": "jest --forceExit", 31 | "lint": "eslint src/", 32 | "deploy": "now -e NODE_ENV=production -e GOOGLE_OAUTH_KEY=@snap-garden-google-oauth-key -e GOOGLE_OAUTH_SECRET=@snap-garden-google-oauth-secret -e MONGO_URI=@snap-garden-mongo" 33 | }, 34 | "dependencies": { 35 | "@feathersjs/authentication": "^4.3.3", 36 | "@feathersjs/authentication-local": "^4.3.3", 37 | "@feathersjs/authentication-oauth": "^4.3.3", 38 | "@feathersjs/configuration": "^4.3.3", 39 | "@feathersjs/errors": "^4.3.3", 40 | "@feathersjs/express": "^4.3.3", 41 | "@feathersjs/feathers": "^4.3.3", 42 | "@feathersjs/socketio": "^4.3.3", 43 | "compression": "^1.7.4", 44 | "cors": "^2.8.5", 45 | "dotenv": "^8.1.0", 46 | "feathers-mongoose": "^8.0.2", 47 | "helmet": "^3.21.1", 48 | "mongodb-core": "^3.2.7", 49 | "mongoose": "^5.7.1", 50 | "node-fetch": "^2.6.0", 51 | "serve-favicon": "^2.5.0", 52 | "winston": "^3.2.1" 53 | }, 54 | "devDependencies": { 55 | "axios": "^0.19.0", 56 | "eslint": "^6.5.1", 57 | "eslint-config-airbnb-base": "^14.0.0", 58 | "eslint-plugin-import": "^2.18.2", 59 | "jest": "^24.9.0", 60 | "nodemon": "^1.19.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/app.hooks.js: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | 3 | module.exports = { 4 | before: { 5 | all: [], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [], 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [], 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /server/src/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const favicon = require('serve-favicon'); 3 | const compress = require('compression'); 4 | const helmet = require('helmet'); 5 | const cors = require('cors'); 6 | const dotenv = require('dotenv'); 7 | 8 | dotenv.config(); 9 | 10 | 11 | const feathers = require('@feathersjs/feathers'); 12 | const configuration = require('@feathersjs/configuration'); 13 | const express = require('@feathersjs/express'); 14 | const socketio = require('@feathersjs/socketio'); 15 | const logger = require('./logger'); 16 | 17 | const middleware = require('./middleware'); 18 | const services = require('./services'); 19 | const appHooks = require('./app.hooks'); 20 | const channels = require('./channels'); 21 | 22 | const authentication = require('./authentication'); 23 | 24 | const mongoose = require('./mongoose'); 25 | 26 | const app = express(feathers()); 27 | 28 | // Load app configuration 29 | app.configure(configuration()); 30 | // Enable security, CORS, compression, favicon and body parsing 31 | app.use(helmet()); 32 | app.use(cors()); 33 | app.use(compress()); 34 | app.use(express.json()); 35 | app.use(express.urlencoded({ extended: true })); 36 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 37 | // Host the public folder 38 | app.use('/', express.static(app.get('public'))); 39 | 40 | // Set up Plugins and providers 41 | app.configure(express.rest()); 42 | app.configure(socketio()); 43 | 44 | app.configure(mongoose); 45 | 46 | // Configure other middleware (see `middleware/index.js`) 47 | app.configure(middleware); 48 | app.configure(authentication); 49 | // Set up our services (see `services/index.js`) 50 | app.configure(services); 51 | // Set up event channels (see channels.js) 52 | app.configure(channels); 53 | 54 | // Configure a middleware for 404s and the error handler 55 | app.use(express.notFound()); 56 | app.use(express.errorHandler({ logger })); 57 | 58 | app.hooks(appHooks); 59 | 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /server/src/authentication.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication'); 2 | const { expressOauth, OAuthStrategy } = require('@feathersjs/authentication-oauth'); 3 | 4 | class GoogleStrategy extends OAuthStrategy { 5 | async getEntityData(profile) { 6 | console.log('profile', profile); 7 | const baseData = await super.getEntityData(profile); 8 | console.log('baseData', baseData); 9 | 10 | return { 11 | ...baseData, 12 | picture: profile.picture, 13 | name: profile.name, 14 | email: profile.email, 15 | }; 16 | } 17 | } 18 | 19 | module.exports = (app) => { 20 | const authentication = new AuthenticationService(app); 21 | 22 | authentication.register('jwt', new JWTStrategy()); 23 | authentication.register('google', new GoogleStrategy()); 24 | 25 | app.use('/authentication', authentication); 26 | app.configure(expressOauth()); 27 | }; 28 | -------------------------------------------------------------------------------- /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)) 32 | // user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel)); 33 | 34 | // Easily organize users by email and userid for things like messaging 35 | // app.channel(`emails/${user.email}`).join(channel); 36 | // app.channel(`userIds/$(user.id}`).join(channel); 37 | } 38 | }); 39 | 40 | // eslint-disable-next-line no-unused-vars 41 | app.publish((data, hook) => { 42 | // Here you can add event publishers to channels set up in `channels.js` 43 | // To publish only for a specific event use `app.publish(eventname, () => {})` 44 | 45 | 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 46 | 47 | // e.g. to publish all service events to all authenticated users use 48 | return app.channel('authenticated'); 49 | }); 50 | 51 | // Here you can also add service specific event publishers 52 | // e.g. the publish the `users` service `created` event to the `admins` channel 53 | // app.service('users').publish('created', () => app.channel('admins')); 54 | 55 | // With the userid and email organization from above you can easily select involved users 56 | // app.service('messages').publish(() => { 57 | // return [ 58 | // app.channel(`userIds/${data.createdBy}`), 59 | // app.channel(`emails/${data.recipientEmail}`) 60 | // ]; 61 | // }); 62 | }; 63 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const logger = require('./logger'); 3 | const app = require('./app'); 4 | 5 | const port = app.get('port'); 6 | const server = app.listen(port); 7 | 8 | process.on('unhandledRejection', (reason, p) => logger.error('Unhandled Rejection at: Promise ', p, reason)); 9 | 10 | server.on('listening', () => logger.info('Feathers application started on http://%s:%d', app.get('host'), port)); 11 | -------------------------------------------------------------------------------- /server/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: 'debug', 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 | -------------------------------------------------------------------------------- /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/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 | googleId: { type: String, required: true }, 9 | email: { type: String, required: true }, 10 | name: { type: String, required: true }, 11 | picture: { type: String, required: true }, 12 | }, { 13 | timestamps: true, 14 | }); 15 | 16 | return mongooseClient.model('users', users); 17 | }; 18 | -------------------------------------------------------------------------------- /server/src/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const logger = require('./logger'); 3 | 4 | module.exports = function (app) { 5 | mongoose.connect( 6 | app.get('mongodb'), 7 | { 8 | useCreateIndex: true, 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | }, 12 | ).catch((err) => { 13 | logger.error(err); 14 | process.exit(1); 15 | }); 16 | 17 | mongoose.Promise = global.Promise; 18 | 19 | app.set('mongooseClient', mongoose); 20 | }; 21 | -------------------------------------------------------------------------------- /server/src/services/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users/users.service.js'); 2 | const upload = require('./upload/upload.service.js'); 3 | // eslint-disable-next-line no-unused-vars 4 | module.exports = function (app) { 5 | app.configure(users); 6 | app.configure(upload); 7 | }; 8 | -------------------------------------------------------------------------------- /server/src/services/upload/upload.class.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | /* eslint-disable no-unused-vars */ 4 | exports.Upload = class Upload { 5 | constructor(options, app) { 6 | this.options = options || {}; 7 | this.app = app; 8 | } 9 | 10 | async create(data) { 11 | const headers = { 12 | Authorization: `Client-ID ${this.app.get('imgur')}`, 13 | 'Content-Type': 'application/json', 14 | }; 15 | const response = await fetch('https://api.imgur.com/3/upload', { 16 | method: 'POST', 17 | headers, 18 | body: JSON.stringify({ 19 | image: data.image, 20 | type: 'base64', 21 | }), 22 | }); 23 | const json = await response.json(); 24 | return json; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /server/src/services/upload/upload.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | module.exports = { 4 | before: { 5 | all: [authenticate('jwt')], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [], 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [], 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /server/src/services/upload/upload.service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `upload` service on path `/upload` 2 | const { Upload } = require('./upload.class'); 3 | const hooks = require('./upload.hooks'); 4 | 5 | module.exports = function (app) { 6 | const paginate = app.get('paginate'); 7 | 8 | const options = { 9 | paginate, 10 | }; 11 | 12 | // Initialize our service with any options it requires 13 | app.use('/upload', new Upload(options, app)); 14 | 15 | // Get our initialized service so that we can register hooks 16 | const service = app.service('upload'); 17 | 18 | service.hooks(hooks); 19 | }; 20 | -------------------------------------------------------------------------------- /server/src/services/users/users.class.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-mongoose'); 2 | 3 | exports.Users = class Users extends Service { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /server/src/services/users/users.hooks.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | 4 | module.exports = { 5 | before: { 6 | all: [], 7 | find: [authenticate('jwt')], 8 | get: [authenticate('jwt')], 9 | create: [], 10 | update: [authenticate('jwt')], 11 | patch: [authenticate('jwt')], 12 | remove: [authenticate('jwt')], 13 | }, 14 | 15 | after: { 16 | all: [ 17 | ], 18 | find: [], 19 | get: [], 20 | create: [], 21 | update: [], 22 | patch: [], 23 | remove: [], 24 | }, 25 | 26 | error: { 27 | all: [], 28 | find: [], 29 | get: [], 30 | create: [], 31 | update: [], 32 | patch: [], 33 | remove: [], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /server/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 Model = createModel(app); 8 | const paginate = app.get('paginate'); 9 | 10 | const options = { 11 | Model, 12 | paginate, 13 | }; 14 | 15 | // Initialize our service with any options it requires 16 | app.use('/users', new Users(options, app)); 17 | 18 | // Get our initialized service so that we can register hooks 19 | const service = app.service('users'); 20 | 21 | service.hooks(hooks); 22 | }; 23 | -------------------------------------------------------------------------------- /server/test/app.test.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const url = require('url'); 3 | const app = require('../src/app'); 4 | 5 | const port = app.get('port') || 8998; 6 | const getUrl = pathname => url.format({ 7 | hostname: app.get('host') || 'localhost', 8 | protocol: 'http', 9 | port, 10 | pathname 11 | }); 12 | 13 | describe('Feathers application tests (with jest)', () => { 14 | let server; 15 | 16 | beforeAll(done => { 17 | server = app.listen(port); 18 | server.once('listening', () => done()); 19 | }); 20 | 21 | afterAll(done => { 22 | server.close(done); 23 | }); 24 | 25 | it('starts and shows the index page', async () => { 26 | expect.assertions(1); 27 | 28 | const { data } = await axios.get(getUrl()); 29 | 30 | expect(data.indexOf('')).not.toBe(-1); 31 | }); 32 | 33 | describe('404', () => { 34 | it('shows a 404 HTML page', async () => { 35 | expect.assertions(2); 36 | try { 37 | await axios.get(getUrl('path/to/nowhere'), { 38 | headers: { 39 | 'Accept': 'text/html' 40 | } 41 | }); 42 | } catch (error) { 43 | const { response } = error; 44 | 45 | expect(response.status).toBe(404); 46 | expect(response.data.indexOf('')).not.toBe(-1); 47 | } 48 | }); 49 | 50 | it('shows a 404 JSON error without stack trace', async () => { 51 | expect.assertions(4); 52 | 53 | try { 54 | await axios.get(getUrl('path/to/nowhere')); 55 | } catch (error) { 56 | const { response } = error; 57 | 58 | expect(response.status).toBe(404); 59 | expect(response.data.code).toBe(404); 60 | expect(response.data.message).toBe('Page not found'); 61 | expect(response.data.name).toBe('NotFound'); 62 | } 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /server/test/authentication.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../src/app'); 2 | 3 | describe('authentication', () => { 4 | it('registered the authentication service', () => { 5 | expect(app.service('authentication')).toBeTruthy(); 6 | }); 7 | 8 | }); 9 | -------------------------------------------------------------------------------- /server/test/services/upload.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../../src/app'); 2 | 3 | describe('\'upload\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('upload'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /server/test/services/users.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../../src/app'); 2 | 3 | describe('\'users\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('users'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | --------------------------------------------------------------------------------