├── .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 |
--------------------------------------------------------------------------------